public async Task GracefulShutdownWaitsForRequestsToFinish() { var requestStarted = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var requestUnblocked = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var requestStopping = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var mockKestrelTrace = new Mock <KestrelTrace>(TestApplicationErrorLogger) { CallBase = true }; mockKestrelTrace .Setup(m => m.Http2ConnectionClosing(It.IsAny <string>())) .Callback(() => requestStopping.SetResult(null)); var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object) { ExpectedConnectionMiddlewareCount = 1 }; testContext.InitializeHeartbeat(); using (var server = new TestServer(async context => { requestStarted.SetResult(null); await requestUnblocked.Task.DefaultTimeout(); await context.Response.WriteAsync("hello world " + context.Request.Protocol); }, testContext, kestrelOptions => { kestrelOptions.Listen(IPAddress.Loopback, 0, listenOptions => { listenOptions.Protocols = HttpProtocols.Http2; listenOptions.UseHttps(_x509Certificate2); }); })) { var requestTask = Client.GetStringAsync($"https://localhost:{server.Port}/"); Assert.False(requestTask.IsCompleted); await requestStarted.Task.DefaultTimeout(); var stopTask = server.StopAsync(); await requestStopping.Task.DefaultTimeout(); // Unblock the request requestUnblocked.SetResult(null); Assert.Equal("hello world HTTP/2", await requestTask); await stopTask.DefaultTimeout(); } Assert.Contains(TestApplicationErrorLogger.Messages, m => m.Message.Contains("Request finished in")); Assert.Contains(TestApplicationErrorLogger.Messages, m => m.Message.Contains("is closing.")); Assert.Contains(TestApplicationErrorLogger.Messages, m => m.Message.Contains("is closed. The last processed stream ID was 1.")); }
public async Task CriticalErrorLoggedIfApplicationDoesntComplete() { //////////////////////////////////////////////////////////////////////////////////////// // WARNING: This test will fail under a debugger because Task.s_currentActiveTasks // // roots HttpConnection. // //////////////////////////////////////////////////////////////////////////////////////// var logWh = new SemaphoreSlim(0); var appStartedWh = new SemaphoreSlim(0); var mockTrace = new Mock <KestrelTrace>(Logger) { CallBase = true }; mockTrace .Setup(trace => trace.ApplicationNeverCompleted(It.IsAny <string>())) .Callback(() => { logWh.Release(); }); var testContext = new TestServiceContext(new LoggerFactory(), mockTrace.Object); testContext.InitializeHeartbeat(); using (var server = new TestServer(context => { appStartedWh.Release(); var tcs = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); return(tcs.Task); }, testContext)) { using (var connection = server.CreateConnection()) { await connection.SendEmptyGet(); Assert.True(await appStartedWh.WaitAsync(TestConstants.DefaultTimeout)); // Close connection without waiting for a response } var logWaitAttempts = 0; for (; !await logWh.WaitAsync(TimeSpan.FromSeconds(1)) && logWaitAttempts < 30; logWaitAttempts++) { GC.Collect(); GC.WaitForPendingFinalizers(); } Assert.True(logWaitAttempts < 10); await server.StopAsync(); } }
public async Task CriticalErrorLoggedIfApplicationDoesntComplete() { //////////////////////////////////////////////////////////////////////////////////////// // WARNING: This test will fail under a debugger because Task.s_currentActiveTasks // // roots HttpConnection. // //////////////////////////////////////////////////////////////////////////////////////// var logWh = new SemaphoreSlim(0); var appStartedWh = new SemaphoreSlim(0); var factory = new LoggerFactory(); // Use a custom logger for callback instead of TestSink because TestSink keeps references // to types when logging, prevents garbage collection, and makes the test fail. factory.AddProvider(new CallbackLoggerProvider(eventId => { if (eventId.Name == "ApplicationNeverCompleted") { Logger.LogInformation("Releasing ApplicationNeverCompleted log wait handle."); logWh.Release(); } })); var testContext = new TestServiceContext(factory); testContext.InitializeHeartbeat(); await using (var server = new TestServer(context => { appStartedWh.Release(); var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); return(tcs.Task); }, testContext)) { using (var connection = server.CreateConnection()) { await connection.SendEmptyGet(); Assert.True(await appStartedWh.WaitAsync(TestConstants.DefaultTimeout)); // Close connection without waiting for a response } var logWaitAttempts = 0; for (; !await logWh.WaitAsync(TimeSpan.FromSeconds(1)) && logWaitAttempts < 30; logWaitAttempts++) { GC.Collect(); GC.WaitForPendingFinalizers(); } Assert.True(logWaitAttempts < 10); } }
public async Task GracefulShutdownWaitsForRequestsToFinish() { var requestStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var requestUnblocked = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var requestStopping = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); TestSink.MessageLogged += context => { if (context.EventId.Name == "Http2ConnectionClosing") { requestStopping.SetResult(); } }; var testContext = new TestServiceContext(LoggerFactory); testContext.InitializeHeartbeat(); await using (var server = new TestServer(async context => { requestStarted.SetResult(); await requestUnblocked.Task.DefaultTimeout(); await context.Response.WriteAsync("hello world " + context.Request.Protocol); }, testContext, kestrelOptions => { kestrelOptions.Listen(IPAddress.Loopback, 0, listenOptions => { listenOptions.Protocols = HttpProtocols.Http2; listenOptions.UseHttps(_x509Certificate2); }); })) { var requestTask = Client.GetStringAsync($"https://localhost:{server.Port}/"); Assert.False(requestTask.IsCompleted); await requestStarted.Task.DefaultTimeout(); var stopTask = server.StopAsync(); await requestStopping.Task.DefaultTimeout(); // Unblock the request requestUnblocked.SetResult(); Assert.Equal("hello world HTTP/2", await requestTask); await stopTask.DefaultTimeout(); } Assert.Contains(LogMessages, m => m.Message.Contains("Request finished ")); Assert.Contains(LogMessages, m => m.Message.Contains("is closing.")); Assert.Contains(LogMessages, m => m.Message.Contains("is closed. The last processed stream ID was 1.")); }
public async Task RequestTimesOutWhenNotDrainedWithinDrainTimeoutPeriod() { // This test requires a real clock since we can't control when the drain timeout is set var serviceContext = new TestServiceContext(LoggerFactory); serviceContext.InitializeHeartbeat(); // Ensure there's still a constant date header value. var clock = new MockSystemClock(); var date = new DateHeaderValueManager(); date.OnHeartbeat(clock.UtcNow); serviceContext.DateHeaderValueManager = date; var appRunningEvent = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); using (var server = new TestServer(context => { context.Features.Get <IHttpMinRequestBodyDataRateFeature>().MinDataRate = null; appRunningEvent.SetResult(null); return(Task.CompletedTask); }, serviceContext)) { using (var connection = server.CreateConnection()) { await connection.Send( "POST / HTTP/1.1", "Host:", "Content-Length: 1", "", ""); await appRunningEvent.Task.DefaultTimeout(); // Disconnects after response completes due to the timeout await connection.ReceiveEnd( "HTTP/1.1 200 OK", $"Date: {serviceContext.DateHeaderValue}", "Content-Length: 0", "", ""); } await server.StopAsync(); } Assert.Contains(TestSink.Writes, w => w.EventId.Id == 32 && w.LogLevel == LogLevel.Information); Assert.Contains(TestSink.Writes, w => w.EventId.Id == 33 && w.LogLevel == LogLevel.Information); }
public async Task RequestTimesOutWhenNotDrainedWithinDrainTimeoutPeriod() { // This test requires a real clock since we can't control when the drain timeout is set var serviceContext = new TestServiceContext(LoggerFactory); serviceContext.InitializeHeartbeat(); var appRunningEvent = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); using (var server = new TestServer(context => { context.Features.Get <IHttpMinRequestBodyDataRateFeature>().MinDataRate = null; appRunningEvent.SetResult(null); return(Task.CompletedTask); }, serviceContext)) { using (var connection = server.CreateConnection()) { await connection.Send( "POST / HTTP/1.1", "Host:", "Content-Length: 1", "", ""); await appRunningEvent.Task.DefaultTimeout(); await connection.Receive( "HTTP/1.1 200 OK", ""); await connection.ReceiveStartsWith( "Date: "); // Disconnected due to the timeout await connection.ReceiveForcedEnd( "Content-Length: 0", "", ""); } } Assert.Contains(TestSink.Writes, w => w.EventId.Id == 32 && w.LogLevel == LogLevel.Information); Assert.Contains(TestSink.Writes, w => w.EventId.Id == 33 && w.LogLevel == LogLevel.Information); }
public async Task ClientCanReceiveFullConnectionCloseResponseWithoutErrorAtALowDataRate() { var chunkSize = 64 * 128 * 1024; var chunkCount = 4; var chunkData = new byte[chunkSize]; var requestAborted = false; var appFuncCompleted = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var mockKestrelTrace = new Mock <IKestrelTrace>(); var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object) { ServerOptions = { Limits = { MinResponseDataRate = new MinDataRate(bytesPerSecond: 240, gracePeriod: TimeSpan.FromSeconds(2)) } } }; testContext.InitializeHeartbeat(); var dateHeaderValueManager = new DateHeaderValueManager(); dateHeaderValueManager.OnHeartbeat(DateTimeOffset.MinValue); testContext.DateHeaderValueManager = dateHeaderValueManager; var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); async Task App(HttpContext context) { context.RequestAborted.Register(() => { requestAborted = true; }); for (var i = 0; i < chunkCount; i++) { await context.Response.BodyWriter.WriteAsync(new Memory <byte>(chunkData, 0, chunkData.Length), context.RequestAborted); } appFuncCompleted.SetResult(null); } using (var server = new TestServer(App, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { // Close the connection with the last request so AssertStreamCompleted actually completes. await connection.Send( "GET / HTTP/1.1", "Host:", "Connection: close", "", ""); await connection.Receive( "HTTP/1.1 200 OK", "Connection: close", $"Date: {dateHeaderValueManager.GetDateHeaderValues().String}"); // Make sure consuming a single chunk exceeds the 2 second timeout. var targetBytesPerSecond = chunkSize / 4; // expectedBytes was determined by manual testing. A constant Date header is used, so this shouldn't change unless // the response header writing logic or response body chunking logic itself changes. await AssertStreamCompletedAtTargetRate(connection.Stream, expectedBytes : 33_553_556, targetBytesPerSecond); await appFuncCompleted.Task.DefaultTimeout(); } await server.StopAsync(); } mockKestrelTrace.Verify(t => t.ResponseMinimumDataRateNotSatisfied(It.IsAny <string>(), It.IsAny <string>()), Times.Never()); mockKestrelTrace.Verify(t => t.ConnectionStop(It.IsAny <string>()), Times.Once()); Assert.False(requestAborted); }
public async Task ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLargeResponseHeaders() { var headerSize = 1024 * 1024; // 1 MB for each header value var headerCount = 64; // 64 MB of headers per response var requestCount = 4; // Minimum of 256 MB of total response headers var headerValue = new string('a', headerSize); var headerStringValues = new StringValues(Enumerable.Repeat(headerValue, headerCount).ToArray()); var requestAborted = false; var mockKestrelTrace = new Mock <IKestrelTrace>(); var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object) { ServerOptions = { Limits = { MinResponseDataRate = new MinDataRate(bytesPerSecond: 240, gracePeriod: TimeSpan.FromSeconds(2)) } } }; testContext.InitializeHeartbeat(); var dateHeaderValueManager = new DateHeaderValueManager(); dateHeaderValueManager.OnHeartbeat(DateTimeOffset.MinValue); testContext.DateHeaderValueManager = dateHeaderValueManager; var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); async Task App(HttpContext context) { context.RequestAborted.Register(() => { requestAborted = true; }); context.Response.Headers[$"X-Custom-Header"] = headerStringValues; context.Response.ContentLength = 0; await context.Response.BodyWriter.FlushAsync(); } using (var server = new TestServer(App, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { for (var i = 0; i < requestCount - 1; i++) { await connection.Send( "GET / HTTP/1.1", "Host:", "", ""); } await connection.Send( "GET / HTTP/1.1", "Host:", "", ""); await connection.Receive( "HTTP/1.1 200 OK", $"Date: {dateHeaderValueManager.GetDateHeaderValues().String}"); var minResponseSize = headerSize * headerCount; var minTotalOutputSize = requestCount * minResponseSize; // Make sure consuming a single set of response headers exceeds the 2 second timeout. var targetBytesPerSecond = minResponseSize / 4; // expectedBytes was determined by manual testing. A constant Date header is used, so this shouldn't change unless // the response header writing logic itself changes. await AssertBytesReceivedAtTargetRate(connection.Stream, expectedBytes : 268_439_596, targetBytesPerSecond); connection.ShutdownSend(); await connection.WaitForConnectionClose(); } await server.StopAsync(); } mockKestrelTrace.Verify(t => t.ResponseMinimumDataRateNotSatisfied(It.IsAny <string>(), It.IsAny <string>()), Times.Never()); mockKestrelTrace.Verify(t => t.ConnectionStop(It.IsAny <string>()), Times.Once()); Assert.False(requestAborted); }
public async Task ConnectionClosedWhenBothRequestAndResponseExperienceBackPressure() { const int bufferSize = 65536; const int bufferCount = 100; var responseSize = bufferCount * bufferSize; var buffer = new byte[bufferSize]; var responseRateTimeoutMessageLogged = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var connectionStopMessageLogged = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var requestAborted = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var copyToAsyncCts = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var mockKestrelTrace = new Mock <IKestrelTrace>(); mockKestrelTrace .Setup(trace => trace.ResponseMinimumDataRateNotSatisfied(It.IsAny <string>(), It.IsAny <string>())) .Callback(() => responseRateTimeoutMessageLogged.SetResult(null)); mockKestrelTrace .Setup(trace => trace.ConnectionStop(It.IsAny <string>())) .Callback(() => connectionStopMessageLogged.SetResult(null)); var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object) { ServerOptions = { Limits = { MinResponseDataRate = new MinDataRate(bytesPerSecond: 1024 * 1024, gracePeriod: TimeSpan.FromSeconds(2)), MaxRequestBodySize = responseSize } } }; testContext.InitializeHeartbeat(); var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); async Task App(HttpContext context) { context.RequestAborted.Register(() => { requestAborted.SetResult(null); }); try { await context.Request.Body.CopyToAsync(context.Response.Body); } catch (Exception ex) { copyToAsyncCts.SetException(ex); throw; } finally { await requestAborted.Task.DefaultTimeout(); } copyToAsyncCts.SetException(new Exception("This shouldn't be reached.")); } using (var server = new TestServer(App, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { // Close the connection with the last request so AssertStreamCompleted actually completes. await connection.Send( "POST / HTTP/1.1", "Host:", $"Content-Length: {responseSize}", "", ""); var sendTask = Task.Run(async() => { for (var i = 0; i < bufferCount; i++) { await connection.Stream.WriteAsync(buffer, 0, buffer.Length); await Task.Delay(10); } }); await requestAborted.Task.DefaultTimeout(); await responseRateTimeoutMessageLogged.Task.DefaultTimeout(); await connectionStopMessageLogged.Task.DefaultTimeout(); // Expect OperationCanceledException instead of IOException because the server initiated the abort due to a response rate timeout. await Assert.ThrowsAnyAsync <OperationCanceledException>(() => copyToAsyncCts.Task).DefaultTimeout(); await AssertStreamAborted(connection.Stream, responseSize); } await server.StopAsync(); } }
public async Task HttpsConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate() { const int chunkSize = 1024; const int chunks = 256 * 1024; var chunkData = new byte[chunkSize]; var certificate = TestResources.GetTestCertificate(); var responseRateTimeoutMessageLogged = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var connectionStopMessageLogged = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var aborted = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var appFuncCompleted = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var mockKestrelTrace = new Mock <IKestrelTrace>(); mockKestrelTrace .Setup(trace => trace.ResponseMinimumDataRateNotSatisfied(It.IsAny <string>(), It.IsAny <string>())) .Callback(() => responseRateTimeoutMessageLogged.SetResult(null)); mockKestrelTrace .Setup(trace => trace.ConnectionStop(It.IsAny <string>())) .Callback(() => connectionStopMessageLogged.SetResult(null)); var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object) { ServerOptions = { Limits = { MinResponseDataRate = new MinDataRate(bytesPerSecond: 1024 * 1024, gracePeriod: TimeSpan.FromSeconds(2)) } } }; testContext.InitializeHeartbeat(); void ConfigureListenOptions(ListenOptions listenOptions) { listenOptions.UseHttps(new HttpsConnectionAdapterOptions { ServerCertificate = certificate }); } using (var server = new TestServer(async context => { context.RequestAborted.Register(() => { aborted.SetResult(null); }); context.Response.ContentLength = chunks * chunkSize; try { for (var i = 0; i < chunks; i++) { await context.Response.BodyWriter.WriteAsync(new Memory <byte>(chunkData, 0, chunkData.Length), context.RequestAborted); } } catch (OperationCanceledException) { appFuncCompleted.SetResult(null); throw; } finally { await aborted.Task.DefaultTimeout(); } }, testContext, ConfigureListenOptions)) { using (var connection = server.CreateConnection()) { using (var sslStream = new SslStream(connection.Stream, false, (sender, cert, chain, errors) => true, null)) { await sslStream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false); var request = Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\n\r\n"); await sslStream.WriteAsync(request, 0, request.Length); await aborted.Task.DefaultTimeout(); await responseRateTimeoutMessageLogged.Task.DefaultTimeout(); await connectionStopMessageLogged.Task.DefaultTimeout(); await appFuncCompleted.Task.DefaultTimeout(); await AssertStreamAborted(connection.Stream, chunkSize *chunks); } } await server.StopAsync(); } }
public async Task ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate() { var logger = LoggerFactory.CreateLogger($"{ typeof(ResponseTests).FullName}.{ nameof(ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate)}"); const int chunkSize = 1024; const int chunks = 256 * 1024; var responseSize = chunks * chunkSize; var chunkData = new byte[chunkSize]; var responseRateTimeoutMessageLogged = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var connectionStopMessageLogged = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var requestAborted = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var appFuncCompleted = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var mockKestrelTrace = new Mock <IKestrelTrace>(); mockKestrelTrace .Setup(trace => trace.ResponseMinimumDataRateNotSatisfied(It.IsAny <string>(), It.IsAny <string>())) .Callback(() => responseRateTimeoutMessageLogged.SetResult(null)); mockKestrelTrace .Setup(trace => trace.ConnectionStop(It.IsAny <string>())) .Callback(() => connectionStopMessageLogged.SetResult(null)); var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object) { ServerOptions = { Limits = { MinResponseDataRate = new MinDataRate(bytesPerSecond: 1024 * 1024, gracePeriod: TimeSpan.FromSeconds(2)) } } }; testContext.InitializeHeartbeat(); var appLogger = LoggerFactory.CreateLogger("App"); async Task App(HttpContext context) { appLogger.LogInformation("Request received"); context.RequestAborted.Register(() => requestAborted.SetResult(null)); context.Response.ContentLength = responseSize; var i = 0; try { for (; i < chunks; i++) { await context.Response.BodyWriter.WriteAsync(new Memory <byte>(chunkData, 0, chunkData.Length), context.RequestAborted); await Task.Yield(); } appFuncCompleted.SetException(new Exception("This shouldn't be reached.")); } catch (OperationCanceledException) { appFuncCompleted.SetResult(null); throw; } catch (Exception ex) { appFuncCompleted.SetException(ex); } finally { appLogger.LogInformation("Wrote {total} bytes", chunkSize * i); await requestAborted.Task.DefaultTimeout(); } } using (var server = new TestServer(App, testContext)) { using (var connection = server.CreateConnection()) { logger.LogInformation("Sending request"); await connection.Send( "GET / HTTP/1.1", "Host:", "", ""); logger.LogInformation("Sent request"); var sw = Stopwatch.StartNew(); logger.LogInformation("Waiting for connection to abort."); await requestAborted.Task.DefaultTimeout(); await responseRateTimeoutMessageLogged.Task.DefaultTimeout(); await connectionStopMessageLogged.Task.DefaultTimeout(); await appFuncCompleted.Task.DefaultTimeout(); await AssertStreamAborted(connection.Stream, chunkSize *chunks); sw.Stop(); logger.LogInformation("Connection was aborted after {totalMilliseconds}ms.", sw.ElapsedMilliseconds); } await server.StopAsync(); } }
public async Task ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLargeResponseChunks() { var chunkSize = 64 * 128 * 1024; var chunkCount = 4; var chunkData = new byte[chunkSize]; var requestAborted = false; var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var testContext = new TestServiceContext(LoggerFactory) { ServerOptions = { Limits = { MinResponseDataRate = new MinDataRate(bytesPerSecond: 240, gracePeriod: TimeSpan.FromSeconds(2)) } } }; testContext.InitializeHeartbeat(); var dateHeaderValueManager = new DateHeaderValueManager(); dateHeaderValueManager.OnHeartbeat(DateTimeOffset.MinValue); testContext.DateHeaderValueManager = dateHeaderValueManager; var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); async Task App(HttpContext context) { context.RequestAborted.Register(() => { requestAborted = true; }); for (var i = 0; i < chunkCount; i++) { await context.Response.BodyWriter.WriteAsync(new Memory <byte>(chunkData, 0, chunkData.Length), context.RequestAborted); } appFuncCompleted.SetResult(); } await using (var server = new TestServer(App, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { // Close the connection with the last request so AssertStreamCompleted actually completes. await connection.Send( "GET / HTTP/1.1", "Host:", "", ""); await connection.Receive( "HTTP/1.1 200 OK", $"Date: {dateHeaderValueManager.GetDateHeaderValues().String}"); // Make sure consuming a single chunk exceeds the 2 second timeout. var targetBytesPerSecond = chunkSize / 4; // expectedBytes was determined by manual testing. A constant Date header is used, so this shouldn't change unless // the response header writing logic or response body chunking logic itself changes. await AssertBytesReceivedAtTargetRate(connection.Stream, expectedBytes : 33_553_537, targetBytesPerSecond); await appFuncCompleted.Task.DefaultTimeout(); connection.ShutdownSend(); await connection.WaitForConnectionClose(); } } Assert.Equal(0, TestSink.Writes.Count(w => w.EventId.Name == "ResponseMinimumDataRateNotSatisfied")); Assert.Equal(1, TestSink.Writes.Count(w => w.EventId.Name == "ConnectionStop")); Assert.False(requestAborted); }
public async Task HttpsConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate() { const int chunkSize = 1024; const int chunks = 256 * 1024; var chunkData = new byte[chunkSize]; var certificate = TestResources.GetTestCertificate(); var responseRateTimeoutMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var connectionStopMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var aborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); TestSink.MessageLogged += context => { if (context.EventId.Name == "ResponseMinimumDataRateNotSatisfied") { responseRateTimeoutMessageLogged.SetResult(); } if (context.EventId.Name == "ConnectionStop") { connectionStopMessageLogged.SetResult(); } }; var testContext = new TestServiceContext(LoggerFactory) { ServerOptions = { Limits = { MinResponseDataRate = new MinDataRate(bytesPerSecond: 1024 * 1024, gracePeriod: TimeSpan.FromSeconds(2)) } } }; testContext.InitializeHeartbeat(); void ConfigureListenOptions(ListenOptions listenOptions) { listenOptions.UseHttps(new HttpsConnectionAdapterOptions { ServerCertificate = certificate }); } await using (var server = new TestServer(async context => { context.RequestAborted.Register(() => { aborted.SetResult(); }); context.Response.ContentLength = chunks * chunkSize; try { for (var i = 0; i < chunks; i++) { await context.Response.BodyWriter.WriteAsync(new Memory <byte>(chunkData, 0, chunkData.Length), context.RequestAborted); } } catch (OperationCanceledException) { appFuncCompleted.SetResult(); throw; } finally { await aborted.Task.DefaultTimeout(); } }, testContext, ConfigureListenOptions)) { using (var connection = server.CreateConnection()) { using (var sslStream = new SslStream(connection.Stream, false, (sender, cert, chain, errors) => true, null)) { await sslStream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.None, false); var request = Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\n\r\n"); await sslStream.WriteAsync(request, 0, request.Length); // Don't use the 5 second timeout for debug builds. This can actually take a while. await aborted.Task.DefaultTimeout(TimeSpan.FromSeconds(30)); await responseRateTimeoutMessageLogged.Task.DefaultTimeout(); await connectionStopMessageLogged.Task.DefaultTimeout(); await appFuncCompleted.Task.DefaultTimeout(); await AssertStreamAborted(connection.Stream, chunkSize *chunks); } } } }
public async Task ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLargeResponseHeaders() { var headerSize = 1024 * 1024; // 1 MB for each header value var headerCount = 64; // 64 MB of headers per response var requestCount = 4; // Minimum of 256 MB of total response headers var headerValue = new string('a', headerSize); var headerStringValues = new StringValues(Enumerable.Repeat(headerValue, headerCount).ToArray()); var requestAborted = false; var mockKestrelTrace = new Mock <IKestrelTrace>(); var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object) { ServerOptions = { Limits = { MinResponseDataRate = new MinDataRate(bytesPerSecond: 240, gracePeriod: TimeSpan.FromSeconds(2)) } } }; testContext.InitializeHeartbeat(); var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); async Task App(HttpContext context) { context.RequestAborted.Register(() => { requestAborted = true; }); context.Response.Headers[$"X-Custom-Header"] = headerStringValues; context.Response.ContentLength = 0; await context.Response.BodyWriter.FlushAsync(); } using (var server = new TestServer(App, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { for (var i = 0; i < requestCount - 1; i++) { await connection.Send( "GET / HTTP/1.1", "Host:", "", ""); } // Close the connection with the last request so AssertStreamCompleted actually completes. await connection.Send( "GET / HTTP/1.1", "Host:", "Connection: close", "", ""); var responseSize = headerSize * headerCount; var minTotalOutputSize = requestCount * responseSize; // Make sure consuming a single set of response headers exceeds the 2 second timeout. var targetBytesPerSecond = responseSize / 4; await AssertStreamCompleted(connection.Stream, minTotalOutputSize, targetBytesPerSecond); } await server.StopAsync(); } mockKestrelTrace.Verify(t => t.ResponseMinimumDataRateNotSatisfied(It.IsAny <string>(), It.IsAny <string>()), Times.Never()); mockKestrelTrace.Verify(t => t.ConnectionStop(It.IsAny <string>()), Times.Once()); Assert.False(requestAborted); }
public async Task ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLargeResponseChunks() { var chunkSize = 64 * 128 * 1024; var chunkCount = 4; var chunkData = new byte[chunkSize]; var requestAborted = false; var appFuncCompleted = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var mockKestrelTrace = new Mock <IKestrelTrace>(); var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object) { ServerOptions = { Limits = { MinResponseDataRate = new MinDataRate(bytesPerSecond: 240, gracePeriod: TimeSpan.FromSeconds(2)) } } }; testContext.InitializeHeartbeat(); var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); async Task App(HttpContext context) { context.RequestAborted.Register(() => { requestAborted = true; }); for (var i = 0; i < chunkCount; i++) { await context.Response.BodyWriter.WriteAsync(new Memory <byte>(chunkData, 0, chunkData.Length), context.RequestAborted); } appFuncCompleted.SetResult(null); } using (var server = new TestServer(App, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { // Close the connection with the last request so AssertStreamCompleted actually completes. await connection.Send( "GET / HTTP/1.1", "Host:", "Connection: close", "", ""); var minTotalOutputSize = chunkCount * chunkSize; // Make sure consuming a single chunk exceeds the 2 second timeout. var targetBytesPerSecond = chunkSize / 4; await AssertStreamCompleted(connection.Stream, minTotalOutputSize, targetBytesPerSecond); await appFuncCompleted.Task.DefaultTimeout(); } await server.StopAsync(); } mockKestrelTrace.Verify(t => t.ResponseMinimumDataRateNotSatisfied(It.IsAny <string>(), It.IsAny <string>()), Times.Never()); mockKestrelTrace.Verify(t => t.ConnectionStop(It.IsAny <string>()), Times.Once()); Assert.False(requestAborted); }
public async Task ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate() { using (StartLog(out var loggerFactory, "ConnClosedWhenRespDoesNotSatisfyMin")) { var logger = loggerFactory.CreateLogger($"{ typeof(ResponseTests).FullName}.{ nameof(ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate)}"); const int chunkSize = 1024; const int chunks = 256 * 1024; var responseSize = chunks * chunkSize; var chunkData = new byte[chunkSize]; var responseRateTimeoutMessageLogged = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var connectionStopMessageLogged = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var requestAborted = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var appFuncCompleted = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var mockKestrelTrace = new Mock <KestrelTrace>(Logger) { CallBase = true }; mockKestrelTrace .Setup(trace => trace.ResponseMininumDataRateNotSatisfied(It.IsAny <string>(), It.IsAny <string>())) .Callback(() => responseRateTimeoutMessageLogged.SetResult(null)); mockKestrelTrace .Setup(trace => trace.ConnectionStop(It.IsAny <string>())) .Callback(() => connectionStopMessageLogged.SetResult(null)); var testContext = new TestServiceContext { LoggerFactory = loggerFactory, Log = mockKestrelTrace.Object, ServerOptions = { Limits = { MinResponseDataRate = new MinDataRate(bytesPerSecond: 1024 * 1024, gracePeriod: TimeSpan.FromSeconds(2)) } } }; testContext.InitializeHeartbeat(); var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); listenOptions.ConnectionAdapters.Add(new LoggingConnectionAdapter(loggerFactory.CreateLogger <LoggingConnectionAdapter>())); var appLogger = loggerFactory.CreateLogger("App"); async Task App(HttpContext context) { appLogger.LogInformation("Request received"); context.RequestAborted.Register(() => requestAborted.SetResult(null)); context.Response.ContentLength = responseSize; try { for (var i = 0; i < chunks; i++) { await context.Response.Body.WriteAsync(chunkData, 0, chunkData.Length, context.RequestAborted); appLogger.LogInformation("Wrote chunk of {chunkSize} bytes", chunkSize); } } catch (OperationCanceledException) { appFuncCompleted.SetResult(null); throw; } } using (var server = new TestServer(App, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { logger.LogInformation("Sending request"); await connection.Send( "GET / HTTP/1.1", "Host:", "", ""); logger.LogInformation("Sent request"); var sw = Stopwatch.StartNew(); logger.LogInformation("Waiting for connection to abort."); await requestAborted.Task.DefaultTimeout(); await responseRateTimeoutMessageLogged.Task.DefaultTimeout(); await connectionStopMessageLogged.Task.DefaultTimeout(); await appFuncCompleted.Task.DefaultTimeout(); await AssertStreamAborted(connection.Reader.BaseStream, chunkSize *chunks); sw.Stop(); logger.LogInformation("Connection was aborted after {totalMilliseconds}ms.", sw.ElapsedMilliseconds); } } } }