Skip to content

http2: port upstream inbound flow control fixes to stop window leaks - #2

Open
hiroTamada wants to merge 3 commits into
masterfrom
hypeship/h2-body-close-flow-refund
Open

http2: port upstream inbound flow control fixes to stop window leaks#2
hiroTamada wants to merge 3 commits into
masterfrom
hypeship/h2-body-close-flow-refund

Conversation

@hiroTamada

@hiroTamada hiroTamada commented Aug 29, 2026

Copy link
Copy Markdown

Summary

Ports two golang.org/x/net inbound flow-control fixes the fork predates. Together they stop pooled HTTP/2 connections from strangling to zero window when response bodies are closed mid-download — the failure mode where every later stream on a connection hangs with zero bytes until the origin resets it after ~5 minutes.

What was wrong

Two independent leaks of connection-level flow control credit, both triggered by aborting response bodies:

  1. Read-credit stranding (deterministic). The Read path refreshed the connection window only once available dropped below half of cc.connFlow, and Close() refunds only unread bytes. Bytes that were read on a stream that then closed before the half-window threshold was crossed were never refunded. Successive aborted downloads accumulate the deficit; once the window hits zero no read can ever happen again, so no refresh can ever fire — a terminal wedge. The refresh ceiling was also cc.connFlow while the advertised window is cc.connFlow + 65535, permanently stranding the protocol-default slack.
  2. Close/data race (probabilistic). processData checked cs.didReset under cc.mu but wrote to the body pipe after releasing it, while Close() refunded buffered bytes and only then broke the pipe. Data written in between was silently discarded by the broken pipe (pipe.Write succeeded on a broken pipe) and its credit was never returned.

What this ports

Not ported: the server-side half of the inflow rewrite (out of scope; the server keeps the old flow accounting plus the 9f24bb44 refund fix).

Testing

  • 新建 TestTransportBodyCloseRaceRefundsConnFlow: drives 200 aborted-mid-flood downloads over one pooled connection against a frame-level fake server that strictly obeys the client's advertised windows, then asserts a full-body canary request completes. Wedges on master (canary starves with zero bytes, window ledger decays to 0) and passes with this change, including -race -count=3.
  • TestTransportSlowReaderLargeResponse (the existing large-download regression test) still passes — the inflow accounting also covers the bug it guards.
  • Full ./http2 suite compared against master: identical failure set (20 pre-existing failures, e.g. TestHeaderOrder3, TestTransportH2c, and TestTransport hangs on both); no new failures. Suite run with -vet=off because pre-existing vet errors in fhttp_test.go block the test build.

Behavior notes for review

  • WINDOW_UPDATE cadence changes: updates are now batched at ≥4 KiB granularity (or window-doubling) instead of refresh-to-full below half-window. SETTINGS values, the connection-preface WINDOW_UPDATE increment, and header/pseudo-header ordering — the fingerprint-relevant surface — are untouched.
  • pipe.Write on a broken pipe now returns errClosedPipeWrite instead of silently discarding; both transport and server callers handle it by refunding.

Release

This branch is based on the v0.6.8-kernel.1 lineage (master + kernel patches + module rename), so merging is a maintainer tagging v0.6.8-kernel.2 from it and bumping github.com/kernel/fhttp in consumers.


Note

Medium Risk
HTTP/2 transport flow-control and body-close ordering changed in core connection pooling paths; module rename breaks consumers until they bump imports. SETTINGS/fingerprint surface is unchanged per PR notes.

Overview
Renames the Go module from github.com/bogdanfinn/fhttp to github.com/kernel/fhttp and updates imports, go:generate bundle paths, and tests accordingly.

