From 926c0a61e81be3e64708c0394a8bc882640e047f Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 27 Feb 2025 23:15:09 +0100 Subject: [PATCH 1/5] - refactor ResponseOptions parameters - add ability to abort the response --- .../IntegrationTests/StatusCodeTests.cs | 2 +- .../CustomHttpStatusCodeResultTests.cs | 8 +-- src/Teapot.Web.Tests/UnitTests/SleepTests.cs | 22 ++++--- .../UnitTests/SuppressBodyTests.cs | 16 ++--- src/Teapot.Web/CustomHttpStatusCodeResult.cs | 61 ++++++++++++------- src/Teapot.Web/ResponseOptions.cs | 39 ++++++++++++ src/Teapot.Web/StatusExtensions.cs | 59 ++++++++++++------ 7 files changed, 145 insertions(+), 62 deletions(-) create mode 100644 src/Teapot.Web/ResponseOptions.cs diff --git a/src/Teapot.Web.Tests/IntegrationTests/StatusCodeTests.cs b/src/Teapot.Web.Tests/IntegrationTests/StatusCodeTests.cs index b2c1a51..e7f8ae2 100644 --- a/src/Teapot.Web.Tests/IntegrationTests/StatusCodeTests.cs +++ b/src/Teapot.Web.Tests/IntegrationTests/StatusCodeTests.cs @@ -39,7 +39,7 @@ public async Task ResponseWithContent([Values] TestCase testCase) [TestCaseSource(typeof(TestCases), nameof(TestCases.StatusCodesWithContent))] public async Task ResponseWithContentSuppressedViaQs([Values] TestCase testCase) { - string uri = $"/{testCase.Code}?{nameof(CustomHttpStatusCodeResult.SuppressBody)}=true"; + string uri = $"/{testCase.Code}?{nameof(ResponseOptions.SuppressBody)}=true"; using HttpRequestMessage httpRequest = new(httpMethod, uri); using HttpResponseMessage response = await _httpClient.SendAsync(httpRequest); Assert.That((int)response.StatusCode, Is.EqualTo(testCase.Code)); diff --git a/src/Teapot.Web.Tests/UnitTests/CustomHttpStatusCodeResultTests.cs b/src/Teapot.Web.Tests/UnitTests/CustomHttpStatusCodeResultTests.cs index c40f485..eae8f41 100644 --- a/src/Teapot.Web.Tests/UnitTests/CustomHttpStatusCodeResultTests.cs +++ b/src/Teapot.Web.Tests/UnitTests/CustomHttpStatusCodeResultTests.cs @@ -38,7 +38,7 @@ public void Setup() [TestCaseSource(typeof(TestCases), nameof(TestCases.StatusCodesAll))] public async Task Response_Is_Correct(TestCase testCase) { - CustomHttpStatusCodeResult target = new(testCase.Code, testCase.TeapotStatusCodeMetadata, null, null, []); + CustomHttpStatusCodeResult target = new(new ResponseOptions(testCase.Code, metadata: testCase.TeapotStatusCodeMetadata)); await target.ExecuteAsync(_httpContext); Assert.Multiple(() => @@ -68,7 +68,7 @@ public async Task Response_Is_Correct(TestCase testCase) [TestCaseSource(typeof(TestCases), nameof(TestCases.StatusCodesAll))] public async Task Response_Json_Is_Correct(TestCase testCase) { - CustomHttpStatusCodeResult target = new(testCase.Code, testCase.TeapotStatusCodeMetadata, null, null, []); + CustomHttpStatusCodeResult target = new(new ResponseOptions(testCase.Code, metadata: testCase.TeapotStatusCodeMetadata)); _httpContext.Request.Headers.Accept = "application/json"; @@ -103,7 +103,7 @@ public async Task Response_No_Content(TestCase testCase) _httpContext.Response.Headers.ContentType = "text/plain"; _httpContext.Response.Headers["Content-Length"] = "42"; - CustomHttpStatusCodeResult target = new(testCase.Code, testCase.TeapotStatusCodeMetadata, null, null, []); + CustomHttpStatusCodeResult target = new(new ResponseOptions(testCase.Code, metadata: testCase.TeapotStatusCodeMetadata)); await target.ExecuteAsync(_httpContext); Assert.Multiple(() => @@ -129,7 +129,7 @@ public async Task Response_Retry_After_Single_Header(TestCase testCase) { "Retry-After", new StringValues("42") } }; - CustomHttpStatusCodeResult target = new(testCase.Code, testCase.TeapotStatusCodeMetadata, sleep: null, suppressBody: null, customHeaders); + CustomHttpStatusCodeResult target = new(new ResponseOptions(testCase.Code, metadata: testCase.TeapotStatusCodeMetadata, customHeaders: customHeaders)); await target.ExecuteAsync(_httpContext); Assert.That(_httpContext.Response.Headers.RetryAfter, Has.Count.EqualTo(1)); } diff --git a/src/Teapot.Web.Tests/UnitTests/SleepTests.cs b/src/Teapot.Web.Tests/UnitTests/SleepTests.cs index 7f855c4..d61a527 100644 --- a/src/Teapot.Web.Tests/UnitTests/SleepTests.cs +++ b/src/Teapot.Web.Tests/UnitTests/SleepTests.cs @@ -22,15 +22,17 @@ public void Setup() { } [Test] - public void SleepReadFromQuery() { + public void SleepReadFromQuery() + { Mock request = HttpRequestHelper.GenerateMockRequest(); - IResult result = StatusExtensions.HandleStatusRequestAsync(200, Sleep, null, null, request.Object, _statusCodes); + IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200, sleep: Sleep), null, request.Object, _statusCodes); - Assert.Multiple(() => { + Assert.Multiple(() => + { Assert.That(result, Is.InstanceOf()); CustomHttpStatusCodeResult r = (CustomHttpStatusCodeResult)result; - Assert.That(r.Sleep, Is.EqualTo(Sleep)); + Assert.That(r.Options.Sleep, Is.EqualTo(Sleep)); }); } @@ -40,13 +42,13 @@ public void SleepReadFromHeader() Mock request = HttpRequestHelper.GenerateMockRequest(); request.Object.Headers.Append(StatusExtensions.SLEEP_HEADER, Sleep.ToString()); - IResult result = StatusExtensions.HandleStatusRequestAsync(200, null, null, null, request.Object, _statusCodes); + IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200), null, request.Object, _statusCodes); Assert.Multiple(() => { Assert.That(result, Is.InstanceOf()); CustomHttpStatusCodeResult r = (CustomHttpStatusCodeResult)result; - Assert.That(r.Sleep, Is.EqualTo(Sleep)); + Assert.That(r.Options.Sleep, Is.EqualTo(Sleep)); }); } @@ -55,13 +57,13 @@ public void SleepReadFromQSTakesPriorityHeader() { Mock request = HttpRequestHelper.GenerateMockRequest(); request.Object.Headers.Append(StatusExtensions.SLEEP_HEADER, Sleep.ToString()); - IResult result = StatusExtensions.HandleStatusRequestAsync(200, Sleep * 2, null, null, request.Object, _statusCodes); + IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200, sleep:Sleep * 2), null, request.Object, _statusCodes); Assert.Multiple(() => { Assert.That(result, Is.InstanceOf()); CustomHttpStatusCodeResult r = (CustomHttpStatusCodeResult)result; - Assert.That(r.Sleep, Is.EqualTo(Sleep * 2)); + Assert.That(r.Options.Sleep, Is.EqualTo(Sleep * 2)); }); } @@ -71,14 +73,14 @@ public void BadSleepHeaderIgnored() Mock request = HttpRequestHelper.GenerateMockRequest(); request.Object.Headers.Append(StatusExtensions.SLEEP_HEADER, "invalid"); - IResult result = StatusExtensions.HandleStatusRequestAsync(200, null, null, null, request.Object, _statusCodes); + IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200), null, request.Object, _statusCodes); Assert.Multiple(() => { Assert.That(result, Is.InstanceOf()); CustomHttpStatusCodeResult r = (CustomHttpStatusCodeResult)result; - Assert.That(r.Sleep, Is.Null); + Assert.That(r.Options.Sleep, Is.Null); }); } diff --git a/src/Teapot.Web.Tests/UnitTests/SuppressBodyTests.cs b/src/Teapot.Web.Tests/UnitTests/SuppressBodyTests.cs index a0e8fc5..065e53d 100644 --- a/src/Teapot.Web.Tests/UnitTests/SuppressBodyTests.cs +++ b/src/Teapot.Web.Tests/UnitTests/SuppressBodyTests.cs @@ -28,14 +28,14 @@ public void Setup() public void SuppressBodyReadFromQuery(bool? suppressBody) { Mock request = HttpRequestHelper.GenerateMockRequest(); - IResult result = StatusExtensions.HandleStatusRequestAsync(200, null, suppressBody, null, request.Object, _statusCodes); + IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200, suppressBody:suppressBody), null, request.Object, _statusCodes); Assert.Multiple(() => { Assert.That(result, Is.InstanceOf()); CustomHttpStatusCodeResult r = (CustomHttpStatusCodeResult)result; - Assert.That(r.SuppressBody, Is.EqualTo(suppressBody)); + Assert.That(r.Options.SuppressBody, Is.EqualTo(suppressBody)); }); } @@ -48,7 +48,7 @@ public void SuppressBodyReadFromHeader(string? suppressBody) Mock request = HttpRequestHelper.GenerateMockRequest(); request.Object.Headers.Append(StatusExtensions.SUPPRESS_BODY_HEADER, suppressBody); - IResult result = StatusExtensions.HandleStatusRequestAsync(200, null, null, null, request.Object, _statusCodes); + IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200), null, request.Object, _statusCodes); Assert.Multiple(() => { @@ -60,7 +60,7 @@ public void SuppressBodyReadFromHeader(string? suppressBody) string { Length: > 0 } stringValue => bool.Parse(stringValue), _ => null }; - Assert.That(r.SuppressBody, Is.EqualTo(expectedValue)); + Assert.That(r.Options.SuppressBody, Is.EqualTo(expectedValue)); }); } @@ -76,7 +76,7 @@ public void SuppressBodyReadFromQSTakesPriorityHeader(string? headerValue, bool? Mock request = HttpRequestHelper.GenerateMockRequest(); request.Object.Headers.Append(StatusExtensions.SUPPRESS_BODY_HEADER, headerValue); - IResult result = StatusExtensions.HandleStatusRequestAsync(200, null, queryStringValue, null, request.Object, _statusCodes); + IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200, suppressBody: queryStringValue), null, request.Object, _statusCodes); Assert.Multiple(() => { @@ -88,7 +88,7 @@ public void SuppressBodyReadFromQSTakesPriorityHeader(string? headerValue, bool? string { Length: > 0 } stringValue => bool.Parse(stringValue), _ => null }; - Assert.That(r.SuppressBody, Is.EqualTo(expectedValue)); + Assert.That(r.Options.SuppressBody, Is.EqualTo(expectedValue)); }); } @@ -98,14 +98,14 @@ public void BadSuppressBodyHeaderIgnored() Mock request = HttpRequestHelper.GenerateMockRequest(); request.Object.Headers.Append(StatusExtensions.SUPPRESS_BODY_HEADER, "invalid"); - IResult result = StatusExtensions.HandleStatusRequestAsync(200, null, null, null, request.Object, _statusCodes); + IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200),null, request.Object, _statusCodes); Assert.Multiple(() => { Assert.That(result, Is.InstanceOf()); CustomHttpStatusCodeResult r = (CustomHttpStatusCodeResult)result; - Assert.That(r.SuppressBody, Is.Null); + Assert.That(r.Options.SuppressBody, Is.Null); }); } } diff --git a/src/Teapot.Web/CustomHttpStatusCodeResult.cs b/src/Teapot.Web/CustomHttpStatusCodeResult.cs index fdbbf97..28749f8 100644 --- a/src/Teapot.Web/CustomHttpStatusCodeResult.cs +++ b/src/Teapot.Web/CustomHttpStatusCodeResult.cs @@ -7,16 +7,10 @@ using System.Linq; using System.Text.Json; using System.Threading.Tasks; -using Teapot.Web.Models; namespace Teapot.Web; -public class CustomHttpStatusCodeResult( - int statusCode, - TeapotStatusCodeMetadata metadata, - int? sleep, - bool? suppressBody, - Dictionary customResponseHeaders) : IResult +public class CustomHttpStatusCodeResult(ResponseOptions options) : IResult { private const int SLEEP_MIN = 0; private const int SLEEP_MAX = 5 * 60 * 1000; // 5 mins in milliseconds @@ -24,34 +18,38 @@ public class CustomHttpStatusCodeResult( private static readonly MediaTypeHeaderValue jsonMimeType = new("application/json"); - public int? Sleep => sleep; - - public bool? SuppressBody => suppressBody; + public ResponseOptions Options => options; public async Task ExecuteAsync(HttpContext context) { - await DoSleep(Sleep); + await DoSleep(Options.Sleep); + + if (Options.AbortBeforeHeaders == true) + { + context.Abort(); + return; + } - context.Response.StatusCode = statusCode; + context.Response.StatusCode = Options.StatusCode; - if (!string.IsNullOrEmpty(metadata.Description)) + if (!string.IsNullOrEmpty(Options.Metadata.Description)) { IHttpResponseFeature? httpResponseFeature = context.Features.Get(); if (httpResponseFeature is not null) { - httpResponseFeature.ReasonPhrase = metadata.Description; + httpResponseFeature.ReasonPhrase = Options.Metadata.Description; } } - if (metadata.IncludeHeaders is not null) + if (Options.Metadata.IncludeHeaders is not null) { - foreach ((string header, string values) in metadata.IncludeHeaders) + foreach ((string header, string values) in Options.Metadata.IncludeHeaders) { context.Response.Headers.Append(header, values); } } - foreach ((string header, StringValues values) in customResponseHeaders) + foreach ((string header, StringValues values) in Options.CustomHeaders) { if (onlySingleHeader.Contains(header)) { @@ -63,7 +61,7 @@ public async Task ExecuteAsync(HttpContext context) } } - if (metadata.ExcludeBody || suppressBody == true) + if (Options.Metadata.ExcludeBody || Options.SuppressBody == true) { //remove Content-Length and Content-Type when there isn't any body context.Response.Headers.Remove("Content-Length"); @@ -75,14 +73,35 @@ public async Task ExecuteAsync(HttpContext context) (string body, string contentType) = acceptTypes.Contains(jsonMimeType) switch { - true => (JsonSerializer.Serialize(new { code = statusCode, description = metadata.Body ?? metadata.Description }), "application/json"), - false => (metadata.Body ?? $"{statusCode} {metadata.Description}", "text/plain") + true => (JsonSerializer.Serialize(new { code = Options.StatusCode, description = Options.Metadata.Body ?? Options.Metadata.Description }), "application/json"), + false => (Options.Metadata.Body ?? $"{Options.StatusCode} {Options.Metadata.Description}", "text/plain") }; context.Response.ContentType = contentType; context.Response.ContentLength = body.Length; - await context.Response.WriteAsync(body); + await context.Response.StartAsync(); + + await DoSleep(Options.SleepAfterHeaders); + + if (Options.AbortAfterHeaders == true) + { + context.Response.Body.Flush(); + await DoSleep(100); + context.Abort(); + return; + } + + if (Options.AbortDuringBody == true) + { + await context.Response.WriteAsync(body.Substring(0, 1)); + context.Response.Body.Flush(); + await DoSleep(100); + context.Abort(); + return; + } + + await context.Response.WriteAsync(body); } } diff --git a/src/Teapot.Web/ResponseOptions.cs b/src/Teapot.Web/ResponseOptions.cs new file mode 100644 index 0000000..7eaa2ef --- /dev/null +++ b/src/Teapot.Web/ResponseOptions.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.Primitives; +using System.Collections.Generic; +using Teapot.Web.Models; + +namespace Teapot.Web; + +public record class ResponseOptions +{ + public ResponseOptions(int statusCode, + int? sleep = null, + int? sleepAfterHeaders = null, + bool? abortBeforeHeaders = null, + bool? abortAfterHeaders = null, + bool? abortDuringBody = null, + bool? suppressBody = null, + TeapotStatusCodeMetadata? metadata=null, + Dictionary? customHeaders = null) + { + StatusCode = statusCode; + Sleep = sleep; + SleepAfterHeaders = sleepAfterHeaders; + AbortBeforeHeaders = abortBeforeHeaders; + AbortAfterHeaders = abortAfterHeaders; + AbortDuringBody = abortDuringBody; + SuppressBody = suppressBody; + CustomHeaders = customHeaders ?? new Dictionary(); + Metadata = metadata ?? new(); + } + + public int StatusCode { get; set; } + public int? Sleep { get; set; } + public int? SleepAfterHeaders { get; set; } + public bool? SuppressBody { get; set; } + public bool? AbortBeforeHeaders { get; set; } + public bool? AbortAfterHeaders { get; set; } + public bool? AbortDuringBody { get; set; } + public Dictionary CustomHeaders { get; set; } + public TeapotStatusCodeMetadata Metadata { get; set; } +} diff --git a/src/Teapot.Web/StatusExtensions.cs b/src/Teapot.Web/StatusExtensions.cs index 8a9aafd..400bd4c 100644 --- a/src/Teapot.Web/StatusExtensions.cs +++ b/src/Teapot.Web/StatusExtensions.cs @@ -14,6 +14,10 @@ internal static class StatusExtensions { public const string SLEEP_HEADER = "X-HttpStatus-Sleep"; public const string SUPPRESS_BODY_HEADER = "X-HttpStatus-SuppressBody"; + public const string SLEEP_AFTER_HEADERS = "X-HttpStatus-SleepAfterHeaders"; + public const string ABORT_BEFORE_HEADERS = "X-HttpStatus-AbortBeforeHeaders"; + public const string ABORT_AFTER_HEADERS = "X-HttpStatus-AbortAfterHeaders"; + public const string ABORT_DURING_BODY = "X-HttpStatus-AbortDuringBody"; public const string CUSTOM_RESPONSE_HEADER_PREFIX = "X-HttpStatus-Response-"; private static readonly string[] httpMethods = ["Get", "Put", "Post", "Delete", "Head", "Options", "Trace", "Patch"]; @@ -43,19 +47,38 @@ internal static IResult HandleStatusRequestAsync( HttpRequest req, [FromServices] TeapotStatusCodeMetadataCollection statusCodes) { - TeapotStatusCodeMetadata statusData = statusCodes.TryGetValue(status, out TeapotStatusCodeMetadata? value) ? + ResponseOptions options = new(status) + { + Sleep = sleep, + SuppressBody = suppressBody + }; + return CommonHandleStatusRequestAsync(options, wildcard, req, statusCodes); + } + + internal static IResult CommonHandleStatusRequestAsync( + ResponseOptions options, + string? wildcard, + HttpRequest req, + [FromServices] TeapotStatusCodeMetadataCollection statusCodes) + { + TeapotStatusCodeMetadata statusData = statusCodes.TryGetValue(options.StatusCode, out TeapotStatusCodeMetadata? value) ? value : - new TeapotStatusCodeMetadata { Description = $"{status} Unknown Code" }; - sleep ??= FindSleepInHeader(req); - suppressBody ??= FindSuppressBodyInHeader(req); + new TeapotStatusCodeMetadata { Description = $"{options.StatusCode} Unknown Code" }; + options.Sleep ??= ParseHeaderInt(req, SLEEP_HEADER); + options.SleepAfterHeaders ??= ParseHeaderInt(req, SLEEP_AFTER_HEADERS); + options.SuppressBody ??= ParseHeaderBool(req, SUPPRESS_BODY_HEADER); + options.AbortAfterHeaders ??= ParseHeaderBool(req, ABORT_AFTER_HEADERS); + options.AbortBeforeHeaders ??= ParseHeaderBool(req, ABORT_BEFORE_HEADERS); + options.AbortDuringBody??= ParseHeaderBool(req, ABORT_DURING_BODY); + - Dictionary customResponseHeaders = req.Headers + Dictionary< string, StringValues> customResponseHeaders = req.Headers .Where(header => header.Key.StartsWith(CUSTOM_RESPONSE_HEADER_PREFIX, StringComparison.InvariantCultureIgnoreCase)) .ToDictionary( header => header.Key.Replace(CUSTOM_RESPONSE_HEADER_PREFIX, string.Empty, StringComparison.InvariantCultureIgnoreCase), header => header.Value); - return new CustomHttpStatusCodeResult(status, statusData, sleep, suppressBody, customResponseHeaders); + return new CustomHttpStatusCodeResult(options); } internal static IResult HandleRandomRequest( @@ -68,8 +91,8 @@ internal static IResult HandleRandomRequest( { try { - int statusCode = GetRandomStatus(range); - return HandleStatusRequestAsync(statusCode, sleep, suppressBody, wildcard, req, statusCodes); + var options = new ResponseOptions(GetRandomStatus(range)); + return CommonHandleStatusRequestAsync(options, wildcard, req, statusCodes); } catch { @@ -77,28 +100,28 @@ internal static IResult HandleRandomRequest( } } - private static int? FindSleepInHeader(HttpRequest req) + private static int? ParseHeaderInt(HttpRequest req, string headerName) { - if (req.Headers.TryGetValue(SLEEP_HEADER, out StringValues sleepHeader) && sleepHeader.Count == 1 && sleepHeader[0] is not null) + if (req.Headers.TryGetValue(headerName, out StringValues values) && values.Count == 1 && values[0] is not null) { - string? val = sleepHeader[0]; - if (int.TryParse(val, out int sleepFromHeader)) + string? val = values[0]; + if (int.TryParse(val, out int value)) { - return sleepFromHeader; + return value; } } return null; } - private static bool? FindSuppressBodyInHeader(HttpRequest req) + private static bool? ParseHeaderBool(HttpRequest req, string headerName) { - if (req.Headers.TryGetValue(SUPPRESS_BODY_HEADER, out StringValues suppressBodyHeader) && suppressBodyHeader.Count == 1 && suppressBodyHeader[0] is not null) + if (req.Headers.TryGetValue(headerName, out StringValues values) && values.Count == 1 && values[0] is not null) { - string? val = suppressBodyHeader[0]; - if (bool.TryParse(val, out bool suppressBodyFromHeader)) + string? val = values[0]; + if (bool.TryParse(val, out bool value)) { - return suppressBodyFromHeader; + return value; } } From 1efa6890915594f3fe66c8ae2c538821cbd51773 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 28 Feb 2025 11:03:49 +0100 Subject: [PATCH 2/5] async flush --- src/Teapot.Web/CustomHttpStatusCodeResult.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Teapot.Web/CustomHttpStatusCodeResult.cs b/src/Teapot.Web/CustomHttpStatusCodeResult.cs index 28749f8..115ff7e 100644 --- a/src/Teapot.Web/CustomHttpStatusCodeResult.cs +++ b/src/Teapot.Web/CustomHttpStatusCodeResult.cs @@ -86,7 +86,7 @@ public async Task ExecuteAsync(HttpContext context) if (Options.AbortAfterHeaders == true) { - context.Response.Body.Flush(); + await context.Response.Body.FlushAsync(); await DoSleep(100); context.Abort(); return; @@ -95,7 +95,7 @@ public async Task ExecuteAsync(HttpContext context) if (Options.AbortDuringBody == true) { await context.Response.WriteAsync(body.Substring(0, 1)); - context.Response.Body.Flush(); + await context.Response.Body.FlushAsync(); await DoSleep(100); context.Abort(); return; From ccc33a65048a5ae0b8e84ddd49baaa9b499132e2 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 28 Feb 2025 16:28:15 +0100 Subject: [PATCH 3/5] - detect production and limit sleep - implement body dribbling --- src/Teapot.Web/CustomHttpStatusCodeResult.cs | 27 ++++++++++++++++---- src/Teapot.Web/ResponseOptions.cs | 16 ++++++++---- src/Teapot.Web/StatusExtensions.cs | 25 +++++++++++++----- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/Teapot.Web/CustomHttpStatusCodeResult.cs b/src/Teapot.Web/CustomHttpStatusCodeResult.cs index 115ff7e..9384365 100644 --- a/src/Teapot.Web/CustomHttpStatusCodeResult.cs +++ b/src/Teapot.Web/CustomHttpStatusCodeResult.cs @@ -14,6 +14,7 @@ public class CustomHttpStatusCodeResult(ResponseOptions options) : IResult { private const int SLEEP_MIN = 0; private const int SLEEP_MAX = 5 * 60 * 1000; // 5 mins in milliseconds + private const int SLEEP_MAX_PROD = 500; // 500 in milliseconds internal static readonly string[] onlySingleHeader = ["Location", "Retry-After"]; private static readonly MediaTypeHeaderValue jsonMimeType = new("application/json"); @@ -30,6 +31,7 @@ public async Task ExecuteAsync(HttpContext context) return; } + context.Response.StatusCode = Options.StatusCode; if (!string.IsNullOrEmpty(Options.Metadata.Description)) @@ -87,7 +89,7 @@ public async Task ExecuteAsync(HttpContext context) if (Options.AbortAfterHeaders == true) { await context.Response.Body.FlushAsync(); - await DoSleep(100); + await DoSleep(10); context.Abort(); return; } @@ -96,18 +98,33 @@ public async Task ExecuteAsync(HttpContext context) { await context.Response.WriteAsync(body.Substring(0, 1)); await context.Response.Body.FlushAsync(); - await DoSleep(100); + await DoSleep(10); context.Abort(); return; } - await context.Response.WriteAsync(body); + if (Options.DribbleBody == true) + { + for (int i = 0; i < body.Length; i++) + { + await context.Response.WriteAsync(body[i..(i + 1)]); + if (i < 20) + { + await context.Response.Body.FlushAsync(); + await DoSleep(10); + } + } + } + else + { + await context.Response.WriteAsync(body); + } } } - private static async Task DoSleep(int? sleep) + private async Task DoSleep(int? sleep) { - int sleepData = Math.Clamp(sleep ?? 0, SLEEP_MIN, SLEEP_MAX); + int sleepData = Math.Clamp(sleep ?? 0, SLEEP_MIN, options.IsProduction == true ? SLEEP_MAX_PROD : SLEEP_MAX); if (sleepData > 0) { await Task.Delay(sleepData); diff --git a/src/Teapot.Web/ResponseOptions.cs b/src/Teapot.Web/ResponseOptions.cs index 7eaa2ef..1df4872 100644 --- a/src/Teapot.Web/ResponseOptions.cs +++ b/src/Teapot.Web/ResponseOptions.cs @@ -6,14 +6,16 @@ namespace Teapot.Web; public record class ResponseOptions { - public ResponseOptions(int statusCode, - int? sleep = null, - int? sleepAfterHeaders = null, + public ResponseOptions(int statusCode, + int? sleep = null, + int? sleepAfterHeaders = null, bool? abortBeforeHeaders = null, bool? abortAfterHeaders = null, bool? abortDuringBody = null, bool? suppressBody = null, - TeapotStatusCodeMetadata? metadata=null, + bool? dribbleBody = null, + bool? isProduction = null, + TeapotStatusCodeMetadata? metadata = null, Dictionary? customHeaders = null) { StatusCode = statusCode; @@ -23,6 +25,8 @@ public ResponseOptions(int statusCode, AbortAfterHeaders = abortAfterHeaders; AbortDuringBody = abortDuringBody; SuppressBody = suppressBody; + DribbleBody = dribbleBody; + IsProduction = isProduction; CustomHeaders = customHeaders ?? new Dictionary(); Metadata = metadata ?? new(); } @@ -34,6 +38,8 @@ public ResponseOptions(int statusCode, public bool? AbortBeforeHeaders { get; set; } public bool? AbortAfterHeaders { get; set; } public bool? AbortDuringBody { get; set; } - public Dictionary CustomHeaders { get; set; } + public bool? DribbleBody { get; set; } + public bool? IsProduction { get; set; } + public Dictionary CustomHeaders { get; set; } public TeapotStatusCodeMetadata Metadata { get; set; } } diff --git a/src/Teapot.Web/StatusExtensions.cs b/src/Teapot.Web/StatusExtensions.cs index 400bd4c..6e3bb8c 100644 --- a/src/Teapot.Web/StatusExtensions.cs +++ b/src/Teapot.Web/StatusExtensions.cs @@ -1,7 +1,10 @@ using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using System; using System.Collections.Generic; @@ -14,6 +17,7 @@ internal static class StatusExtensions { public const string SLEEP_HEADER = "X-HttpStatus-Sleep"; public const string SUPPRESS_BODY_HEADER = "X-HttpStatus-SuppressBody"; + public const string DRIBBLE_BODY_HEADER = "X-HttpStatus-DribbleBody"; public const string SLEEP_AFTER_HEADERS = "X-HttpStatus-SleepAfterHeaders"; public const string ABORT_BEFORE_HEADERS = "X-HttpStatus-AbortBeforeHeaders"; public const string ABORT_AFTER_HEADERS = "X-HttpStatus-AbortAfterHeaders"; @@ -32,7 +36,7 @@ internal static WebApplication MapStatusEndpoints(this WebApplication app, strin app.MapMethods("/random/{range}", httpMethods, HandleRandomRequest); //.RequireRateLimiting(policyName); app.MapMethods("/random/{range}/{*wildcard}", httpMethods, HandleRandomRequest); - //.RequireRateLimiting(policyName); + //.RequireRateLimiting(policyName); app.MapGet("im-a-teapot", () => TypedResults.Redirect("https://www.ietf.org/rfc/rfc2324.txt")); @@ -45,21 +49,25 @@ internal static IResult HandleStatusRequestAsync( bool? suppressBody, string? wildcard, HttpRequest req, - [FromServices] TeapotStatusCodeMetadataCollection statusCodes) + [FromServices] TeapotStatusCodeMetadataCollection statusCodes, + [FromServices] IWebHostEnvironment env + ) { ResponseOptions options = new(status) { Sleep = sleep, SuppressBody = suppressBody }; - return CommonHandleStatusRequestAsync(options, wildcard, req, statusCodes); + return CommonHandleStatusRequestAsync(options, wildcard, req, statusCodes, env); } internal static IResult CommonHandleStatusRequestAsync( ResponseOptions options, string? wildcard, HttpRequest req, - [FromServices] TeapotStatusCodeMetadataCollection statusCodes) + TeapotStatusCodeMetadataCollection statusCodes, + IWebHostEnvironment env + ) { TeapotStatusCodeMetadata statusData = statusCodes.TryGetValue(options.StatusCode, out TeapotStatusCodeMetadata? value) ? value : @@ -67,12 +75,14 @@ internal static IResult CommonHandleStatusRequestAsync( options.Sleep ??= ParseHeaderInt(req, SLEEP_HEADER); options.SleepAfterHeaders ??= ParseHeaderInt(req, SLEEP_AFTER_HEADERS); options.SuppressBody ??= ParseHeaderBool(req, SUPPRESS_BODY_HEADER); + options.DribbleBody ??= ParseHeaderBool(req, DRIBBLE_BODY_HEADER); options.AbortAfterHeaders ??= ParseHeaderBool(req, ABORT_AFTER_HEADERS); options.AbortBeforeHeaders ??= ParseHeaderBool(req, ABORT_BEFORE_HEADERS); - options.AbortDuringBody??= ParseHeaderBool(req, ABORT_DURING_BODY); + options.AbortDuringBody ??= ParseHeaderBool(req, ABORT_DURING_BODY); + options.IsProduction = !env.IsDevelopment(); - Dictionary< string, StringValues> customResponseHeaders = req.Headers + Dictionary customResponseHeaders = req.Headers .Where(header => header.Key.StartsWith(CUSTOM_RESPONSE_HEADER_PREFIX, StringComparison.InvariantCultureIgnoreCase)) .ToDictionary( header => header.Key.Replace(CUSTOM_RESPONSE_HEADER_PREFIX, string.Empty, StringComparison.InvariantCultureIgnoreCase), @@ -84,6 +94,7 @@ internal static IResult CommonHandleStatusRequestAsync( internal static IResult HandleRandomRequest( HttpRequest req, [FromServices] TeapotStatusCodeMetadataCollection statusCodes, + [FromServices] IWebHostEnvironment env, int? sleep, bool? suppressBody, string? wildcard, @@ -92,7 +103,7 @@ internal static IResult HandleRandomRequest( try { var options = new ResponseOptions(GetRandomStatus(range)); - return CommonHandleStatusRequestAsync(options, wildcard, req, statusCodes); + return CommonHandleStatusRequestAsync(options, wildcard, req, statusCodes, env); } catch { From b998600d6c0f1631fd0ff1d4d84762425ea82cbb Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 6 Mar 2025 15:08:43 +0100 Subject: [PATCH 4/5] revert SLEEP_MAX_PROD --- src/Teapot.Web/CustomHttpStatusCodeResult.cs | 5 ++--- src/Teapot.Web/ResponseOptions.cs | 3 --- src/Teapot.Web/StatusExtensions.cs | 12 ++++-------- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/Teapot.Web/CustomHttpStatusCodeResult.cs b/src/Teapot.Web/CustomHttpStatusCodeResult.cs index 9384365..c73f30b 100644 --- a/src/Teapot.Web/CustomHttpStatusCodeResult.cs +++ b/src/Teapot.Web/CustomHttpStatusCodeResult.cs @@ -14,7 +14,6 @@ public class CustomHttpStatusCodeResult(ResponseOptions options) : IResult { private const int SLEEP_MIN = 0; private const int SLEEP_MAX = 5 * 60 * 1000; // 5 mins in milliseconds - private const int SLEEP_MAX_PROD = 500; // 500 in milliseconds internal static readonly string[] onlySingleHeader = ["Location", "Retry-After"]; private static readonly MediaTypeHeaderValue jsonMimeType = new("application/json"); @@ -122,9 +121,9 @@ public async Task ExecuteAsync(HttpContext context) } } - private async Task DoSleep(int? sleep) + private static async Task DoSleep(int? sleep) { - int sleepData = Math.Clamp(sleep ?? 0, SLEEP_MIN, options.IsProduction == true ? SLEEP_MAX_PROD : SLEEP_MAX); + int sleepData = Math.Clamp(sleep ?? 0, SLEEP_MIN, SLEEP_MAX); if (sleepData > 0) { await Task.Delay(sleepData); diff --git a/src/Teapot.Web/ResponseOptions.cs b/src/Teapot.Web/ResponseOptions.cs index 1df4872..5ca3102 100644 --- a/src/Teapot.Web/ResponseOptions.cs +++ b/src/Teapot.Web/ResponseOptions.cs @@ -14,7 +14,6 @@ public ResponseOptions(int statusCode, bool? abortDuringBody = null, bool? suppressBody = null, bool? dribbleBody = null, - bool? isProduction = null, TeapotStatusCodeMetadata? metadata = null, Dictionary? customHeaders = null) { @@ -26,7 +25,6 @@ public ResponseOptions(int statusCode, AbortDuringBody = abortDuringBody; SuppressBody = suppressBody; DribbleBody = dribbleBody; - IsProduction = isProduction; CustomHeaders = customHeaders ?? new Dictionary(); Metadata = metadata ?? new(); } @@ -39,7 +37,6 @@ public ResponseOptions(int statusCode, public bool? AbortAfterHeaders { get; set; } public bool? AbortDuringBody { get; set; } public bool? DribbleBody { get; set; } - public bool? IsProduction { get; set; } public Dictionary CustomHeaders { get; set; } public TeapotStatusCodeMetadata Metadata { get; set; } } diff --git a/src/Teapot.Web/StatusExtensions.cs b/src/Teapot.Web/StatusExtensions.cs index 6e3bb8c..2169e98 100644 --- a/src/Teapot.Web/StatusExtensions.cs +++ b/src/Teapot.Web/StatusExtensions.cs @@ -49,8 +49,7 @@ internal static IResult HandleStatusRequestAsync( bool? suppressBody, string? wildcard, HttpRequest req, - [FromServices] TeapotStatusCodeMetadataCollection statusCodes, - [FromServices] IWebHostEnvironment env + [FromServices] TeapotStatusCodeMetadataCollection statusCodes ) { ResponseOptions options = new(status) @@ -58,15 +57,14 @@ [FromServices] IWebHostEnvironment env Sleep = sleep, SuppressBody = suppressBody }; - return CommonHandleStatusRequestAsync(options, wildcard, req, statusCodes, env); + return CommonHandleStatusRequestAsync(options, wildcard, req, statusCodes); } internal static IResult CommonHandleStatusRequestAsync( ResponseOptions options, string? wildcard, HttpRequest req, - TeapotStatusCodeMetadataCollection statusCodes, - IWebHostEnvironment env + TeapotStatusCodeMetadataCollection statusCodes ) { TeapotStatusCodeMetadata statusData = statusCodes.TryGetValue(options.StatusCode, out TeapotStatusCodeMetadata? value) ? @@ -79,7 +77,6 @@ IWebHostEnvironment env options.AbortAfterHeaders ??= ParseHeaderBool(req, ABORT_AFTER_HEADERS); options.AbortBeforeHeaders ??= ParseHeaderBool(req, ABORT_BEFORE_HEADERS); options.AbortDuringBody ??= ParseHeaderBool(req, ABORT_DURING_BODY); - options.IsProduction = !env.IsDevelopment(); Dictionary customResponseHeaders = req.Headers @@ -94,7 +91,6 @@ IWebHostEnvironment env internal static IResult HandleRandomRequest( HttpRequest req, [FromServices] TeapotStatusCodeMetadataCollection statusCodes, - [FromServices] IWebHostEnvironment env, int? sleep, bool? suppressBody, string? wildcard, @@ -103,7 +99,7 @@ internal static IResult HandleRandomRequest( try { var options = new ResponseOptions(GetRandomStatus(range)); - return CommonHandleStatusRequestAsync(options, wildcard, req, statusCodes, env); + return CommonHandleStatusRequestAsync(options, wildcard, req, statusCodes); } catch { From b80a76907e9279f9eb2bc5b7a4f150bd339f4e10 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 7 Mar 2025 14:23:45 +0100 Subject: [PATCH 5/5] fix --- src/Teapot.Web/CustomHttpStatusCodeResult.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Teapot.Web/CustomHttpStatusCodeResult.cs b/src/Teapot.Web/CustomHttpStatusCodeResult.cs index c73f30b..988d901 100644 --- a/src/Teapot.Web/CustomHttpStatusCodeResult.cs +++ b/src/Teapot.Web/CustomHttpStatusCodeResult.cs @@ -87,7 +87,6 @@ public async Task ExecuteAsync(HttpContext context) if (Options.AbortAfterHeaders == true) { - await context.Response.Body.FlushAsync(); await DoSleep(10); context.Abort(); return;