Block HTTP/2 request-body sends on flow control#911
Merged
Conversation
An HTTP/2 request body larger than the peer's flow control window failed
with {error, send_buffer_full}: body sends used the non-blocking
h2_connection:send_data/4, which buffers up to a cap instead of waiting
for WINDOW_UPDATE. Request-body sends now use send_data/5 with
#{block => Timeout}, parking the caller until the window opens. The new
send_timeout option bounds the wait (default 30000 ms, infinity allowed,
nonblock restores the old fail-fast behavior). Covers the whole-body
send, streamed chunks, the end-stream frame and body producer funs.
HTTP/1.1 and HTTP/3 paths are unchanged.
Tested against a frame-level h2 server with a small initial window and
delayed, split or withheld WINDOW_UPDATEs, plus a digest echo server for
a body over the old 1 MB cap.
Add flow-control tests for body-producer funs (arity 0 and {fun, State}),
a streamed chunk hitting send_timeout, and the async request path with
send_timeout set. Every changed line of the blocking-send fix is now
exercised.
A failed body send now RST_STREAM(CANCEL)s the abandoned stream from h2_send_data, so its buffered body no longer lingers on a shared connection. This also covers the nonblock send_buffer_full case. send_timeout no longer leaks between pooled requests: the per-request value is passed along instead of stored, and the connection default is no longer seeded from the first checkout's options. Streamed requests now forward send_timeout through send_request_headers/5 and keep it in a per-stream field set at every stream start, so a reused pooled connection applies the caller's deadline. The new async coverage test exposed a pre-existing bug: the h2 stream entry stored the gen_statem call reference where the async delivery clauses expect the stream_to pid, so every async HTTP/2 response was silently dropped. The entry now stores the stream owner. Regression tests: RST_STREAM observed on the wire after a timed-out send, a 25 ms send_timeout followed by a default request on the same pooled conn, a streamed request with send_timeout on a reused conn, and the direct hackney_conn async API forms.
HTTP/2 {async, once} now honors stream_next/1 with the same contract as
HTTP/1.1, backed by h2 manual flow control so an unpulled response keeps
the peer's send window closed. Direct connections honor {send_timeout, T}
from connect options again (pooled ones keep the constant default). Fix
the unmatched return that tripped dialyzer.
benoitc
force-pushed
the
fix/h2-blocking-send-flow-control
branch
from
July 16, 2026 22:19
81506d1 to
4589443
Compare
notify => self() leaked late {h2_raw_server, ...} messages into the
shared eunit process, where later suites' catch-all receives consumed
them as unexpected responses. Notifications now go to a per-test
collector process that dies with the test, so nothing reaches other
tests' mailboxes.
2 tasks
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.
An HTTP/2 request body larger than the peer's flow control window failed with
{error, send_buffer_full}: body sends used the non-blockingh2_connection:send_data/4, which buffers up to a 1 MB cap instead of waiting for WINDOW_UPDATE. Request-body sends now usesend_data/5with#{block => Timeout}, parking the caller until the window opens while the h2 connection keeps processing inbound frames.The new
send_timeoutoption bounds the wait: default 30000 ms,infinityallowed,nonblockrestores the old fail-fast behavior. It works per-request and as a connection option, mirroringrecv_timeout. Covers the whole-body send, streamed chunks, the end-stream frame and body producer funs. HTTP/1.1 and HTTP/3 paths are unchanged, and no h2 bump is needed (0.11.0 already ships the blocking form).Tests extend the frame-level repro server with a
window_updateknob (delayed, split or withheld WINDOW_UPDATEs) and add a flow-control suite: a large body over a small window completes, a never-opening window yields{error, timeout}in bounded time instead of hanging,nonblockstill fails fast, and a 1.5 MB body (over the old cap) is delivered byte-for-byte, verified by digest.