The functional work ports upstream golang.org/x/net HTTP/2 inbound flow-control fixes so pooled connections no longer wedge when response bodies are aborted mid-transfer:

  • Introduces an inflow type that credits every consumed byte and batches WINDOW_UPDATE frames (≥4 KiB or window-doubling), replacing half-window refresh and the fork’s adaptive small-window stream logic on the client transport.
  • transportResponseBody.Close breaks the body pipe before refunding unread credit; pipe.Write after a break returns errClosedPipeWrite instead of silently accepting data.
  • processData enforces connection + stream windows with takeInflows, refunds credit when DATA loses the race with a closed body, and the server stops treating handler-closed request bodies as a stream error while returning connection-level flow for discarded DATA.

Adds TestTransportSlowReaderLargeResponse and TestTransportBodyCloseRaceRefundsConnFlow to guard WINDOW_UPDATE desync and connection-credit leaks.

Reviewed by Cursor Bugbot for commit 99252f9. Bugbot is set up for automated code reviews on this repo. Configure here.

Rafael Garcia and others added 3 commits August 8, 2026 12:32
transportResponseBody.Read computed the stream receive-window refresh as

    unsent = streamFlow - available + bufPipe.Len()

bufPipe.Len() is body data received but not yet consumed by the
application. The amount that is safe to return to the peer is
streamFlow - available - buffered (what golang.org/x/net/http2 computes),
so the buffered term must be subtracted. Adding it over-credits the
stream window by 2x buffered on every refresh.

With a slow reader (proxy relaying to a backpressured client, rate-
limited download), the receive buffer stays large, the advertised stream
window desyncs from the real connection accounting, and a large download
dies partway with:

    stream error: stream ID N; FLOW_CONTROL_ERROR

Add a regression test that downloads 48 MiB over an in-process server
through a Transport configured with a Chrome-like 6 MiB stream window
while consuming the body at ~12 MiB/s. It fails on master with
FLOW_CONTROL_ERROR after ~46 MiB and passes with this fix.

Refs bogdanfinn/tls-client#257
So Kernel services can pin this fork via a go.mod replace while the
upstream WINDOW_UPDATE fix (bogdanfinn#24) is pending review.
Import-path-only change; no code changes beyond the preceding fix.
Port two golang.org/x/net fixes the fork predates:

- 7805fdc3 "http2: rewrite inbound flow control tracking" (client side):
  replace the threshold-based WINDOW_UPDATE refresh with the inflow
  accounting type. The old Read path refreshed the connection window
  only once available dropped below half of cc.connFlow, and Close
  refunded only unread bytes, so bytes read from a body that was then
  closed were never returned to the peer. Aborted downloads accumulate
  the stranded deficit until the connection window reaches zero, at
  which point every later stream on the pooled connection starves with
  zero bytes: no read can happen, so no refresh can ever fire again.
  The refresh ceiling was also cc.connFlow while the advertised window
  is cc.connFlow+65535, permanently stranding the protocol-default
  slack. inflow credits every consumed byte exactly once and batches
  wire updates (>=4KiB, or enough to at least double the peer's
  remaining window).

- 9f24bb44 "http2: properly discard data received after request/response
  body is closed": break the body pipe before refunding unread credit
  (not after), fail pipe writes after a break instead of silently
  discarding, and refund connection-level credit for DATA that loses
  the race with Close. Also the server-side handler-closed-body refund.

TestTransportBodyCloseRaceRefundsConnFlow drives 200 aborted-mid-flood
downloads against a frame-level server that strictly obeys the client's
advertised windows, then asserts a full-body canary request still
completes. It wedges on master (canary starves with zero bytes) and
passes with this change.
@hiroTamada
hiroTamada marked this pull request as ready for review August 29, 2026 22:49
@hiroTamada

Copy link
Copy Markdown
Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 99252f9. Configure here.

@hiroTamada
hiroTamada requested a review from rgarcia August 29, 2026 23:24
注册 for free to join this conversation on GitHub. Already have an account? 登录 to comment

标签

None yet

项目

None yet

Development

Successfully merging this pull request may close these issues.

x/net/http2: TestTransportReturnsUnusedFlowControlMultipleWrites failures x/net/http2: flow control desync when receiving data for canceled stream

1 participant