TestServer: Aggressive response streaming causes OperationCanceledException during request serialization
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 276
Description
### Is there an existing issue for this?
- [x] I have searched the existing issues
### Describe the bug
### Summary
When a server endpoint uses aggressive response streaming (with frequent flushes), TestServer's shared pipe/stream architecture can cause `OperationCanceledException` to propagate to the client's request serialization, even though the request and response should be independent streams in a real HTTP connection.
### Problem
TestServer appears to use a shared `Stream` or `PipeWriter` for the request/response pipeline, which doesn't properly simulate the **full-duplex, independent nature of real HTTP connections**. When the server starts aggressively streaming the response (e.g., chunked transfer encoding with per-item flushes), the pipe completion/cancellation can affect the client's ability to write the request body.
In a real HTTP connection:
- **Request stream**: Client → Server (independent, can be chunked)
- **Response stream**: Server → Client (independent, can be chunked)
- These operate as **separate channels** that can work simultaneously
With TestServer's current implementation:
- Request and response are more tightly coupled via shared pipe/stream
- Aggressive streaming/flushing on the response side can trigger cancellation on the request side
- This doesn't properly simulate the buffering and independence of real HTTP
### To Reproduce
**Server-side code** (ASP.NET Core controller with aggressive streaming):
```csharp
[HttpPost("api/bulk")]
public async Task Post([FromBody] BulkRequest request)
{
Response.ContentType = "application/json";
// Disable buffering for true streaming
var bufferingFeature = Response.HttpContext.Features.Get();
bufferingFeature?.DisableBuffering();
await using var writer = new Utf8JsonWriter(Response.Body);
writer.WriteStartArray();
foreach (var item in ProcessItems(request))
{
JsonSerializer.Serialize(writer, item);
await writer.FlushAsync(); // Aggressive flushing per item
await Response.Body.FlushAsync();
}
writer.WriteEndArray();
await writer.FlushAsync();
}
```
**Test code** (using TestServer):
```csharp
var testServer = new TestServer(new WebHostBuilder().UseStartup());
var client = testServer.CreateClient();
var request = new BulkRequest { Items = GenerateLargeItemList(10000) };
// Using Newtonsoft.Json formatter (or any streaming serializer)
var httpContent = new ObjectContent(
request,
new JsonMediaTypeFormatter(),
"application/json"
);
// This throws OperationCanceledException during request serialization
var response = await client.PostAsync("api/bulk", httpContent);
```
**Exception**:
```
System.OperationCanceledException: Flush was canceled on underlying PipeWriter.
at System.IO.Pipelines.PipeWriterStream.g__AwaitTask|29_0(ValueTask`1 valueTask)
at Newtonsoft.Json.Utilities.JavaScriptUtils.WriteEscapedJavaScriptString(...)
```
### Expected Behavior
The client should be able to complete writing the request body independently of the server's response streaming behavior, just like in a real HTTP connection over TCP.
### Actual Behavior
The server's aggressive response streaming causes cancellation to propagate to the client's request serialization, resulting in `OperationCanceledException`.
### Workaround
- Use System.Text.Json on both client and server (reduces serialization time)
- Test against an actual Kestrel instance over TCP instead of TestServer
- Avoid aggressive flushing in server code when using TestServer
### Suggested Fix
Consider using `System.Threading.Channels` or separate `Pipe` instances for request and response streams to better simulate the full-duplex, independent nature of real HTTP connections. This would:
- Properly isolate request from response stream lifecycle
- Better simulate chunked transfer encoding in both directions
- Prevent cancellation propagation between independent streams
- More accurately represent real HTTP behavior
### Related Issues
- #21677 - TestServer doesn't properly set `Transfer-Encoding` header for streaming
- #17158 - Fixed TestServer hang with duplex streaming (but didn't address stream independence)
### Environment
- ASP.NET Core version: 8.0 (likely affects earlier versions)
- TestServer package: Microsoft.AspNetCore.TestHost
### Additional Context
The author of #21677 suggested: *"I'd prefer to see TestServer deprecated in favour of an in-memory kestrel"* - which would naturally solve this issue by providing true stream independence.
Contributor guide
Assessment
This issue has not been assessed yet.