Skip to content

Commit ca5c3d9

Browse files
committed
Add request cancelled middleware
1 parent 3e7788f commit ca5c3d9

File tree

5 files changed

+104
-7
lines changed

5 files changed

+104
-7
lines changed

Source/Boxed.AspNetCore/ApplicationBuilderExtensions.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,35 @@ public static IApplicationBuilder UseHttpException(
3434
return application.UseMiddleware<HttpExceptionMiddleware>(options);
3535
}
3636

37+
/// <summary>
38+
/// Handles <see cref="OperationCanceledException"/> caused by the HTTP request being aborted, then shortcuts and
39+
/// returns an error status code.
40+
/// See https://andrewlock.net/using-cancellationtokens-in-asp-net-core-minimal-apis/.
41+
/// </summary>
42+
/// <param name="application">The application builder.</param>
43+
/// <returns>The same application builder.</returns>
44+
public static IApplicationBuilder UseRequestCancelled(this IApplicationBuilder application) =>
45+
UseRequestCancelled(application, null);
46+
47+
/// <summary>
48+
/// Handles <see cref="OperationCanceledException"/> caused by the HTTP request being aborted, then shortcuts and
49+
/// returns an error status code.
50+
/// See https://andrewlock.net/using-cancellationtokens-in-asp-net-core-minimal-apis/.
51+
/// </summary>
52+
/// <param name="application">The application builder.</param>
53+
/// <param name="configureOptions">The middleware options.</param>
54+
/// <returns>The same application builder.</returns>
55+
public static IApplicationBuilder UseRequestCancelled(
56+
this IApplicationBuilder application,
57+
Action<RequestCanceledMiddlewareOptions>? configureOptions)
58+
{
59+
ArgumentNullException.ThrowIfNull(application);
60+
61+
var options = new RequestCanceledMiddlewareOptions();
62+
configureOptions?.Invoke(options);
63+
return application.UseMiddleware<RequestCanceledMiddleware>(options);
64+
}
65+
3766
/// <summary>
3867
/// Measures the time the request takes to process and returns this in a Server-Timing trailing HTTP header.
3968
/// It is used to surface any back-end server timing metrics (e.g. database read/write, CPU time, file system

Source/Boxed.AspNetCore/LoggerExtensions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,10 @@ internal static partial class LoggerExtensions
1313
Level = LogLevel.Information,
1414
Message = "Executing HttpExceptionMiddleware, setting HTTP status code {StatusCode}.")]
1515
public static partial void SettingHttpStatusCode(this ILogger logger, Exception exception, int statusCode);
16+
17+
[LoggerMessage(
18+
EventId = 4001,
19+
Level = LogLevel.Information,
20+
Message = "Request was cancelled.")]
21+
public static partial void RequestCancelled(this ILogger logger);
1622
}

Source/Boxed.AspNetCore/Middleware/HttpExceptionMiddleware.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,14 @@ namespace Boxed.AspNetCore.Middleware;
1313
/// <seealso cref="IMiddleware" />
1414
public class HttpExceptionMiddleware : IMiddleware
1515
{
16-
private readonly RequestDelegate next;
1716
private readonly HttpExceptionMiddlewareOptions options;
1817

1918
/// <summary>
2019
/// Initializes a new instance of the <see cref="HttpExceptionMiddleware"/> class.
2120
/// </summary>
22-
/// <param name="next">The next.</param>
2321
/// <param name="options">The options.</param>
24-
public HttpExceptionMiddleware(RequestDelegate next, HttpExceptionMiddlewareOptions options)
25-
{
26-
this.next = next;
22+
public HttpExceptionMiddleware(HttpExceptionMiddlewareOptions options) =>
2723
this.options = options;
28-
}
2924

3025
/// <inheritdoc/>
3126
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
@@ -35,7 +30,7 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next)
3530

3631
try
3732
{
38-
await this.next.Invoke(context).ConfigureAwait(false);
33+
await next.Invoke(context).ConfigureAwait(false);
3934
}
4035
catch (HttpException httpException)
4136
{
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
namespace Boxed.AspNetCore.Middleware;
2+
3+
using System;
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.Extensions.Logging;
7+
8+
/// <summary>
9+
/// A middleware which handles <see cref="OperationCanceledException"/> caused by the HTTP request being aborted, then
10+
/// shortcuts and returns an error status code.
11+
/// </summary>
12+
/// <seealso cref="IMiddleware" />
13+
public class RequestCanceledMiddleware : IMiddleware
14+
{
15+
private readonly ILogger<RequestCanceledMiddleware> logger;
16+
private readonly RequestCanceledMiddlewareOptions options;
17+
18+
/// <summary>
19+
/// Initializes a new instance of the <see cref="RequestCanceledMiddleware"/> class.
20+
/// </summary>
21+
/// <param name="options">The middleware options.</param>
22+
/// <param name="logger">A logger.</param>
23+
public RequestCanceledMiddleware(
24+
RequestCanceledMiddlewareOptions options,
25+
ILogger<RequestCanceledMiddleware> logger)
26+
{
27+
this.options = options;
28+
this.logger = logger;
29+
}
30+
31+
/// <inheritdoc/>
32+
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
33+
{
34+
ArgumentNullException.ThrowIfNull(context);
35+
ArgumentNullException.ThrowIfNull(next);
36+
37+
try
38+
{
39+
await next(context).ConfigureAwait(false);
40+
}
41+
catch (OperationCanceledException operationCanceledException)
42+
when (operationCanceledException.CancellationToken == context.RequestAborted)
43+
{
44+
this.logger.RequestCancelled();
45+
context.Response.StatusCode = this.options.StatusCode;
46+
}
47+
}
48+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace Boxed.AspNetCore.Middleware;
2+
3+
/// <summary>
4+
/// Options controlling <see cref="RequestCanceledMiddleware"/>.
5+
/// </summary>
6+
public class RequestCanceledMiddlewareOptions
7+
{
8+
/// <summary>
9+
/// The non-standard 499 status code 'Client Closed Request' used by NGINX to signify an aborted/cancelled request.
10+
/// </summary>
11+
public const int ClientClosedRequest = 499;
12+
13+
/// <summary>
14+
/// Gets or sets the status code to return for a cancelled request. The default is the non-standard 499
15+
/// 'Client Closed Request' used by NGINX.
16+
/// See https://stackoverflow.com/questions/46234679/what-is-the-correct-http-status-code-for-a-cancelled-request.
17+
/// </summary>
18+
public int StatusCode { get; set; } = ClientClosedRequest;
19+
}

0 commit comments

Comments
 (0)