Skip to content

http2: stream the request body to the application - #3717

Merged
benoitc merged 13 commits into
masterfrom
fix/h2-stream-body
Aug 30, 2026
Merged

http2: stream the request body to the application#3717
benoitc merged 13 commits into
masterfrom
fix/h2-stream-body

Conversation

@benoitc

@benoitc benoitc commented Aug 29, 2026

Copy link
Copy Markdown
Owner

The whole HTTP/2 body was buffered before dispatch and window credit was returned as frames arrived, so a client could grow a worker without the application being called.

The request is now dispatched on its headers, the application pulls the body off the stream as it reads (wsgi.input or ASGI receive()), and window credit is returned only for what it consumed. A body left unread is cut off with RST_STREAM(NO_ERROR) after the response. Streams on an ASGI connection are served concurrently. Replaces #3716.

Follow-up commits fix the blockers a review of the HTTP/2 support found:

  • a graceful GOAWAY is honoured wherever it lands in a read, through a shared H2Connection subclass instead of forcing h2's state
  • the send-credit wait is bounded by timeout; a peer that stops reading gets RST_STREAM(CANCEL) and the application is stopped, not told the response succeeded
  • an application error after the headers went out resets the stream instead of sending a second HEADERS block that corrupted the HPACK table
  • an empty final body chunk ends the stream
  • gthread/gevent: frames read during a credit wait are handled in order, idle connections close after keepalive, stalled bodies are cancelled after timeout, and streams reset before they are served are dropped
  • ASGI: receive() after the body blocks until the peer goes instead of spinning the loop, a paused reader is resumed, and in-flight streams get the disconnect grace period

Verified with the unit suite, the docker http2 and asgi_compliance suites, h2spec (asgi 146/146; gthread 142, gevent 143 with pre-existing gaps), and live checks per worker.

@benoitc
benoitc force-pushed the fix/h2-stream-body branch 8 times, most recently from 32e1b07 to 9e9de1a Compare August 30, 2026 07:55
The whole body was buffered before dispatch and window credit was
returned as frames arrived, so a client could grow a worker without the
application being called.

Dispatch on headers, let the application pull the body off the stream
as it reads, and return window credit only for what it consumed.
@benoitc
benoitc force-pushed the fix/h2-stream-body branch from 9e9de1a to da7d2e3 Compare August 30, 2026 09:35
A shared H2Connection subclass keeps the state open on GOAWAY(NO_ERROR)
so established streams finish and later ones are refused.
Wait at most cfg.timeout, handle frames read meanwhile in order, reset
the stream with CANCEL on expiry and make the WSGI response raise.
Refuse it at the connection level and reset the stream with
INTERNAL_ERROR instead, so the HPACK table stays intact.
Read with cfg.keepalive between requests, closing with GOAWAY, and
cfg.timeout while a body is pulled, cancelling the stream on expiry.
Remove them on RST_STREAM and never hand out a request whose stream is
closed, so HEADERS+RST_STREAM floods cannot grow the worker.
Queue an empty DATA frame carrying END_STREAM in both connection classes.
receive() waits for the peer to go away instead of returning again at
once, and the receive loop resumes a reader paused under backpressure.
@benoitc
benoitc force-pushed the fix/h2-stream-body branch from d653546 to 4d2f53c Compare August 30, 2026 14:03
…settings doc

The tests took the write lock before the app was dispatched, which
deadlocked on Python 3.10 and 3.11 scheduling; the app now signals
before the lock is taken and is released with a second event.
A docker suite runs the official h2spec image against gthread, gevent
and asgi services, failing on any case outside a per-worker list of
known gaps, with a CI job on HTTP/2 changes.
@benoitc
benoitc merged commit 59ac955 into master Aug 30, 2026
34 of 35 checks passed
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.

1 participant