From f9501155eaf368203cec662a1ebb7e588ab942fa Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 1 Jul 2025 04:12:05 +0000
Subject: [PATCH 1/5] Initial plan
From b8ae8292a389d647a084cd78a894a560794879dd Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 1 Jul 2025 04:16:34 +0000
Subject: [PATCH 2/5] Initial exploration and setup for timeout configuration
Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com>
---
src/Teapot.Web.Tests/Teapot.Web.Tests.csproj | 4 ++--
src/Teapot.Web/Teapot.Web.csproj | 5 +----
2 files changed, 3 insertions(+), 6 deletions(-)
diff --git a/src/Teapot.Web.Tests/Teapot.Web.Tests.csproj b/src/Teapot.Web.Tests/Teapot.Web.Tests.csproj
index 19c2858..2a138da 100644
--- a/src/Teapot.Web.Tests/Teapot.Web.Tests.csproj
+++ b/src/Teapot.Web.Tests/Teapot.Web.Tests.csproj
@@ -1,7 +1,7 @@
- net9.0
+ net8.0
enable
enable
false
@@ -9,7 +9,7 @@
-
+
diff --git a/src/Teapot.Web/Teapot.Web.csproj b/src/Teapot.Web/Teapot.Web.csproj
index 543c416..978ca43 100644
--- a/src/Teapot.Web/Teapot.Web.csproj
+++ b/src/Teapot.Web/Teapot.Web.csproj
@@ -1,7 +1,7 @@
- net9.0
+ net8.0
enable
26052284-5a8d-4e62-83dc-7e394f7fa038
Linux
@@ -11,9 +11,6 @@
-
- All
-
From de4533af454c6a9e47304794740fd1b4dae118a3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 1 Jul 2025 04:27:35 +0000
Subject: [PATCH 3/5] Implement configurable timeout with 30-second default
Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com>
---
src/Teapot.Web.Tests/Teapot.Web.Tests.csproj | 4 +-
src/Teapot.Web.Tests/UnitTests/SleepTests.cs | 63 +++++++++++++++++--
.../UnitTests/SuppressBodyTests.cs | 11 ++--
.../Configuration/TimeoutOptions.cs | 14 +++++
src/Teapot.Web/CustomHttpStatusCodeResult.cs | 6 +-
src/Teapot.Web/Pages/Index.cshtml | 4 +-
src/Teapot.Web/Program.cs | 6 ++
src/Teapot.Web/StatusExtensions.cs | 14 +++--
src/Teapot.Web/Teapot.Web.csproj | 5 +-
src/Teapot.Web/appsettings.json | 3 +
10 files changed, 109 insertions(+), 21 deletions(-)
create mode 100644 src/Teapot.Web/Configuration/TimeoutOptions.cs
diff --git a/src/Teapot.Web.Tests/Teapot.Web.Tests.csproj b/src/Teapot.Web.Tests/Teapot.Web.Tests.csproj
index 2a138da..19c2858 100644
--- a/src/Teapot.Web.Tests/Teapot.Web.Tests.csproj
+++ b/src/Teapot.Web.Tests/Teapot.Web.Tests.csproj
@@ -1,7 +1,7 @@
- net8.0
+ net9.0
enable
enable
false
@@ -9,7 +9,7 @@
-
+
diff --git a/src/Teapot.Web.Tests/UnitTests/SleepTests.cs b/src/Teapot.Web.Tests/UnitTests/SleepTests.cs
index 98c4b62..cdff964 100644
--- a/src/Teapot.Web.Tests/UnitTests/SleepTests.cs
+++ b/src/Teapot.Web.Tests/UnitTests/SleepTests.cs
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Http;
+using Teapot.Web.Configuration;
using Teapot.Web.Models;
using Teapot.Web.Models.Unofficial;
@@ -7,6 +8,7 @@ public class SleepTests {
private const int Sleep = 500;
private TeapotStatusCodeMetadataCollection _statusCodes;
+ private TimeoutOptions _timeoutOptions;
[SetUp]
public void Setup() {
@@ -19,13 +21,14 @@ public void Setup() {
new NginxStatusCodeMetadata(),
new TwitterStatusCodeMetadata()
);
+ _timeoutOptions = new TimeoutOptions();
}
[Test]
public void SleepReadFromQuery()
{
Mock request = HttpRequestHelper.GenerateMockRequest();
- IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200, new(), sleep: Sleep), null, request.Object);
+ IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200, new(), sleep: Sleep), null, request.Object, _timeoutOptions);
Assert.Multiple(() =>
{
@@ -42,7 +45,7 @@ public void SleepReadFromHeader()
Mock request = HttpRequestHelper.GenerateMockRequest();
request.Object.Headers.Append(StatusExtensions.SLEEP_HEADER, Sleep.ToString());
- IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200, new()), null, request.Object);
+ IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200, new()), null, request.Object, _timeoutOptions);
Assert.Multiple(() => {
Assert.That(result, Is.InstanceOf());
@@ -57,7 +60,7 @@ public void SleepReadFromQSTakesPriorityHeader() {
Mock request = HttpRequestHelper.GenerateMockRequest();
request.Object.Headers.Append(StatusExtensions.SLEEP_HEADER, Sleep.ToString());
- IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200, new(), sleep: Sleep * 2), null, request.Object);
+ IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200, new(), sleep: Sleep * 2), null, request.Object, _timeoutOptions);
Assert.Multiple(() => {
Assert.That(result, Is.InstanceOf());
@@ -73,7 +76,7 @@ public void BadSleepHeaderIgnored()
Mock request = HttpRequestHelper.GenerateMockRequest();
request.Object.Headers.Append(StatusExtensions.SLEEP_HEADER, "invalid");
- IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200, new()), null, request.Object);
+ IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200, new()), null, request.Object, _timeoutOptions);
Assert.Multiple(() =>
{
@@ -84,4 +87,56 @@ public void BadSleepHeaderIgnored()
});
}
+ [Test]
+ public void SleepMaxTimeout_DefaultIsThirtySeconds()
+ {
+ // Verify default timeout is 30 seconds (30,000 ms)
+ var defaultTimeoutOptions = new TimeoutOptions();
+ Assert.That(defaultTimeoutOptions.MaxSleepMilliseconds, Is.EqualTo(30 * 1000));
+ }
+
+ [Test]
+ public void SleepMaxTimeout_ConfigurableViaOptions()
+ {
+ // Verify timeout can be configured
+ var customTimeoutOptions = new TimeoutOptions { MaxSleepMilliseconds = 60 * 1000 }; // 1 minute
+ Mock request = HttpRequestHelper.GenerateMockRequest();
+
+ IResult result = StatusExtensions.CommonHandleStatusRequestAsync(
+ new ResponseOptions(200, new(), sleep: 45 * 1000), // 45 seconds
+ null,
+ request.Object,
+ customTimeoutOptions);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(result, Is.InstanceOf());
+ CustomHttpStatusCodeResult r = (CustomHttpStatusCodeResult)result;
+ Assert.That(r.Options.Sleep, Is.EqualTo(45 * 1000));
+ });
+ }
+
+ [Test]
+ public void SleepMaxTimeout_ClampsExcessiveValues()
+ {
+ // Test that values exceeding the max are clamped
+ var timeoutOptions = new TimeoutOptions { MaxSleepMilliseconds = 30 * 1000 }; // 30 seconds
+ Mock request = HttpRequestHelper.GenerateMockRequest();
+
+ IResult result = StatusExtensions.CommonHandleStatusRequestAsync(
+ new ResponseOptions(200, new(), sleep: 60 * 1000), // 60 seconds - should be clamped to 30
+ null,
+ request.Object,
+ timeoutOptions);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(result, Is.InstanceOf());
+ CustomHttpStatusCodeResult r = (CustomHttpStatusCodeResult)result;
+ // The sleep value in ResponseOptions should remain as requested,
+ // but the actual clamping happens in DoSleep method
+ Assert.That(r.Options.Sleep, Is.EqualTo(60 * 1000));
+ });
+ }
+
}
diff --git a/src/Teapot.Web.Tests/UnitTests/SuppressBodyTests.cs b/src/Teapot.Web.Tests/UnitTests/SuppressBodyTests.cs
index fb6e504..f0b3543 100644
--- a/src/Teapot.Web.Tests/UnitTests/SuppressBodyTests.cs
+++ b/src/Teapot.Web.Tests/UnitTests/SuppressBodyTests.cs
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Http;
+using Teapot.Web.Configuration;
using Teapot.Web.Models;
using Teapot.Web.Models.Unofficial;
@@ -6,6 +7,7 @@ namespace Teapot.Web.Tests.UnitTests;
public class SuppressBodyTests
{
private TeapotStatusCodeMetadataCollection _statusCodes;
+ private TimeoutOptions _timeoutOptions;
[SetUp]
public void Setup()
@@ -19,6 +21,7 @@ public void Setup()
new NginxStatusCodeMetadata(),
new TwitterStatusCodeMetadata()
);
+ _timeoutOptions = new TimeoutOptions();
}
[Test]
@@ -28,7 +31,7 @@ public void Setup()
public void SuppressBodyReadFromQuery(bool? suppressBody)
{
Mock request = HttpRequestHelper.GenerateMockRequest();
- IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200, new(), suppressBody: suppressBody), null, request.Object);
+ IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200, new(), suppressBody: suppressBody), null, request.Object, _timeoutOptions);
Assert.Multiple(() =>
{
@@ -48,7 +51,7 @@ public void SuppressBodyReadFromHeader(string? suppressBody)
Mock request = HttpRequestHelper.GenerateMockRequest();
request.Object.Headers.Append(StatusExtensions.SUPPRESS_BODY_HEADER, suppressBody);
- IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200, new()), null, request.Object);
+ IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200, new()), null, request.Object, _timeoutOptions);
Assert.Multiple(() =>
{
@@ -76,7 +79,7 @@ public void SuppressBodyReadFromQSTakesPriorityHeader(string? headerValue, bool?
Mock request = HttpRequestHelper.GenerateMockRequest();
request.Object.Headers.Append(StatusExtensions.SUPPRESS_BODY_HEADER, headerValue);
- IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200, new(), suppressBody: queryStringValue), null, request.Object);
+ IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200, new(), suppressBody: queryStringValue), null, request.Object, _timeoutOptions);
Assert.Multiple(() =>
{
@@ -98,7 +101,7 @@ public void BadSuppressBodyHeaderIgnored()
Mock request = HttpRequestHelper.GenerateMockRequest();
request.Object.Headers.Append(StatusExtensions.SUPPRESS_BODY_HEADER, "invalid");
- IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200, new()), null, request.Object);
+ IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200, new()), null, request.Object, _timeoutOptions);
Assert.Multiple(() =>
{
diff --git a/src/Teapot.Web/Configuration/TimeoutOptions.cs b/src/Teapot.Web/Configuration/TimeoutOptions.cs
new file mode 100644
index 0000000..4da1b25
--- /dev/null
+++ b/src/Teapot.Web/Configuration/TimeoutOptions.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Teapot.Web.Configuration;
+
+public class TimeoutOptions
+{
+ public const string SectionName = "Timeout";
+
+ ///
+ /// Maximum sleep timeout in milliseconds. Default is 30 seconds (30,000 ms).
+ ///
+ [Range(0, int.MaxValue, ErrorMessage = "MaxSleepMilliseconds must be a non-negative value")]
+ public int MaxSleepMilliseconds { get; set; } = 30 * 1000; // 30 seconds default
+}
\ No newline at end of file
diff --git a/src/Teapot.Web/CustomHttpStatusCodeResult.cs b/src/Teapot.Web/CustomHttpStatusCodeResult.cs
index 988d901..98c1067 100644
--- a/src/Teapot.Web/CustomHttpStatusCodeResult.cs
+++ b/src/Teapot.Web/CustomHttpStatusCodeResult.cs
@@ -10,10 +10,10 @@
namespace Teapot.Web;
-public class CustomHttpStatusCodeResult(ResponseOptions options) : IResult
+public class CustomHttpStatusCodeResult(ResponseOptions options, int maxSleepMilliseconds = 30 * 1000) : IResult
{
private const int SLEEP_MIN = 0;
- private const int SLEEP_MAX = 5 * 60 * 1000; // 5 mins in milliseconds
+ private readonly int SLEEP_MAX = maxSleepMilliseconds;
internal static readonly string[] onlySingleHeader = ["Location", "Retry-After"];
private static readonly MediaTypeHeaderValue jsonMimeType = new("application/json");
@@ -120,7 +120,7 @@ public async Task ExecuteAsync(HttpContext context)
}
}
- private static async Task DoSleep(int? sleep)
+ private async Task DoSleep(int? sleep)
{
int sleepData = Math.Clamp(sleep ?? 0, SLEEP_MIN, SLEEP_MAX);
if (sleepData > 0)
diff --git a/src/Teapot.Web/Pages/Index.cshtml b/src/Teapot.Web/Pages/Index.cshtml
index 96218df..169f0ef 100644
--- a/src/Teapot.Web/Pages/Index.cshtml
+++ b/src/Teapot.Web/Pages/Index.cshtml
@@ -31,10 +31,10 @@
- If you want a delay on the response add a query string or provide a header of @StatusExtensions.SLEEP_HEADER for the sleep duration (the time in ms, max 5 minutes*), like this:
+ If you want a delay on the response add a query string or provide a header of @StatusExtensions.SLEEP_HEADER for the sleep duration (the time in ms, max 30 seconds*), like this:
httpstat.us/200?sleep=5000.
- *When using the hosted instance the timeout is actually 230 seconds, which is the max timeout allowed by an Azure App Service (see this thread post). If you host it yourself expect the limits to be different.
+ *The default maximum timeout is 30 seconds, but this can be configured. When using the hosted instance the timeout may be further limited by the hosting environment (such as Azure App Service's 230-second limit).
diff --git a/src/Teapot.Web/Program.cs b/src/Teapot.Web/Program.cs
index dbecf5c..fa3cfba 100644
--- a/src/Teapot.Web/Program.cs
+++ b/src/Teapot.Web/Program.cs
@@ -3,6 +3,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Teapot.Web;
+using Teapot.Web.Configuration;
using Teapot.Web.Models;
using Teapot.Web.Models.Unofficial;
@@ -21,6 +22,11 @@
builder.Services.AddSingleton();
builder.Services.AddApplicationInsightsTelemetry();
+// Configure timeout options
+builder.Services.Configure(builder.Configuration.GetSection(TimeoutOptions.SectionName));
+builder.Services.AddSingleton(provider =>
+ provider.GetRequiredService>().Value);
+
//builder.Services.AddFairUseRateLimiter();
builder.Services.AddCors();
diff --git a/src/Teapot.Web/StatusExtensions.cs b/src/Teapot.Web/StatusExtensions.cs
index 04139f5..e9d5213 100644
--- a/src/Teapot.Web/StatusExtensions.cs
+++ b/src/Teapot.Web/StatusExtensions.cs
@@ -9,6 +9,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Teapot.Web.Configuration;
using Teapot.Web.Models;
namespace Teapot.Web;
@@ -49,7 +50,8 @@ internal static IResult HandleStatusRequestAsync(
bool? suppressBody,
string? wildcard,
HttpRequest req,
- [FromServices] TeapotStatusCodeMetadataCollection statusCodes
+ [FromServices] TeapotStatusCodeMetadataCollection statusCodes,
+ [FromServices] TimeoutOptions timeoutOptions
)
{
TeapotStatusCodeMetadata statusData = statusCodes.TryGetValue(status, out TeapotStatusCodeMetadata? value) ?
@@ -61,13 +63,14 @@ [FromServices] TeapotStatusCodeMetadataCollection statusCodes
Sleep = sleep,
SuppressBody = suppressBody
};
- return CommonHandleStatusRequestAsync(options, wildcard, req);
+ return CommonHandleStatusRequestAsync(options, wildcard, req, timeoutOptions);
}
internal static IResult CommonHandleStatusRequestAsync(
ResponseOptions options,
string? wildcard,
- HttpRequest req)
+ HttpRequest req,
+ TimeoutOptions timeoutOptions)
{
options.Sleep ??= ParseHeaderInt(req, SLEEP_HEADER);
options.SleepAfterHeaders ??= ParseHeaderInt(req, SLEEP_AFTER_HEADERS);
@@ -83,12 +86,13 @@ internal static IResult CommonHandleStatusRequestAsync(
header => header.Key.Replace(CUSTOM_RESPONSE_HEADER_PREFIX, string.Empty, StringComparison.InvariantCultureIgnoreCase),
header => header.Value);
- return new CustomHttpStatusCodeResult(options);
+ return new CustomHttpStatusCodeResult(options, timeoutOptions.MaxSleepMilliseconds);
}
internal static IResult HandleRandomRequest(
HttpRequest req,
[FromServices] TeapotStatusCodeMetadataCollection statusCodes,
+ [FromServices] TimeoutOptions timeoutOptions,
int? sleep,
bool? suppressBody,
string? wildcard,
@@ -102,7 +106,7 @@ internal static IResult HandleRandomRequest(
new TeapotStatusCodeMetadata { Description = $"{status} Unknown Code" };
ResponseOptions options = new(status, statusData);
- return CommonHandleStatusRequestAsync(options, wildcard, req);
+ return CommonHandleStatusRequestAsync(options, wildcard, req, timeoutOptions);
}
catch
{
diff --git a/src/Teapot.Web/Teapot.Web.csproj b/src/Teapot.Web/Teapot.Web.csproj
index 978ca43..543c416 100644
--- a/src/Teapot.Web/Teapot.Web.csproj
+++ b/src/Teapot.Web/Teapot.Web.csproj
@@ -1,7 +1,7 @@
- net8.0
+ net9.0
enable
26052284-5a8d-4e62-83dc-7e394f7fa038
Linux
@@ -11,6 +11,9 @@
+
+ All
+
diff --git a/src/Teapot.Web/appsettings.json b/src/Teapot.Web/appsettings.json
index 0972fa6..478eb6c 100644
--- a/src/Teapot.Web/appsettings.json
+++ b/src/Teapot.Web/appsettings.json
@@ -10,5 +10,8 @@
"EndpointDefaults": {
"Protocols": "Http1AndHttp2AndHttp3"
}
+ },
+ "Timeout": {
+ "MaxSleepMilliseconds": 30000
}
}
From e24ddb6d885108241502f7b5013f4c6b5cdae92d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 1 Jul 2025 04:50:51 +0000
Subject: [PATCH 4/5] Remove Timeout configuration from appsettings.json to use
hardcoded default
Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com>
---
src/Teapot.Web/appsettings.json | 3 ---
1 file changed, 3 deletions(-)
diff --git a/src/Teapot.Web/appsettings.json b/src/Teapot.Web/appsettings.json
index 478eb6c..0972fa6 100644
--- a/src/Teapot.Web/appsettings.json
+++ b/src/Teapot.Web/appsettings.json
@@ -10,8 +10,5 @@
"EndpointDefaults": {
"Protocols": "Http1AndHttp2AndHttp3"
}
- },
- "Timeout": {
- "MaxSleepMilliseconds": 30000
}
}
From 454def0aa5231626826b5ceb24e23e70ab7f9161 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 1 Jul 2025 05:27:13 +0000
Subject: [PATCH 5/5] Return BadRequest for sleep values exceeding max timeout
Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com>
---
.../IntegrationTests/TimeoutTests.cs | 79 ++++++++++++++++++
src/Teapot.Web.Tests/UnitTests/SleepTests.cs | 82 +++++++++++++++++--
src/Teapot.Web/StatusExtensions.cs | 7 ++
3 files changed, 162 insertions(+), 6 deletions(-)
create mode 100644 src/Teapot.Web.Tests/IntegrationTests/TimeoutTests.cs
diff --git a/src/Teapot.Web.Tests/IntegrationTests/TimeoutTests.cs b/src/Teapot.Web.Tests/IntegrationTests/TimeoutTests.cs
new file mode 100644
index 0000000..ce7e7aa
--- /dev/null
+++ b/src/Teapot.Web.Tests/IntegrationTests/TimeoutTests.cs
@@ -0,0 +1,79 @@
+using Microsoft.AspNetCore.Mvc.Testing;
+
+namespace Teapot.Web.Tests.IntegrationTests;
+
+[TestFixtureSource(typeof(HttpMethods), nameof(HttpMethods.All))]
+public class TimeoutTests(HttpMethod httpMethod)
+{
+ [OneTimeSetUp]
+ public void OneTimeSetUp()
+ {
+ _httpClient = new WebApplicationFactory().CreateDefaultClient();
+ }
+
+ [OneTimeTearDown]
+ public void OneTimeTearDown()
+ {
+ _httpClient.Dispose();
+ }
+
+ private HttpClient _httpClient = null!;
+
+ [Test]
+ public async Task SleepExceedsMaxTimeout_ReturnsBadRequest()
+ {
+ // Default max timeout is 30 seconds (30,000 ms), try 60 seconds
+ string uri = "/200?sleep=60000";
+ using HttpRequestMessage httpRequest = new(httpMethod, uri);
+ using HttpResponseMessage response = await _httpClient.SendAsync(httpRequest);
+ Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
+ }
+
+ [Test]
+ public async Task SleepAfterHeadersExceedsMaxTimeout_ReturnsBadRequest()
+ {
+ // Default max timeout is 30 seconds (30,000 ms), try 60 seconds via header
+ string uri = "/200";
+ using HttpRequestMessage httpRequest = new(httpMethod, uri);
+ httpRequest.Headers.Add("X-HttpStatus-SleepAfterHeaders", "60000");
+ using HttpResponseMessage response = await _httpClient.SendAsync(httpRequest);
+ Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
+ }
+
+ [Test]
+ public async Task SleepWithinMaxTimeout_Succeeds()
+ {
+ // Default max timeout is 30 seconds (30,000 ms), try 5 seconds
+ string uri = "/200?sleep=5000";
+ using HttpRequestMessage httpRequest = new(httpMethod, uri);
+ using HttpResponseMessage response = await _httpClient.SendAsync(httpRequest);
+ Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
+
+ string body = await response.Content.ReadAsStringAsync();
+ Assert.That(body, Does.Contain("200"));
+ }
+
+ [Test]
+ public async Task SleepEqualToMaxTimeout_Succeeds()
+ {
+ // Default max timeout is 30 seconds (30,000 ms), try exactly 30 seconds
+ // But for testing purposes, let's use a smaller value to avoid long test runs
+ string uri = "/200?sleep=1000"; // 1 second - well within limit
+ using HttpRequestMessage httpRequest = new(httpMethod, uri);
+ using HttpResponseMessage response = await _httpClient.SendAsync(httpRequest);
+ Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
+
+ string body = await response.Content.ReadAsStringAsync();
+ Assert.That(body, Does.Contain("200"));
+ }
+
+ [Test]
+ public async Task SleepJustOverMaxTimeout_ReturnsBadRequest()
+ {
+ // Default max timeout is 30 seconds (30,000 ms), try 30,001 ms
+ string uri = "/200?sleep=30001";
+ using HttpRequestMessage httpRequest = new(httpMethod, uri);
+ using HttpResponseMessage response = await _httpClient.SendAsync(httpRequest);
+ Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
+ }
+}
\ No newline at end of file
diff --git a/src/Teapot.Web.Tests/UnitTests/SleepTests.cs b/src/Teapot.Web.Tests/UnitTests/SleepTests.cs
index cdff964..c748a4a 100644
--- a/src/Teapot.Web.Tests/UnitTests/SleepTests.cs
+++ b/src/Teapot.Web.Tests/UnitTests/SleepTests.cs
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.HttpResults;
using Teapot.Web.Configuration;
using Teapot.Web.Models;
using Teapot.Web.Models.Unofficial;
@@ -117,14 +118,85 @@ public void SleepMaxTimeout_ConfigurableViaOptions()
}
[Test]
- public void SleepMaxTimeout_ClampsExcessiveValues()
+ public void SleepMaxTimeout_ExcessiveValuesReturnBadRequest()
{
- // Test that values exceeding the max are clamped
+ // Test that values exceeding the max now return BadRequest instead of being clamped
var timeoutOptions = new TimeoutOptions { MaxSleepMilliseconds = 30 * 1000 }; // 30 seconds
Mock request = HttpRequestHelper.GenerateMockRequest();
IResult result = StatusExtensions.CommonHandleStatusRequestAsync(
- new ResponseOptions(200, new(), sleep: 60 * 1000), // 60 seconds - should be clamped to 30
+ new ResponseOptions(200, new(), sleep: 60 * 1000), // 60 seconds - exceeds 30 second max
+ null,
+ request.Object,
+ timeoutOptions);
+
+ // Should now return BadRequest instead of being clamped
+ Assert.That(result, Is.InstanceOf());
+ }
+
+ [Test]
+ public void SleepExceedsMaxTimeout_ReturnsBadRequest()
+ {
+ // Test that sleep values exceeding the max timeout return BadRequest
+ var timeoutOptions = new TimeoutOptions { MaxSleepMilliseconds = 30 * 1000 }; // 30 seconds
+ Mock request = HttpRequestHelper.GenerateMockRequest();
+
+ IResult result = StatusExtensions.CommonHandleStatusRequestAsync(
+ new ResponseOptions(200, new(), sleep: 60 * 1000), // 60 seconds - exceeds 30 second max
+ null,
+ request.Object,
+ timeoutOptions);
+
+ Assert.That(result, Is.InstanceOf());
+ }
+
+ [Test]
+ public void SleepAfterHeadersExceedsMaxTimeout_ReturnsBadRequest()
+ {
+ // Test that sleepAfterHeaders values exceeding the max timeout return BadRequest
+ var timeoutOptions = new TimeoutOptions { MaxSleepMilliseconds = 30 * 1000 }; // 30 seconds
+ Mock request = HttpRequestHelper.GenerateMockRequest();
+ request.Object.Headers.Append(StatusExtensions.SLEEP_AFTER_HEADERS, (60 * 1000).ToString()); // 60 seconds
+
+ IResult result = StatusExtensions.CommonHandleStatusRequestAsync(
+ new ResponseOptions(200, new()),
+ null,
+ request.Object,
+ timeoutOptions);
+
+ Assert.That(result, Is.InstanceOf());
+ }
+
+ [Test]
+ public void SleepWithinMaxTimeout_Succeeds()
+ {
+ // Test that sleep values within the max timeout work normally
+ var timeoutOptions = new TimeoutOptions { MaxSleepMilliseconds = 30 * 1000 }; // 30 seconds
+ Mock request = HttpRequestHelper.GenerateMockRequest();
+
+ IResult result = StatusExtensions.CommonHandleStatusRequestAsync(
+ new ResponseOptions(200, new(), sleep: 15 * 1000), // 15 seconds - within 30 second max
+ null,
+ request.Object,
+ timeoutOptions);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(result, Is.InstanceOf());
+ CustomHttpStatusCodeResult r = (CustomHttpStatusCodeResult)result;
+ Assert.That(r.Options.Sleep, Is.EqualTo(15 * 1000));
+ });
+ }
+
+ [Test]
+ public void SleepEqualToMaxTimeout_Succeeds()
+ {
+ // Test that sleep values equal to the max timeout work normally
+ var timeoutOptions = new TimeoutOptions { MaxSleepMilliseconds = 30 * 1000 }; // 30 seconds
+ Mock request = HttpRequestHelper.GenerateMockRequest();
+
+ IResult result = StatusExtensions.CommonHandleStatusRequestAsync(
+ new ResponseOptions(200, new(), sleep: 30 * 1000), // 30 seconds - exactly at max
null,
request.Object,
timeoutOptions);
@@ -133,9 +205,7 @@ public void SleepMaxTimeout_ClampsExcessiveValues()
{
Assert.That(result, Is.InstanceOf());
CustomHttpStatusCodeResult r = (CustomHttpStatusCodeResult)result;
- // The sleep value in ResponseOptions should remain as requested,
- // but the actual clamping happens in DoSleep method
- Assert.That(r.Options.Sleep, Is.EqualTo(60 * 1000));
+ Assert.That(r.Options.Sleep, Is.EqualTo(30 * 1000));
});
}
diff --git a/src/Teapot.Web/StatusExtensions.cs b/src/Teapot.Web/StatusExtensions.cs
index e9d5213..459c2c2 100644
--- a/src/Teapot.Web/StatusExtensions.cs
+++ b/src/Teapot.Web/StatusExtensions.cs
@@ -86,6 +86,13 @@ internal static IResult CommonHandleStatusRequestAsync(
header => header.Key.Replace(CUSTOM_RESPONSE_HEADER_PREFIX, string.Empty, StringComparison.InvariantCultureIgnoreCase),
header => header.Value);
+ // Check if any sleep values exceed the maximum timeout
+ if ((options.Sleep.HasValue && options.Sleep.Value > timeoutOptions.MaxSleepMilliseconds) ||
+ (options.SleepAfterHeaders.HasValue && options.SleepAfterHeaders.Value > timeoutOptions.MaxSleepMilliseconds))
+ {
+ return TypedResults.BadRequest();
+ }
+
return new CustomHttpStatusCodeResult(options, timeoutOptions.MaxSleepMilliseconds);
}