diff --git a/src/Hosting/TestHost/src/HttpContextBuilder.cs b/src/Hosting/TestHost/src/HttpContextBuilder.cs index aab30590aa..c286f26488 100644 --- a/src/Hosting/TestHost/src/HttpContextBuilder.cs +++ b/src/Hosting/TestHost/src/HttpContextBuilder.cs @@ -110,8 +110,10 @@ namespace Microsoft.AspNetCore.TestHost try { await _application.ProcessRequestAsync(_testContext); - await CompleteRequestAsync(); + + // Matches Kestrel server: response is completed before request is drained await CompleteResponseAsync(); + await CompleteRequestAsync(); _application.DisposeContext(_testContext, exception: null); } catch (Exception ex) @@ -171,18 +173,9 @@ namespace Microsoft.AspNetCore.TestHost await _requestPipe.Reader.CompleteAsync(); } - if (_sendRequestStreamTask != null) - { - try - { - // Ensure duplex request is either completely read or has been aborted. - await _sendRequestStreamTask; - } - catch (OperationCanceledException) - { - // Request was canceled, likely because it wasn't read before the request ended. - } - } + // Don't wait for request to drain. It could block indefinitely. In a real server + // we would wait for a timeout and then kill the socket. + // Potential future improvement: add logging that the request timed out } internal async Task CompleteResponseAsync() diff --git a/src/Hosting/TestHost/test/TestClientTests.cs b/src/Hosting/TestHost/test/TestClientTests.cs index 9ae6caebfe..b4f5b06b4d 100644 --- a/src/Hosting/TestHost/test/TestClientTests.cs +++ b/src/Hosting/TestHost/test/TestClientTests.cs @@ -384,6 +384,53 @@ namespace Microsoft.AspNetCore.TestHost await writeTask; } + [Fact] + public async Task ClientStreaming_ResponseCompletesWithoutResponseBodyWrite() + { + // Arrange + var requestStreamTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + RequestDelegate appDelegate = ctx => + { + ctx.Response.Headers["test-header"] = "true"; + return Task.CompletedTask; + }; + + Stream requestStream = null; + + var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate)); + var server = new TestServer(builder); + var client = server.CreateClient(); + + var httpRequest = new HttpRequestMessage(HttpMethod.Post, "http://localhost:12345"); + httpRequest.Version = new Version(2, 0); + httpRequest.Content = new PushContent(async stream => + { + requestStream = stream; + await requestStreamTcs.Task; + }); + + // Act + var response = await client.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead).WithTimeout(); + + var responseContent = await response.Content.ReadAsStreamAsync().WithTimeout(); + + // Assert + response.EnsureSuccessStatusCode(); + Assert.Equal("true", response.Headers.GetValues("test-header").Single()); + + // Read response + byte[] buffer = new byte[1024]; + var length = await responseContent.ReadAsync(buffer).AsTask().WithTimeout(); + Assert.Equal(0, length); + + // Writing to request stream will fail because server is complete + await Assert.ThrowsAnyAsync(() => requestStream.WriteAsync(buffer).AsTask()); + + // Unblock request + requestStreamTcs.TrySetResult(null); + } + [Fact] public async Task ClientStreaming_ServerAbort() {