public async Task WritingToConnectionAfterUnobservedCloseTriggersRequestAbortedToken(ListenOptions listenOptions) { const int connectionPausedEventId = 4; const int maxRequestBufferSize = 4096; var requestAborted = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var readCallbackUnwired = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var clientClosedConnection = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var writeTcs = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); TestSink.MessageLogged += context => { if (context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv" && context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets") { return; } if (context.EventId.Id == connectionPausedEventId) { readCallbackUnwired.TrySetResult(null); } }; var mockKestrelTrace = new Mock <IKestrelTrace>(); var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object) { ServerOptions = { Limits = { MaxRequestBufferSize = maxRequestBufferSize, MaxRequestLineSize = maxRequestBufferSize, MaxRequestHeadersTotalSize = maxRequestBufferSize, } } }; var scratchBuffer = new byte[maxRequestBufferSize * 8]; using (var server = new TestServer(async context => { context.RequestAborted.Register(() => requestAborted.SetResult(null)); await clientClosedConnection.Task; try { for (var i = 0; i < 1000; i++) { await context.Response.BodyWriter.WriteAsync(new Memory <byte>(scratchBuffer, 0, scratchBuffer.Length), context.RequestAborted); await Task.Delay(10); } } catch (Exception ex) { writeTcs.SetException(ex); throw; } finally { await requestAborted.Task.DefaultTimeout(); } writeTcs.SetException(new Exception("This shouldn't be reached.")); }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { await connection.Send( "POST / HTTP/1.1", "Host:", $"Content-Length: {scratchBuffer.Length}", "", ""); var ignore = connection.Stream.WriteAsync(scratchBuffer, 0, scratchBuffer.Length); // Wait until the read callback is no longer hooked up so that the connection disconnect isn't observed. await readCallbackUnwired.Task.DefaultTimeout(); } clientClosedConnection.SetResult(null); await Assert.ThrowsAnyAsync <OperationCanceledException>(() => writeTcs.Task).DefaultTimeout(); await server.StopAsync(); } mockKestrelTrace.Verify(t => t.ConnectionStop(It.IsAny <string>()), Times.Once()); Assert.True(requestAborted.Task.IsCompleted); }
public async Task AppCanHandleClientAbortingConnectionMidResponse(ListenOptions listenOptions) { const int connectionResetEventId = 19; const int connectionFinEventId = 6; const int connectionStopEventId = 2; const int responseBodySegmentSize = 65536; const int responseBodySegmentCount = 100; var requestAborted = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var appCompletedTcs = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var scratchBuffer = new byte[responseBodySegmentSize]; using (var server = new TestServer(async context => { context.RequestAborted.Register(() => requestAborted.SetResult(null)); for (var i = 0; i < responseBodySegmentCount; i++) { await context.Response.Body.WriteAsync(scratchBuffer, 0, scratchBuffer.Length); await Task.Delay(10); } await requestAborted.Task.DefaultTimeout(); appCompletedTcs.SetResult(null); }, new TestServiceContext(LoggerFactory), listenOptions)) { using (var connection = server.CreateConnection()) { await connection.Send( "GET / HTTP/1.1", "Host:", "", ""); // Read just part of the response and close the connection. // https://github.com/aspnet/KestrelHttpServer/issues/2554 await connection.Stream.ReadAsync(scratchBuffer, 0, scratchBuffer.Length); connection.Reset(); } await requestAborted.Task.DefaultTimeout(); // After the RequestAborted token is tripped, the connection reset should be logged. // On Linux and macOS, the connection close is still sometimes observed as a FIN despite the LingerState. var presShutdownTransportLogs = TestSink.Writes.Where( w => w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv" || w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets"); var connectionResetLogs = presShutdownTransportLogs.Where( w => w.EventId == connectionResetEventId || (!TestPlatformHelper.IsWindows && w.EventId == connectionFinEventId)); Assert.NotEmpty(connectionResetLogs); // On macOS, the default 5 shutdown timeout is insufficient for the write loop to complete, so give it extra time. await appCompletedTcs.Task.DefaultTimeout(); await server.StopAsync(); } var coreLogs = TestSink.Writes.Where(w => w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel"); Assert.Single(coreLogs.Where(w => w.EventId == connectionStopEventId)); var transportLogs = TestSink.Writes.Where(w => w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel" || w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv" || w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets"); Assert.Empty(transportLogs.Where(w => w.LogLevel > LogLevel.Debug)); }
public async Task ThrowsOnWriteWithRequestAbortedTokenAfterRequestIsAborted(ListenOptions listenOptions) { // This should match _maxBytesPreCompleted in SocketOutput var maxBytesPreCompleted = 65536; // Ensure string is long enough to disable write-behind buffering var largeString = new string('a', maxBytesPreCompleted + 1); var writeTcs = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var requestAbortedWh = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var requestStartWh = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); using (var server = new TestServer(async httpContext => { requestStartWh.SetResult(null); var response = httpContext.Response; var request = httpContext.Request; var lifetime = httpContext.Features.Get <IHttpRequestLifetimeFeature>(); lifetime.RequestAborted.Register(() => requestAbortedWh.SetResult(null)); await requestAbortedWh.Task.DefaultTimeout(); try { await response.WriteAsync(largeString, cancellationToken: lifetime.RequestAborted); } catch (Exception ex) { writeTcs.SetException(ex); throw; } finally { await requestAbortedWh.Task.DefaultTimeout(); } writeTcs.SetException(new Exception("This shouldn't be reached.")); }, new TestServiceContext(LoggerFactory), listenOptions)) { using (var connection = server.CreateConnection()) { await connection.Send( "POST / HTTP/1.1", "Host:", "Content-Length: 0", "", ""); await requestStartWh.Task.DefaultTimeout(); } // Write failed - can throw TaskCanceledException or OperationCanceledException, // depending on how far the canceled write goes. await Assert.ThrowsAnyAsync <OperationCanceledException>(async() => await writeTcs.Task).DefaultTimeout(); // RequestAborted tripped await requestAbortedWh.Task.DefaultTimeout(); await server.StopAsync(); } }