fix(http2): do not reserve capacity before body data is available - #4061
Merged
Conversation
PipeToSendStream used to call `reserve_capacity(1)` at the top of every poll iteration as a probe, before asking the body for the next chunk. The reservation is immediately assigned from the connection-level flow-control window, and while the stream is parked waiting for more body data the reservation keeps the last byte of the connection window pinned to the stream. Against peers that only emit a WINDOW_UPDATE once their receive window is fully exhausted (for example Bun's built-in HTTP/2 server, as well as other implementations with a similar strategy), this one-byte reservation is enough to deadlock a second concurrent stream: the connection window never drops to zero on the peer, so no WINDOW_UPDATE is ever sent, and the second stream can never get any capacity. Restructure the loop so it polls the body for the next frame first and only reserves capacity equal to the chunk's exact size. The polled chunk is stashed in a new `buffered_data` field so it survives the `poll_capacity` wait across `Poll::Pending` returns without being dropped. Zero-length data frames are forwarded immediately without touching the reservation. `poll_reset` is now registered at the top of every iteration so RST_STREAM still wakes the task while it waits for either more body data or more capacity. Add a regression test that pairs a streaming request filling the connection window with a second one-byte request, talking to a raw h2::server that never releases recv capacity, and asserts the second request reaches the server. Closes #4003
seanmonstar
force-pushed
the
sean/omooyxqukpvl
branch
from
May 4, 2026 20:41
d6195d8 to
e980da5
Compare
sandersaares
added a commit
to sandersaares/hyper
that referenced
this pull request
Aug 4, 2026
…eration `PipeToSendStream::poll` calls `SendStream::poll_reset` as the first statement of its send loop. Every one of those calls clones the current waker into h2's one-shot `send_task` slot and drops the waker stored there before, and every call takes h2's connection-wide mutex. The loop runs more often than it used to: since hyperium#4061 the body frame is polled first and stashed in `buffered_data`, so a single chunk drives an extra iteration. Measured against a server that grants send capacity on the third poll, one 10240-byte body chunk now costs four `poll_reset` calls where 1.9.0 cost one, taking waker registrations per request from three to six. Runtimes with a more expensive waker than tokio's pay that directly; a load client on such a runtime regressed about 20% in CPU time per request across the 1.9.0 -> 1.10.1 bump, with no change in syscall or allocation counts. Most of those registrations were redundant. h2 registers on the same one-shot slot from `poll_capacity`, and reports a reset there as `Ready(None)`, so a pipe parked on send capacity is already reachable by a RST_STREAM. Waiting on the body is the only suspension point h2 knows nothing about. So the reset check is now hoisted out of the loop and uses the non-registering `SendStream::reset_reason`, while `poll_reset` is called only when `poll_frame` actually returns `Pending`. Because `ready!` on `poll_capacity` returns from `poll` rather than from the inner loop, a reset observed while parked on capacity still re-enters at the hoisted check and still surfaces the peer's `Reason`, not the generic "send stream capacity unexpectedly closed" message. This takes the same workload to three reset checks and two waker registrations per request.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Continuing #4051
Closes #4003
Closes #4051