From ca5c3d9413b483f2b4dd42ef1ffd6131da25e8dd Mon Sep 17 00:00:00 2001 From: Muhammad Rehan Saeed Date: Wed, 24 Aug 2022 10:34:07 +0100 Subject: [PATCH] Add request cancelled middleware --- .../ApplicationBuilderExtensions.cs | 29 +++++++++++ Source/Boxed.AspNetCore/LoggerExtensions.cs | 6 +++ .../Middleware/HttpExceptionMiddleware.cs | 9 +--- .../Middleware/RequestCanceledMiddleware.cs | 48 +++++++++++++++++++ .../RequestCanceledMiddlewareOptions.cs | 19 ++++++++ 5 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 Source/Boxed.AspNetCore/Middleware/RequestCanceledMiddleware.cs create mode 100644 Source/Boxed.AspNetCore/Middleware/RequestCanceledMiddlewareOptions.cs diff --git a/Source/Boxed.AspNetCore/ApplicationBuilderExtensions.cs b/Source/Boxed.AspNetCore/ApplicationBuilderExtensions.cs index b3106c32..3cfa6017 100644 --- a/Source/Boxed.AspNetCore/ApplicationBuilderExtensions.cs +++ b/Source/Boxed.AspNetCore/ApplicationBuilderExtensions.cs @@ -34,6 +34,35 @@ public static IApplicationBuilder UseHttpException( return application.UseMiddleware(options); } + /// + /// Handles caused by the HTTP request being aborted, then shortcuts and + /// returns an error status code. + /// See https://andrewlock.net/using-cancellationtokens-in-asp-net-core-minimal-apis/. + /// + /// The application builder. + /// The same application builder. + public static IApplicationBuilder UseRequestCancelled(this IApplicationBuilder application) => + UseRequestCancelled(application, null); + + /// + /// Handles caused by the HTTP request being aborted, then shortcuts and + /// returns an error status code. + /// See https://andrewlock.net/using-cancellationtokens-in-asp-net-core-minimal-apis/. + /// + /// The application builder. + /// The middleware options. + /// The same application builder. + public static IApplicationBuilder UseRequestCancelled( + this IApplicationBuilder application, + Action? configureOptions) + { + ArgumentNullException.ThrowIfNull(application); + + var options = new RequestCanceledMiddlewareOptions(); + configureOptions?.Invoke(options); + return application.UseMiddleware(options); + } + /// /// Measures the time the request takes to process and returns this in a Server-Timing trailing HTTP header. /// It is used to surface any back-end server timing metrics (e.g. database read/write, CPU time, file system diff --git a/Source/Boxed.AspNetCore/LoggerExtensions.cs b/Source/Boxed.AspNetCore/LoggerExtensions.cs index b3a0d494..cf5ac6ed 100644 --- a/Source/Boxed.AspNetCore/LoggerExtensions.cs +++ b/Source/Boxed.AspNetCore/LoggerExtensions.cs @@ -13,4 +13,10 @@ internal static partial class LoggerExtensions Level = LogLevel.Information, Message = "Executing HttpExceptionMiddleware, setting HTTP status code {StatusCode}.")] public static partial void SettingHttpStatusCode(this ILogger logger, Exception exception, int statusCode); + + [LoggerMessage( + EventId = 4001, + Level = LogLevel.Information, + Message = "Request was cancelled.")] + public static partial void RequestCancelled(this ILogger logger); } diff --git a/Source/Boxed.AspNetCore/Middleware/HttpExceptionMiddleware.cs b/Source/Boxed.AspNetCore/Middleware/HttpExceptionMiddleware.cs index a01927cb..a0befbf3 100644 --- a/Source/Boxed.AspNetCore/Middleware/HttpExceptionMiddleware.cs +++ b/Source/Boxed.AspNetCore/Middleware/HttpExceptionMiddleware.cs @@ -13,19 +13,14 @@ namespace Boxed.AspNetCore.Middleware; /// public class HttpExceptionMiddleware : IMiddleware { - private readonly RequestDelegate next; private readonly HttpExceptionMiddlewareOptions options; /// /// Initializes a new instance of the class. /// - /// The next. /// The options. - public HttpExceptionMiddleware(RequestDelegate next, HttpExceptionMiddlewareOptions options) - { - this.next = next; + public HttpExceptionMiddleware(HttpExceptionMiddlewareOptions options) => this.options = options; - } /// public async Task InvokeAsync(HttpContext context, RequestDelegate next) @@ -35,7 +30,7 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next) try { - await this.next.Invoke(context).ConfigureAwait(false); + await next.Invoke(context).ConfigureAwait(false); } catch (HttpException httpException) { diff --git a/Source/Boxed.AspNetCore/Middleware/RequestCanceledMiddleware.cs b/Source/Boxed.AspNetCore/Middleware/RequestCanceledMiddleware.cs new file mode 100644 index 00000000..e0e16c71 --- /dev/null +++ b/Source/Boxed.AspNetCore/Middleware/RequestCanceledMiddleware.cs @@ -0,0 +1,48 @@ +namespace Boxed.AspNetCore.Middleware; + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +/// +/// A middleware which handles caused by the HTTP request being aborted, then +/// shortcuts and returns an error status code. +/// +/// +public class RequestCanceledMiddleware : IMiddleware +{ + private readonly ILogger logger; + private readonly RequestCanceledMiddlewareOptions options; + + /// + /// Initializes a new instance of the class. + /// + /// The middleware options. + /// A logger. + public RequestCanceledMiddleware( + RequestCanceledMiddlewareOptions options, + ILogger logger) + { + this.options = options; + this.logger = logger; + } + + /// + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(next); + + try + { + await next(context).ConfigureAwait(false); + } + catch (OperationCanceledException operationCanceledException) + when (operationCanceledException.CancellationToken == context.RequestAborted) + { + this.logger.RequestCancelled(); + context.Response.StatusCode = this.options.StatusCode; + } + } +} diff --git a/Source/Boxed.AspNetCore/Middleware/RequestCanceledMiddlewareOptions.cs b/Source/Boxed.AspNetCore/Middleware/RequestCanceledMiddlewareOptions.cs new file mode 100644 index 00000000..97e30774 --- /dev/null +++ b/Source/Boxed.AspNetCore/Middleware/RequestCanceledMiddlewareOptions.cs @@ -0,0 +1,19 @@ +namespace Boxed.AspNetCore.Middleware; + +/// +/// Options controlling . +/// +public class RequestCanceledMiddlewareOptions +{ + /// + /// The non-standard 499 status code 'Client Closed Request' used by NGINX to signify an aborted/cancelled request. + /// + public const int ClientClosedRequest = 499; + + /// + /// Gets or sets the status code to return for a cancelled request. The default is the non-standard 499 + /// 'Client Closed Request' used by NGINX. + /// See https://stackoverflow.com/questions/46234679/what-is-the-correct-http-status-code-for-a-cancelled-request. + /// + public int StatusCode { get; set; } = ClientClosedRequest; +}