Kestrel WebTransport completes a client unidirectional stream's reader at FIN, before the app reads buffered data
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 55/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- csharp
- Domain
- backend, networking
Research direction
Read WebTransportStream.cs around the ConnectionClosed registration and DisposeAsync, then trace how WebTransportSession.cs handles TryRemoveStream. Reproduce with the unidirectional stream steps and compare against the bidirectional case. Done means the application can read all bytes sent before FIN, and cleanup waits until the application completes its reader or disposes the stream.
Written by the indexing model from the issue text.
Description
Is there an existing issue for this?
- I have searched the existing issues
Describe the bug
When a WebTransport client opens a unidirectional stream, writes some bytes and finishes it (FIN), Kestrel can complete the application's PipeReader for that stream before the app has read the buffered data. The app then gets:
System.InvalidOperationException: Reading is not allowed after reader was completed.
Or it reads a truncated stream. This happens intermittently: about 2 in 8 runs on loopback with no proxy in between.
From reading main, the path looks like this:
WebTransportStream's constructor registers oncontext.StreamContext.ConnectionClosed, and the callback callsWebTransportSession.TryRemoveStream(streamId). The comment there says it "will not trigger if closed only of of the directions of a stream. Stream must be fully ended before this will be called."- An inbound unidirectional stream has only one direction, so it counts as "fully ended" as soon as the peer's FIN is received by the transport, even while that data still sits unread in the pipe.
TryRemoveStreamcallsstream.DisposeAsync().AsTask().GetAwaiter().GetResult().WebTransportStream.DisposeAsyncrunsawait _duplexPipe.Input.CompleteAsync(). That completes the application's reader while the application may still be reading it.
Bidirectional streams don't hit this in practice, because the application's side stays open until it writes and completes.
Files:
src/Servers/Kestrel/Core/src/Internal/WebTransport/WebTransportStream.cs(theConnectionClosedregistration andDisposeAsync)src/Servers/Kestrel/Core/src/Internal/WebTransport/WebTransportSession.cs(TryRemoveStream)
Expected Behavior
Data a peer sent before its FIN on a unidirectional stream can always be read in full by the application. The stream should be cleaned up only after the application has completed its reader, or disposed the stream itself, rather than when the transport sees the stream end.
Steps To Reproduce
-
Linux, libmsquic 2.6.2, .NET SDK
11.0.100-rc.3.26522.148. A Kestrel app withWebTransportAndH3Datagramsenabled,UseQuic(), andHttp1AndHttp2AndHttp3on loopback. -
Handler:
app.Run(async context => { var session = await context.Features.GetRequiredFeature<IHttpWebTransportFeature>() .AcceptAsync(context.RequestAborted); var stream = await session.AcceptStreamAsync(context.RequestAborted); await using (stream!) { using var buffer = new MemoryStream(); await stream!.Transport.Input.CopyToAsync(buffer, context.RequestAborted); // throws intermittently Console.WriteLine(buffer.Length); } }); -
Client (any WebTransport client): open a session, open a unidirectional stream (type
0x54+ session id), write 4 bytes, finish the stream. -
Repeat. Intermittently,
CopyToAsyncthrowsInvalidOperationException: Reading is not allowed after reader was completed.
The same scenario over a bidirectional stream (client writes, then FIN; server reads to the end) passes every time.
Exceptions (if any)
System.InvalidOperationException: Reading is not allowed after reader was completed.
at System.IO.Pipelines.ThrowHelper.ThrowInvalidOperationException_NoReadingAllowed()
at System.IO.Pipelines.Pipe.ReadAsync(CancellationToken token)
at System.IO.Pipelines.PipeReader.CopyToAsync(Stream destination, CancellationToken cancellationToken)
.NET Version
11.0.100-rc.3.26522.148
Anything else?
- Related WebTransport follow-ups: #42788. A separate interop issue: #70384.
- We hit this while proxying WebTransport sessions, where client unidirectional streams are forwarded. The forwarder sees the stream cut off, with no way to tell whether all the data arrived.
- Dominant language
- C#
- Stars
- 38.5k
- Forks
- 11.6k
- Avg merge
- 3d 1h
- Merged PRs (30d)
- 289
Getting set up
Starts the project's dev container in your browser, under your own GitHub account.
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from dotnet/aspnetcore
-
area-networking
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
dotnet/aspnetcore#70545 · 1 comment ·
Maintainers usually reply within 1 day
-
area-infrastructure
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
dotnet/aspnetcore#70524 · 1 comment ·
Maintainers usually reply within 1 day
-
[OpenTelemetry] Hosting replaces an Activity.DisplayName that the app set, when a route matchesOpenarea-hosting Regression
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
dotnet/aspnetcore#70516 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
[SSE] ServerSentEventsResult.WrapEvents is missing [EnumeratorCancellation], so RequestAborted never reaches the app's generator on the IAsyncEnumerable<T> overloadPossibly taken @bharatprajapati-coder claimed this 1 day ago. Openarea-minimal
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
dotnet/aspnetcore#70419 · 2 comments ·
Maintainers usually reply within 1 day
-
[Blazor] DefaultInMemoryCircuitPersistenceProvider never disposes the retention CancellationTokenSource when a persist is rejected at capacityPossibly taken @Vinoth2562000 claimed this 1 day ago. Openarea-blazor
Difficulty 2/5 1-3 hours Newbie friendliness 77/100
dotnet/aspnetcore#70418 · 1 comment · 1 assignee ·
Maintainers usually reply within 1 day
All issues in dotnet/aspnetcore
Similar issues
-
bug emitter:client:csharp
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
microsoft/typespec#12572 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
needs-area-label
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 2 days
-
AWS Lambda: KeyNotFoundException drops HTTP-triggered transactions when allowAllHeaders is disabledOpenbug
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
newrelic/newrelic-dotnet-agent#3949 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 87/100
Maintainers usually reply within 1 day
-
priority:P2
Difficulty 2/5 1-3 hours Newbie friendliness 80/100