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

Aperta
#70,386 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
55/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
csharp

Direzione di ricerca

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.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

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.
Lingua principale
C#
Stelle
38.5k
Fork
11.6k
Merge medio
3g 1h
PR unite (30g)
289

Preparare l'ambiente

Apri in Codespaces

Avvia il container di sviluppo del progetto nel browser, con il tuo account GitHub.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di dotnet/aspnetcore

Tutte le issue di dotnet/aspnetcore

Issue simili

Altre issue su C#

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.