Skip to content

Don't collect a stream before its fin has been sent - #2570

Open
kixelated wants to merge 1 commit into
cloudflare:masterfrom
kixelated:fix/zero-length-fin-dropped
Open

Don't collect a stream before its fin has been sent#2570
kixelated wants to merge 1 commit into
cloudflare:masterfrom
kixelated:fix/zero-length-fin-dropped

Conversation

@kixelated

Copy link
Copy Markdown

(AI generated but reviewed)

A fin carries no bytes of its own, so acking the stream data up to the final offset says nothing about whether the peer learned the final size. SendBuf::is_complete() checked only the acked ranges, which meant a zero-length fin written at an already-acked offset reported the send side complete the moment it was written.

That is enough for the caller to collect the stream, and collecting it discards the queued fin before any frame carrying it is built. The peer reads every byte and never sees the end of the stream, while the sender believes it delivered one. stream_send() returns Ok, so nothing surfaces the loss.

Reaching it needs the ack for the stream data to land between the fin being written and the next send(), which is routine for an application that writes its payload and finishes the stream on a later turn of its event loop.

Track the fin ack separately and require it. RESET_STREAM carries the final size in place of a fin, so SendBuf::reset() sets the flag too; without that a reset stream would never be collected.

Fixes #2525

A `fin` carries no bytes of its own, so acking the stream data up to the
final offset says nothing about whether the peer learned the final size.
SendBuf::is_complete() checked only the acked ranges, which meant a
zero-length `fin` written at an already-acked offset reported the send
side complete the moment it was written.

That is enough for the caller to collect the stream, and collecting it
discards the queued `fin` before any frame carrying it is built. The peer
reads every byte and never sees the end of the stream, while the sender
believes it delivered one. stream_send() returns Ok, so nothing surfaces
the loss.

Reaching it needs the ack for the stream data to land between the `fin`
being written and the next send(), which is routine for an application
that writes its payload and finishes the stream on a later turn of its
event loop.

Track the `fin` ack separately and require it. RESET_STREAM carries the
final size in place of a `fin`, so SendBuf::reset() sets the flag too;
without that a reset stream would never be collected.

Fixes cloudflare#2525
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kixelated
kixelated requested a review from a team as a code owner August 5, 2026 21:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

zero-length STREAM FIN can be dropped before it is sent

1 participant