dotnet / dotnet/aspnetcore

TestServer: Aggressive response streaming causes OperationCanceledException during request serialization

Open
#65,541 0 comments 0 reactions 0 assignees View on GitHub
area-networking
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.