Skip to content

Commit 34294a3

Browse files
JamesNKanalogrelay
authored andcommitted
Fix TestServer hang with duplex streaming requests (#17158)
1 parent ad8ecf9 commit 34294a3

File tree

2 files changed

+53
-13
lines changed

2 files changed

+53
-13
lines changed

src/Hosting/TestHost/src/HttpContextBuilder.cs

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,10 @@ async Task RunRequestAsync()
110110
try
111111
{
112112
await _application.ProcessRequestAsync(_testContext);
113-
await CompleteRequestAsync();
113+
114+
// Matches Kestrel server: response is completed before request is drained
114115
await CompleteResponseAsync();
116+
await CompleteRequestAsync();
115117
_application.DisposeContext(_testContext, exception: null);
116118
}
117119
catch (Exception ex)
@@ -171,18 +173,9 @@ private async Task CompleteRequestAsync()
171173
await _requestPipe.Reader.CompleteAsync();
172174
}
173175

174-
if (_sendRequestStreamTask != null)
175-
{
176-
try
177-
{
178-
// Ensure duplex request is either completely read or has been aborted.
179-
await _sendRequestStreamTask;
180-
}
181-
catch (OperationCanceledException)
182-
{
183-
// Request was canceled, likely because it wasn't read before the request ended.
184-
}
185-
}
176+
// Don't wait for request to drain. It could block indefinitely. In a real server
177+
// we would wait for a timeout and then kill the socket.
178+
// Potential future improvement: add logging that the request timed out
186179
}
187180

188181
internal async Task CompleteResponseAsync()

src/Hosting/TestHost/test/TestClientTests.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,53 @@ public async Task ClientStreaming_ResponseCompletesWithoutReadingRequest()
384384
await writeTask;
385385
}
386386

387+
[Fact]
388+
public async Task ClientStreaming_ResponseCompletesWithoutResponseBodyWrite()
389+
{
390+
// Arrange
391+
var requestStreamTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
392+
393+
RequestDelegate appDelegate = ctx =>
394+
{
395+
ctx.Response.Headers["test-header"] = "true";
396+
return Task.CompletedTask;
397+
};
398+
399+
Stream requestStream = null;
400+
401+
var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate));
402+
var server = new TestServer(builder);
403+
var client = server.CreateClient();
404+
405+
var httpRequest = new HttpRequestMessage(HttpMethod.Post, "http://localhost:12345");
406+
httpRequest.Version = new Version(2, 0);
407+
httpRequest.Content = new PushContent(async stream =>
408+
{
409+
requestStream = stream;
410+
await requestStreamTcs.Task;
411+
});
412+
413+
// Act
414+
var response = await client.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead).WithTimeout();
415+
416+
var responseContent = await response.Content.ReadAsStreamAsync().WithTimeout();
417+
418+
// Assert
419+
response.EnsureSuccessStatusCode();
420+
Assert.Equal("true", response.Headers.GetValues("test-header").Single());
421+
422+
// Read response
423+
byte[] buffer = new byte[1024];
424+
var length = await responseContent.ReadAsync(buffer).AsTask().WithTimeout();
425+
Assert.Equal(0, length);
426+
427+
// Writing to request stream will fail because server is complete
428+
await Assert.ThrowsAnyAsync<Exception>(() => requestStream.WriteAsync(buffer).AsTask());
429+
430+
// Unblock request
431+
requestStreamTcs.TrySetResult(null);
432+
}
433+
387434
[Fact]
388435
public async Task ClientStreaming_ServerAbort()
389436
{

0 commit comments

Comments
 (0)