Skip to content

fix(h2): apply server preface WINDOW_UPDATE to the connection send window - #69

Merged
FumingPower3925 merged 1 commit into
mainfrom
fix/h2-preface-window-update
Jul 3, 2026
Merged

FumingPower3925 merged 1 commit into
mainfrom
fix/h2-preface-window-update

Conversation

@FumingPower3925

Copy link
Copy Markdown
Contributor

Problem

aspnet-h2 post-4k-h2 measured 0 rps in the v1.5.x benchmarks (suspect), while its get-json-h2 worked and every other framework's post-4k-h2 worked. Root cause is a loadgen h2 flow-control bug, not ASP.NET.

Root cause

The h2 handshake's SETTINGS-ACK wait loop discarded every non-ACK frame — including a connection-level WINDOW_UPDATE(0, …) the server sends in its preface to grow the connection window above the RFC 7540 §6.9.2 floor of 65535. Dropping it strands connSendWindow at 65535, which deadlocks sustained request-body sends after ~65535 bytes (~16× 4 KB/conn). Kestrel sends this WINDOW_UPDATE before its SETTINGS ack (caught + discarded); servers that send it after were unaffected.

Fix

Capture connection-level WINDOW_UPDATE grants during the handshake and seed connSendWindow with them. Purely additive — servers without a preface grant are unchanged.

Verification (before → after)

unfixed fixed
macOS, h2c POST 4 KB vs Kestrel 60 then 0 1,115,283 req, 0 err
multipass Ubuntu VM (Linux) 60 then 0 672,985 req, 0 err

h2c GET and go test h2 suite unaffected (regression-checked).

…ndow

The H2 handshake's SETTINGS-ACK wait loop discarded every non-ACK frame,
including a connection-level WINDOW_UPDATE(0, ...) the server may send in its
preface to grow the connection flow-control window above the RFC 7540 §6.9.2
floor of 65535. Dropping it stranded connSendWindow at 65535, deadlocking
sustained request-body sends (post-4k-h2 / post-64k-h2) after ~65535 bytes:
the server only replenishes against its larger configured window (e.g.
Kestrel's 1 MiB InitialConnectionWindowSize), a threshold the stranded client
never reaches.

ASP.NET/Kestrel sends this WINDOW_UPDATE in its preface (before its SETTINGS
ack), so aspnet-h2 post-4k measured 0 rps in v1.5.x; servers that send it
after the ack were unaffected (readLoop applied it).

Capture connection-level WINDOW_UPDATE grants during the handshake and seed
connSendWindow with them. Additive + safe: servers without a preface grant are
unchanged. Verified macOS + Linux/multipass: aspnet-h2 POST-4k 0 -> sustained
(1.1M / 673k req, 0 errors); h2c GET + h2 test suite unaffected.
@FumingPower3925

Copy link
Copy Markdown
Contributor Author

Regression sweep — every h2c server, macOS + Linux (multipass)

Tested the fixed vs unfixed loadgen against every h2c server we support, on both macOS and an Ubuntu/multipass VM. POST = /upload 4 KB over h2c; all runs -h2-conns 4 -h2-streams 10. Zero errors everywhere (except a small, equal fixed-vs-unfixed count on fastapi — pre-existing, not this change). Throughput is noisy on macOS (13 servers ran concurrently) — judged on errors/stall, not rps; the VM runs were sequential and show fixed≈unfixed.

macOS (all stacks)

server stack unfixed POST req fixed POST req result
stdhttp-h2 Go net/http 85,263 80,086 no regression
gin-h2 Go net/http 59,766 58,623 no regression
echo-h2 Go net/http 104,440 77,953 no regression
chi-h2 Go net/http 184,786 79,928 no regression
iris-h2 Go net/http 78,440 68,576 no regression
hertz-h2 Go netpoll 177,355 72,193 no regression
axum-h2 Rust/hyper 276,006 530,428 no regression
hyper-h2 Rust/hyper 466,758 674,465 no regression
fastapi-h2 Python 25,081 (168 err) 21,599 (191 err) no regression (errors pre-existing & equal)
aspnet-h2 .NET Kestrel 0 (stall) 414,968 FIXED
hono-h2 Bun 0 (stall) 110,534 FIXED (bonus)
elysia-h2 Bun 0 (stall) 101,095 FIXED (bonus)
celeris-auto+upg Go/io_uring — — n/a on macOS (io_uring is Linux-only) — covered in VM below

Linux (multipass Ubuntu VM)

server unfixed POST req fixed POST req result
stdhttp-h2 311,709 316,562 no regression
gin-h2 200,983 197,068 no regression
echo-h2 313,969 275,597 no regression
chi-h2 313,255 311,749 no regression
iris-h2 240,888 246,631 no regression
hertz-h2 299,097 276,645 no regression
celeris (h2c-upgrade) 1,053,446 1,226,236 no regression
aspnet-h2 0 (stall) 425,228 FIXED

Conclusion: the fix is purely additive — servers that don't send a preface connection WINDOW_UPDATE are byte-for-byte unaffected; the three that do (Kestrel + both Bun runtimes) went from a hard POST stall (0 req) to fully sustained. No regression on any h2c server, on either OS. go vet clean and the loadgen h2 go test suite passes.

@FumingPower3925
FumingPower3925 merged commit aad6d6b into main Jul 3, 2026
3 checks passed
@FumingPower3925
FumingPower3925 deleted the fix/h2-preface-window-update branch September 5, 2026 22:32
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