Kestrel WebTransport completes a client unidirectional stream's reader at FIN, before the app reads buffered data

Open
#70,386 1 comment 0 reactions 0 assignees View on GitHub

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

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

area-networking feature-kestrel
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:

  1. WebTransportStream's constructor registers on context.StreamContext.ConnectionClosed, and the callback calls WebTransportSession.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."
  2. 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.
  3. TryRemoveStream calls stream.DisposeAsync().AsTask().GetAwaiter().GetResult().
  4. WebTransportStream.DisposeAsync runs await _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 (the ConnectionClosed registration and DisposeAsync)
  • 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
  1. Linux, libmsquic 2.6.2, .NET SDK 11.0.100-rc.3.26522.148. A Kestrel app with WebTransportAndH3Datagrams enabled, UseQuic(), and Http1AndHttp2AndHttp3 on loopback.

  2. 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);
        }
    });
    
  3. Client (any WebTransport client): open a session, open a unidirectional stream (type 0x54 + session id), write 4 bytes, finish the stream.

  4. Repeat. Intermittently, CopyToAsync throws InvalidOperationException: 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

Open in Codespaces

Starts the project's dev container in your browser, under your own GitHub account.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from dotnet/aspnetcore

All issues in dotnet/aspnetcore

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.