Skip to content

fix(http2): Http2Client applies response-body backpressure to the HTTP/2 stream and uses 4 MiB / 16 MiB receive windows - #2015

Merged
mosuem merged 0 commit into
masterfrom
mosum/http2-6b-http2-client-backpressure-and-windows
Oct 22, 2026
Merged

mosuem merged 0 commit into
masterfrom
mosum/http2-6b-http2-client-backpressure-and-windows

Conversation

@mosuem

@mosuem mosuem commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Note

This PR was generated by an AI coding agent (Jetski) on behalf of @mosuem.

Summary

Http2Client forwarded every DATA frame of a response into the StreamController behind StreamedResponse.stream regardless of whether anyone was reading it. The HTTP/2 stream's subscription was never paused, so the stream kept acknowledging data with WINDOW_UPDATE frames (RFC 9113 Section 5.2) and the server kept sending: a consumer that paused the body — or simply read it slower than the network delivered it — accumulated the entire response in memory. HTTP/2 flow control exists precisely so that a receiver can stop granting credit when it is not consuming.

Separately, the client used the protocol's default 65535-byte windows, which cap a connection at 64 KiB per round trip no matter how many streams it carries.

Changes

  • lib/src/http2_client.dart
    • bodyController now forwards onPause/onResume to the HTTP/2 stream's subscription. The stream's incoming queue then stops dispatching (StreamMessageQueueIn._tryDispatch only dispatches to a non-paused listener, and it is dispatching that calls windowHandler.dataProcessed, i.e. sends WINDOW_UPDATE), so once the data the server was already allowed to send has arrived it must wait. Memory held by a paused body is bounded by the stream window; across all of a connection's paused streams by the connection window.
    • _dial configures ClientSettings(streamWindowSize: 4 MiB, connectionWindowSize: 16 MiB). The two changes belong together: with backpressure and the 64 KiB connection window, one paused body would exhaust the connection's credit and stall every other stream on it (the connection queue stops handing data to a buffering stream and only replenishes connection credit for data it handed over).

Not changed, as candidates for follow-ups: a body that has not been listened to yet is still buffered without bound (pausing the HTTP/2 subscription until the first listener would delay onDone, and with it the pool slot's release, for callers that never read the body — a behavior change worth its own discussion); and whether the window sizes should be Http2Client constructor parameters rather than constants.

Test Verification (Fails Before $\rightarrow$ Passes After)

a-paused-response-body-stops-granting-flow-control-credit in test/http2_client_test.dart, against a frame-level TLS server (FrameReader/FrameWriter on the accepted socket), with no sleeps:

  1. Asserts the client's SETTINGS advertise a 4 MiB stream window and its stream-0 WINDOW_UPDATE raises the connection window to 16 MiB.
  2. Sends the response HEADERS, pauses the body subscription, then sends exactly the stream window (4 MiB) of DATA followed by a PING. The client answers the PING only after processing every frame before it, so any stream WINDOW_UPDATE arriving before the PING ACK was granted while paused.
  3. Resumes the body and waits for the full window to be granted back, then sends the final DATA with END_STREAM and checks the body is complete (4 MiB + 3 bytes).

Before fix (test on the previous library code — fails at step 1):

00:00 +0 -1: http2-client-test a-paused-response-body-stops-granting-flow-control-credit [E]
  Expected: <4194304>
    Actual: <null>

With only the window sizes applied (no onPause/onResume forwarding) it fails at step 2, at the first half-window acknowledgement:

00:00 +0 -1: http2-client-test a-paused-response-body-stops-granting-flow-control-credit [E]
  The client granted 2097152 bytes of credit while the response body was paused.

After fix:

00:00 +1: All tests passed!

Full package:http2 suite at this point of the stack (on master): +268 ~7: All tests passed!

@mosuem
mosuem added this pull request to stack #2016 October 8, 2026 10:42
@mosuem
mosuem force-pushed the mosum/http2-6-http2-client-pool-and-flow-control branch from 3b80204 to faab4e8 Compare October 8, 2026 12:04
@mosuem
mosuem force-pushed the mosum/http2-6b-http2-client-backpressure-and-windows branch from 3cd4fe8 to 3695462 Compare October 8, 2026 12:04
@mosuem
mosuem force-pushed the mosum/http2-6-http2-client-pool-and-flow-control branch from faab4e8 to b4bd763 Compare October 9, 2026 07:54
@mosuem
mosuem force-pushed the mosum/http2-6b-http2-client-backpressure-and-windows branch from 3695462 to 37e1b65 Compare October 9, 2026 07:54
@mosuem
mosuem removed this pull request from stack #2016 October 9, 2026 07:55
@mosuem
mosuem added this pull request to stack #2020 October 9, 2026 07:55
@mosuem
mosuem force-pushed the mosum/http2-6-http2-client-pool-and-flow-control branch from b4bd763 to c157584 Compare October 9, 2026 08:18
@mosuem
mosuem force-pushed the mosum/http2-6b-http2-client-backpressure-and-windows branch from 37e1b65 to 24dacab Compare October 9, 2026 08:18
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

PR Health ⚠️

Health checks are rerunning. These details are out-of-date.

@demolaf

demolaf commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

@mosuem thanks for this PR. The 4 MiB / 16 MiB window change in _dial fixes a ~2× slowdown our users are hitting in production (firebase/firebase-admin-dart#333).

Since this PR sits on top of the stack, could the window change land separately against master ahead of the rest? It only needs #1999, which is already merged. A 3.2.0 release with it would let us ship the fix.

@mosuem
mosuem force-pushed the mosum/http2-6b-http2-client-backpressure-and-windows branch from ed41d40 to 1d29611 Compare October 22, 2026 06:40
@github-actions github-actions Bot added package:cupertino_http Issues related to package:cupertino_http package:http type-infra A repository infrastructure change or enhancement package:http_parser package:http_multi_server labels Oct 22, 2026
@mosuem
mosuem removed this pull request from stack #2020 October 22, 2026 06:44
@mosuem
mosuem merged commit 1d29611 into master Oct 22, 2026
7 checks passed
@mosuem
mosuem force-pushed the mosum/http2-6-http2-client-pool-and-flow-control branch from 9174e50 to c038a93 Compare October 22, 2026 06:44
@mosuem
mosuem deleted the mosum/http2-6b-http2-client-backpressure-and-windows branch October 22, 2026 06:44
@mosuem
mosuem changed the base branch from mosum/http2-6-http2-client-pool-and-flow-control to master October 22, 2026 06:44
@mosuem
mosuem restored the mosum/http2-6b-http2-client-backpressure-and-windows branch October 22, 2026 06:45
@mosuem
mosuem deleted the mosum/http2-6b-http2-client-backpressure-and-windows branch October 23, 2026 05:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package:cupertino_http Issues related to package:cupertino_http package:http_multi_server package:http_parser package:http package:http2 type-infra A repository infrastructure change or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants