diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index 321784dfe7e4..fabae04d35e5 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -181,6 +181,10 @@ public void Abort() } _requestAbortSource.Dispose(); } + else + { + _disconnectToken = new CancellationToken(canceled: true); + } ForceCancelRequest(); Request.Dispose(); // Only Abort, Response.Dispose() tries a graceful flush diff --git a/src/Servers/HttpSys/test/FunctionalTests/RequestTests.cs b/src/Servers/HttpSys/test/FunctionalTests/RequestTests.cs index def6ce32c693..b40df9a02f41 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/RequestTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/RequestTests.cs @@ -468,6 +468,101 @@ public async Task Request_EscapedControlCharacters_400() } } + [ConditionalFact] + public async Task RequestAborted_AfterAccessingProperty_Notified() + { + var registered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var result = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using var server = Utilities.CreateHttpServerReturnRoot("/", out var address, async httpContext => + { + var ct = httpContext.RequestAborted; + + if (!ct.CanBeCanceled || ct.IsCancellationRequested) + { + result.SetException(new Exception("The CT isn't valid.")); + return; + } + + ct.Register(() => result.SetResult()); + + registered.SetResult(); + + // Don't exit until it fires or else it could be disposed. + await result.Task.DefaultTimeout(); + }); + + // Send a request and then abort. + + var uri = new Uri(address); + StringBuilder builder = new StringBuilder(); + builder.AppendLine("POST / HTTP/1.1"); + builder.AppendLine("Connection: close"); + builder.AppendLine("Content-Length: 10"); + builder.Append("HOST: "); + builder.AppendLine(uri.Authority); + builder.AppendLine(); + + byte[] request = Encoding.ASCII.GetBytes(builder.ToString()); + + using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp); + + await socket.ConnectAsync(uri.Host, uri.Port); + socket.Send(request); + + // Wait for the token to be setup before aborting. + await registered.Task.DefaultTimeout(); + + socket.Close(); + + await result.Task.DefaultTimeout(); + } + + [ConditionalFact] + public async Task RequestAbortedDurringRead_BeforeAccessingProperty_TokenAlreadyCanceled() + { + var requestAborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var result = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using var server = Utilities.CreateHttpServerReturnRoot("/", out var address, async httpContext => + { + await requestAborted.Task.DefaultTimeout(); + try + { + await httpContext.Request.Body.ReadAsync(new byte[10]).DefaultTimeout(); + result.SetException(new Exception("This should have aborted")); + return; + } + catch (IOException) + { + } + + result.SetResult(httpContext.RequestAborted.IsCancellationRequested); + }); + + // Send a request and then abort. + + var uri = new Uri(address); + StringBuilder builder = new StringBuilder(); + builder.AppendLine("POST / HTTP/1.1"); + builder.AppendLine("Connection: close"); + builder.AppendLine("Content-Length: 10"); + builder.Append("HOST: "); + builder.AppendLine(uri.Authority); + builder.AppendLine(); + + byte[] request = Encoding.ASCII.GetBytes(builder.ToString()); + + using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp); + + await socket.ConnectAsync(uri.Host, uri.Port); + socket.Send(request); + socket.Close(); + + requestAborted.SetResult(); + + var wasCancelled = await result.Task; + Assert.True(wasCancelled); + } + private IServer CreateServer(out string root, RequestDelegate app) { // TODO: We're just doing this to get a dynamic port. This can be removed later when we add support for hot-adding prefixes.