Skip to content

Commit

Permalink
Added operation cancel operation (#46330)
Browse files Browse the repository at this point in the history
  • Loading branch information
singh733 authored Feb 16, 2023
1 parent 6a7bcda commit 9068fcf
Show file tree
Hide file tree
Showing 18 changed files with 96 additions and 15 deletions.
1 change: 1 addition & 0 deletions src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#nullable enable
const Microsoft.AspNetCore.Http.StatusCodes.Status499ClientClosedRequest = 499 -> int
*REMOVED*Microsoft.AspNetCore.Http.DefaultEndpointFilterInvocationContext.DefaultEndpointFilterInvocationContext(Microsoft.AspNetCore.Http.HttpContext! httpContext, params object![]! arguments) -> void
Microsoft.AspNetCore.Http.DefaultEndpointFilterInvocationContext.DefaultEndpointFilterInvocationContext(Microsoft.AspNetCore.Http.HttpContext! httpContext, params object?[]! arguments) -> void
Microsoft.AspNetCore.Http.HttpResults.EmptyHttpResult
Expand Down
6 changes: 6 additions & 0 deletions src/Http/Http.Abstractions/src/StatusCodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,12 @@ public static class StatusCodes
/// </summary>
public const int Status451UnavailableForLegalReasons = 451;

/// <summary>
/// HTTP status code 499. This is an unofficial status code originally defined by Nginx and is commonly used
/// in logs when the client has disconnected.
/// </summary>
public const int Status499ClientClosedRequest = 499;

/// <summary>
/// HTTP status code 500.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ await writer.WriteAsync(new ProblemDetailsContext()
[Theory]
[InlineData(StatusCodes.Status400BadRequest, "Bad Request", "https://tools.ietf.org/html/rfc9110#section-15.5.1")]
[InlineData(StatusCodes.Status418ImATeapot, "I'm a teapot", null)]
[InlineData(499, null, null)]
[InlineData(498, null, null)]
public async Task WriteAsync_UsesStatusCode_FromProblemDetails_WhenSpecified(
int statusCode,
string title,
Expand Down
3 changes: 1 addition & 2 deletions src/Http/Http.Results/test/ResultsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1076,15 +1076,14 @@ public void Problem_WithArgs_ResultHasCorrectValues()
[Theory]
[InlineData(StatusCodes.Status400BadRequest, "Bad Request", "https://tools.ietf.org/html/rfc9110#section-15.5.1")]
[InlineData(StatusCodes.Status418ImATeapot, "I'm a teapot", null)]
[InlineData(499, null, null)]
[InlineData(498, null, null)]
public void Problem_WithOnlyHttpStatus_ResultHasCorrectValues(
int statusCode,
string title,
string type)
{
// Act
var result = Results.Problem(statusCode: statusCode) as ProblemHttpResult;

// Assert
Assert.Null(result.ProblemDetails.Detail);
Assert.Null(result.ProblemDetails.Instance);
Expand Down
1 change: 1 addition & 0 deletions src/Http/WebUtilities/src/ReasonPhrases.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public static class ReasonPhrases
{ 429, "Too Many Requests" },
{ 431, "Request Header Fields Too Large" },
{ 451, "Unavailable For Legal Reasons" },
{ 499, "Client Closed Request" },

{ 500, "Internal Server Error" },
{ 501, "Not Implemented" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,18 @@ public async Task Invoke(HttpContext context)
}
catch (Exception ex)
{
if ((ex is OperationCanceledException || ex is IOException) && context.RequestAborted.IsCancellationRequested)
{
_logger.RequestAbortedException();

if (!context.Response.HasStarted)
{
context.Response.StatusCode = StatusCodes.Status499ClientClosedRequest;
}

return;
}

_logger.UnhandledException(ex);

if (context.Response.HasStarted)
Expand Down
3 changes: 3 additions & 0 deletions src/Middleware/Diagnostics/src/DiagnosticsLoggerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ internal static partial class DiagnosticsLoggerExtensions
[LoggerMessage(1, LogLevel.Error, "An unhandled exception has occurred while executing the request.", EventName = "UnhandledException")]
public static partial void UnhandledException(this ILogger logger, Exception exception);

[LoggerMessage(4, LogLevel.Debug, "The request was aborted by the client.", EventName = "RequestAborted")]
public static partial void RequestAbortedException(this ILogger logger);

// ExceptionHandlerMiddleware
[LoggerMessage(2, LogLevel.Warning, "The response has already started, the error handler will not be executed.", EventName = "ResponseStarted")]
public static partial void ResponseStartedErrorHandler(this ILogger logger);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,18 @@ static async Task Awaited(ExceptionHandlerMiddlewareImpl middleware, HttpContext

private async Task HandleException(HttpContext context, ExceptionDispatchInfo edi)
{
if ((edi.SourceException is OperationCanceledException || edi.SourceException is IOException) && context.RequestAborted.IsCancellationRequested)
{
_logger.RequestAbortedException();

if (!context.Response.HasStarted)
{
context.Response.StatusCode = StatusCodes.Status499ClientClosedRequest;
}

return;
}

_logger.UnhandledException(edi.SourceException);
// We can't do anything if the response has already started, just abort.
if (context.Response.HasStarted)
Expand Down
1 change: 1 addition & 0 deletions src/Servers/HttpSys/src/LoggerEventIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,5 @@ internal static class LoggerEventIds
public const int ListenerDisposing = 46;
public const int RequestValidationFailed = 47;
public const int CreateDisconnectTokenError = 48;
public const int RequestAborted = 49;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,7 @@ internal static partial class RequestContextLog

[LoggerMessage(LoggerEventIds.RequestsDrained, LogLevel.Information, "All requests drained.", EventName = "RequestsDrained")]
public static partial void RequestsDrained(ILogger logger);

[LoggerMessage(LoggerEventIds.RequestAborted, LogLevel.Debug, "The request was aborted by the client.", EventName = "RequestAborted")]
public static partial void RequestAborted(ILogger logger);
}
13 changes: 12 additions & 1 deletion src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,14 @@ public override async Task ExecuteAsync()
{
applicationException = ex;

Log.RequestProcessError(Logger, ex);
if ((ex is OperationCanceledException || ex is IOException) && DisconnectToken.IsCancellationRequested)
{
Log.RequestAborted(Logger);
}
else
{
Log.RequestProcessError(Logger, ex);
}
if (Response.HasStarted)
{
// Otherwise the default is Cancel = 0x8 (h2) or 0x010c (h3).
Expand Down Expand Up @@ -84,6 +91,10 @@ public override async Task ExecuteAsync()
{
SetFatalResponse(badHttpRequestException.StatusCode);
}
else if ((ex is OperationCanceledException || ex is IOException) && DisconnectToken.IsCancellationRequested)
{
SetFatalResponse(StatusCodes.Status499ClientClosedRequest);
}
else
{
SetFatalResponse(StatusCodes.Status500InternalServerError);
Expand Down
3 changes: 3 additions & 0 deletions src/Servers/IIS/IIS/src/Core/IISHttpContext.Log.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,8 @@ public static void ConnectionBadRequest(ILogger logger, string connectionId, Mic

[LoggerMessage(4, LogLevel.Debug, @"Connection id ""{ConnectionId}"" bad request data: ""{message}""", EventName = nameof(ConnectionBadRequest))]
private static partial void ConnectionBadRequest(ILogger logger, string connectionId, string message, Microsoft.AspNetCore.Http.BadHttpRequestException ex);

[LoggerMessage(5, LogLevel.Debug, @"Connection ID ""{ConnectionId}"", Request ID ""{TraceIdentifier}"": The request was aborted by the client.", EventName = "RequestAborted")]
public static partial void RequestAborted(ILogger logger, string connectionId, string traceIdentifier);
}
}
5 changes: 5 additions & 0 deletions src/Servers/IIS/IIS/src/Core/IISHttpContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,11 @@ protected void ReportApplicationError(Exception ex)
Log.ApplicationError(_logger, ((IHttpConnectionFeature)this).ConnectionId, ((IHttpRequestIdentifierFeature)this).TraceIdentifier, ex);
}

protected void ReportRequestAborted()
{
Log.RequestAborted(_logger, ((IHttpConnectionFeature)this).ConnectionId, ((IHttpRequestIdentifierFeature)this).TraceIdentifier);
}

public void PostCompletion(NativeMethods.REQUEST_NOTIFICATION_STATUS requestNotificationStatus)
{
NativeMethods.HttpSetCompletionStatus(_requestNativeHandle, requestNotificationStatus);
Expand Down
16 changes: 12 additions & 4 deletions src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Buffers;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Server.IIS.Core;
Expand Down Expand Up @@ -43,7 +44,15 @@ public override async Task<bool> ProcessRequestAsync()
}
catch (Exception ex)
{
ReportApplicationError(ex);
if ((ex is OperationCanceledException || ex is IOException) && ClientDisconnected)
{
ReportRequestAborted();
}
else
{
ReportApplicationError(ex);
}

success = false;
}

Expand Down Expand Up @@ -82,9 +91,8 @@ public override async Task<bool> ProcessRequestAsync()
}
else if (!HasResponseStarted && _requestRejectedException == null)
{
// If the request was aborted and no response was sent, there's no
// meaningful status code to log.
StatusCode = 0;
// If the request was aborted and no response was sent, we use status code 499 for logging
StatusCode = ClientDisconnected ? StatusCodes.Status499ClientClosedRequest : 0;
success = false;
}

Expand Down
14 changes: 10 additions & 4 deletions src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -698,7 +698,14 @@ private async Task ProcessRequests<TContext>(IHttpApplication<TContext> applicat
}
catch (Exception ex)
{
ReportApplicationError(ex);
if ((ex is OperationCanceledException || ex is IOException) && _connectionAborted)
{
Log.RequestAborted(ConnectionId, TraceIdentifier);
}
else
{
ReportApplicationError(ex);
}
}

KestrelEventSource.Log.RequestStop(this);
Expand Down Expand Up @@ -739,9 +746,8 @@ private async Task ProcessRequests<TContext>(IHttpApplication<TContext> applicat
}
else if (!HasResponseStarted)
{
// If the request was aborted and no response was sent, there's no
// meaningful status code to log.
StatusCode = 0;
// If the request was aborted and no response was sent, we use status code 499 for logging
StatusCode = StatusCodes.Status499ClientClosedRequest;
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/Servers/Kestrel/Core/src/Internal/Http/ReasonPhrases.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ internal static class ReasonPhrases
private static readonly byte[] _bytesStatus429 = CreateStatusBytes(StatusCodes.Status429TooManyRequests);
private static readonly byte[] _bytesStatus431 = CreateStatusBytes(StatusCodes.Status431RequestHeaderFieldsTooLarge);
private static readonly byte[] _bytesStatus451 = CreateStatusBytes(StatusCodes.Status451UnavailableForLegalReasons);
private static readonly byte[] _bytesStatus499 = CreateStatusBytes(StatusCodes.Status499ClientClosedRequest);

private static readonly byte[] _bytesStatus500 = CreateStatusBytes(StatusCodes.Status500InternalServerError);
private static readonly byte[] _bytesStatus501 = CreateStatusBytes(StatusCodes.Status501NotImplemented);
Expand Down Expand Up @@ -149,6 +150,7 @@ public static byte[] ToStatusBytes(int statusCode, string? reasonPhrase = null)
StatusCodes.Status429TooManyRequests => _bytesStatus429,
StatusCodes.Status431RequestHeaderFieldsTooLarge => _bytesStatus431,
StatusCodes.Status451UnavailableForLegalReasons => _bytesStatus451,
StatusCodes.Status499ClientClosedRequest => _bytesStatus499,

StatusCodes.Status500InternalServerError => _bytesStatus500,
StatusCodes.Status501NotImplemented => _bytesStatus501,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ public void Http3DisabledWithHttp1AndNoTls(EndPoint endPoint)
GeneralLog.Http3DisabledWithHttp1AndNoTls(_generalLogger, endPoint);
}

public void RequestAborted(string connectionId, string traceIdentifier)
{
GeneralLog.RequestAbortedException(_generalLogger, connectionId, traceIdentifier);
}

private static partial class GeneralLog
{
[LoggerMessage(13, LogLevel.Error, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": An unhandled exception was thrown by the application.", EventName = "ApplicationError")]
Expand Down Expand Up @@ -99,6 +104,9 @@ private static partial class GeneralLog
[LoggerMessage(65, LogLevel.Warning, "HTTP/3 is not enabled for {Endpoint}. HTTP/3 requires TLS. Connections to this endpoint will use HTTP/1.1.", EventName = "Http3DisabledWithHttp1AndNoTls")]
public static partial void Http3DisabledWithHttp1AndNoTls(ILogger logger, EndPoint endPoint);

// Highest shared ID is 65. New consecutive IDs start at 66
[LoggerMessage(66, LogLevel.Debug, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": The request was aborted by the client.", EventName = "RequestAborted")]
public static partial void RequestAbortedException(ILogger logger, string connectionId, string traceIdentifier);

// Highest shared ID is 66. New consecutive IDs start at 67
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ public Task ResponseStatusCodeSetBeforeHttpContextDisposeRequestAborted()
return Task.CompletedTask;
},
expectedClientStatusCode: null,
expectedServerStatusCode: 0);
expectedServerStatusCode: (HttpStatusCode)499);
}

[Fact]
Expand All @@ -309,7 +309,7 @@ public Task ResponseStatusCodeSetBeforeHttpContextDisposeRequestAbortedAppExcept
throw new Exception();
},
expectedClientStatusCode: null,
expectedServerStatusCode: 0);
expectedServerStatusCode: (HttpStatusCode)499);
}

[Fact]
Expand Down

0 comments on commit 9068fcf

Please sign in to comment.