Skip to content

Commit

Permalink
Add request cancelled middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
RehanSaeed committed Aug 24, 2022
1 parent 3e7788f commit ca5c3d9
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 7 deletions.
29 changes: 29 additions & 0 deletions Source/Boxed.AspNetCore/ApplicationBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,35 @@ public static IApplicationBuilder UseHttpException(
return application.UseMiddleware<HttpExceptionMiddleware>(options);
}

/// <summary>
/// Handles <see cref="OperationCanceledException"/> 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/.
/// </summary>
/// <param name="application">The application builder.</param>
/// <returns>The same application builder.</returns>
public static IApplicationBuilder UseRequestCancelled(this IApplicationBuilder application) =>
UseRequestCancelled(application, null);

/// <summary>
/// Handles <see cref="OperationCanceledException"/> 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/.
/// </summary>
/// <param name="application">The application builder.</param>
/// <param name="configureOptions">The middleware options.</param>
/// <returns>The same application builder.</returns>
public static IApplicationBuilder UseRequestCancelled(
this IApplicationBuilder application,
Action<RequestCanceledMiddlewareOptions>? configureOptions)
{
ArgumentNullException.ThrowIfNull(application);

var options = new RequestCanceledMiddlewareOptions();
configureOptions?.Invoke(options);
return application.UseMiddleware<RequestCanceledMiddleware>(options);
}

/// <summary>
/// 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
Expand Down
6 changes: 6 additions & 0 deletions Source/Boxed.AspNetCore/LoggerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
9 changes: 2 additions & 7 deletions Source/Boxed.AspNetCore/Middleware/HttpExceptionMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,14 @@ namespace Boxed.AspNetCore.Middleware;
/// <seealso cref="IMiddleware" />
public class HttpExceptionMiddleware : IMiddleware
{
private readonly RequestDelegate next;
private readonly HttpExceptionMiddlewareOptions options;

/// <summary>
/// Initializes a new instance of the <see cref="HttpExceptionMiddleware"/> class.
/// </summary>
/// <param name="next">The next.</param>
/// <param name="options">The options.</param>
public HttpExceptionMiddleware(RequestDelegate next, HttpExceptionMiddlewareOptions options)
{
this.next = next;
public HttpExceptionMiddleware(HttpExceptionMiddlewareOptions options) =>
this.options = options;
}

/// <inheritdoc/>
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
Expand All @@ -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)
{
Expand Down
48 changes: 48 additions & 0 deletions Source/Boxed.AspNetCore/Middleware/RequestCanceledMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
namespace Boxed.AspNetCore.Middleware;

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;

/// <summary>
/// A middleware which handles <see cref="OperationCanceledException"/> caused by the HTTP request being aborted, then
/// shortcuts and returns an error status code.
/// </summary>
/// <seealso cref="IMiddleware" />
public class RequestCanceledMiddleware : IMiddleware
{
private readonly ILogger<RequestCanceledMiddleware> logger;
private readonly RequestCanceledMiddlewareOptions options;

/// <summary>
/// Initializes a new instance of the <see cref="RequestCanceledMiddleware"/> class.
/// </summary>
/// <param name="options">The middleware options.</param>
/// <param name="logger">A logger.</param>
public RequestCanceledMiddleware(
RequestCanceledMiddlewareOptions options,
ILogger<RequestCanceledMiddleware> logger)
{
this.options = options;
this.logger = logger;
}

/// <inheritdoc/>
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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Boxed.AspNetCore.Middleware;

/// <summary>
/// Options controlling <see cref="RequestCanceledMiddleware"/>.
/// </summary>
public class RequestCanceledMiddlewareOptions
{
/// <summary>
/// The non-standard 499 status code 'Client Closed Request' used by NGINX to signify an aborted/cancelled request.
/// </summary>
public const int ClientClosedRequest = 499;

/// <summary>
/// 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.
/// </summary>
public int StatusCode { get; set; } = ClientClosedRequest;
}

0 comments on commit ca5c3d9

Please sign in to comment.