Repository navigation
waltz: fix h2 and gRPC stream lifecycle and flow control - #11841
Merged
0x0ece merged 1 commit intoOct 2, 2026
Merged
Conversation
┌─ ⚡ PERF · 2531717 vs main@48ae213 ─────────────────────────────────
│ SUITE BASELINE NEW Δ
│ replay tps, mainnet 24,568 tps 24,726 tps · +0.64%
│ bench tps, localnet 760,180 tps 761,652 tps · +0.19%
│ snapshot load, testnet 4.92 s 4.98 s · +1.36%
│ mem total, mainnet 159.96 GiB 159.96 GiB · 0.00%
│ mem total, testnet 124.53 GiB 124.53 GiB · 0.00%
│ mem total, ag mainnet 184.45 GiB 184.45 GiB · 0.00%
│ clean compile, firedancer 3.23 s 3.21 s · -0.40%
│ binary size, firedancer 58.35 MB 58.35 MB · 0.00%
├─────────────────────────────────────────────────────────────────────
@@ 0 REGRESSIONS · 0 WARNINGS · 0 IMPROVED · 8 NOISE @@
└─────────────────────────────────────────────────────────────────────history · 4 pushes ┌─ HISTORY · Δ vs main, per push, newest first ──────────────────────────────────
│ HEAD TPS BENCH SNAP MEM·M MEM·T AG·M COMPILE BINARY
│ 2531717 +0.64% +0.19% +1.36% 0.00% 0.00% 0.00% -0.40% 0.00%
│ 2ac1f79 -0.74% -0.05% +2.41% 0.00% 0.00% 0.00% +1.44% 0.00%
│ 8b42589 +0.09% -0.08% +0.86% 0.00% 0.00% 0.00% +1.68% 0.00%
│ 889871e +0.30% +0.47% +2.94% 0.00% 0.00% 0.00% -0.16% 0.00%
└──────────────────────────────────────────────────────────────────────────────── |
esemeniuc
force-pushed
the
fix/h2-late-closed-stream-headers
branch
3 times, most recently
from
October 1, 2026 16:31
b4f9bf8 to
d7550d8
Compare
esemeniuc
force-pushed
the
fix/h2-late-closed-stream-headers
branch
from
October 1, 2026 17:48
d7550d8 to
a57d5d6
Compare
esemeniuc
force-pushed
the
fix/h2-late-closed-stream-headers
branch
3 times, most recently
from
October 1, 2026 21:14
0559831 to
8b42589
Compare
esemeniuc
marked this pull request as ready for review
October 1, 2026 21:24
Contributor
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The changes are internally consistent and include focused regression coverage for the corrected edge cases.
Review effort: Balanced
Findings: None
What changed in this PR
Fixes HTTP/2 and gRPC stream lifecycle, flow-control accounting, and handshake edge cases.
Changes:
- Corrects handling of late frames, padding, continuations, and HPACK updates.
- Fixes handshake completion and gRPC stream termination/deadline behavior.
- Adds focused regression tests and documents released-stream header handling.
| File | Description |
|---|---|
src/waltz/h2/test_h2_padded_data.c |
Tests padded DATA flow-control accounting. |
src/waltz/h2/test_h2_conn.c |
Tests late frames and handshake completion. |
src/waltz/h2/README.md |
Documents released-stream header validation. |
src/waltz/h2/fd_hpack.c |
Rejects nonzero HPACK table-size updates correctly. |
src/waltz/h2/fd_h2_conn.h |
Corrects the handshake flag mask. |
src/waltz/h2/fd_h2_conn.c |
Fixes late-frame handling and flow control. |
src/waltz/grpc/test_grpc_client.c |
Tests deadlines and stream termination cases. |
src/waltz/grpc/fd_grpc_client.c |
Fixes deadline servicing and early server termination. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
esemeniuc
force-pushed
the
fix/h2-late-closed-stream-headers
branch
from
October 1, 2026 21:55
8b42589 to
2ac1f79
Compare
esemeniuc
force-pushed
the
fix/h2-late-closed-stream-headers
branch
from
October 2, 2026 00:10
2ac1f79 to
bfc9a67
Compare
esemeniuc
force-pushed
the
fix/h2-late-closed-stream-headers
branch
from
October 2, 2026 01:47
1a7d0f8 to
7193e97
Compare
Late response HEADERS after a gRPC header timeout previously disconnected unrelated requests. Drop frames on closed streams while preserving HPACK validation and connection flow-control accounting. Classify stream IDs per initiator: frames on an ID that was never opened are a connection PROTOCOL_ERROR, frames on a used, refused or implicitly closed ID are dropped. Clients reject server-initiated HEADERS unless conn->allow_server_requests is set. Validate discarded and fragmented field blocks incrementally with fd_hpack_skip, without retaining decoded strings, so validation survives a stream release or abort in the middle of a block. Reject nonzero dynamic table size updates in fd_hpack_rd. Count DATA padding against the receive windows, close the stream and notify its owner on stream flow-control errors and zero WINDOW_UPDATE increments, and ignore control frames on closed streams. Reserve TX space before consuming a DATA frame header, including empty frames, and stop receive processing on a connection error. Fix the handshake flag mask and ignore CONTINUATION flags other than END_HEADERS. fd_h2_stream_error sends RST_STREAM only for open or half-closed streams. In fd_grpc_client, queue RST_STREAM before application callbacks can consume TX space, end requests whose END_STREAM arrived before the end of their field block, reset streams the server ends before the client half-closes, and keep deadlines running while a field block is pending. Move fd_h2_tx_rst_stream out of line to keep its stack frame out of the DATA receive path, and let fuzz_h2 and fuzz_h2_actor exercise server-initiated streams. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
esemeniuc
force-pushed
the
fix/h2-late-closed-stream-headers
branch
from
October 2, 2026 04:08
7193e97 to
2531717
Compare
0x0ece
approved these changes
Oct 2, 2026
Contributor
Author
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.


A gRPC header timeout sends RST_STREAM and releases the stream. If the response HEADERS arrive afterwards, fd_h2 treats them as a connection error and drops every other request on the connection. Reviewing that path turned up related bugs in fd_h2 and fd_grpc_client, all fixed here.
fd_h2
Late HEADERS on a closed stream
fd_hpack_skipstill HPACK-decodes it incrementally across HEADERS and CONTINUATION, without keeping the decoded fields, and invalid HPACK is COMPRESSION_ERROR. No HPACK state is needed between blocks because fd_h2 advertises SETTINGS_HEADER_TABLE_SIZE=0. HEADERS on a local ID that was never opened is still PROTOCOL_ERROR.CONTINUATION for a stream released or aborted mid-block (for example, fd_grpc_client rejecting the first fragment, or, since fix 15, a deadline firing between fragments)
fd_h2_stream_errororfd_h2_stream_resetbut keeps it in its map.DATA on a stream that is not open
rst_streamcallback runs. An overrun used to send RST_STREAM only, leaving the stream open and counted. DATA on a stream the app keeps in IDLE state is a connection PROTOCOL_ERROR (before: RST_STREAM(STREAM_CLOSED)).WINDOW_UPDATE and RST_STREAM on closed or never-opened streams
rst_streamagain. Only IDs at or above the higher of the next local and next peer stream IDs counted as never opened.DATA padding
Handshake completion mask
FD_H2_CONN_FLAGS_HANDSHAKINGstayed0xf0when bit 7 became the WINDOW_UPDATE flag, so it included WINDOW_UPDATE and missed CLIENT_INITIAL. A WINDOW_UPDATE pending at the end of the handshake keptconn_establishedfrom firing, which blocked every fd_grpc_client request. A misbehaving server triggers it with one DATA frame before its SETTINGS ACK. When fd_h2 is the server, a conforming client does by sending more than about 19.6 KB of DATA before acknowledging SETTINGS.HPACK table size update with a multi-byte size
fd_hpack_rdconsumed a leading0x3fupdate as one byte and decoded its continuation byte as a header field. For example,0x3f 0x88became:status: 200.0x20) are skipped and any other update is an HPACK error, which is all that is valid once the peer acknowledges SETTINGS_HEADER_TABLE_SIZE=0 (RFC 7541 §6.3).END_STREAM on a CONTINUATION frame, where the flag is undefined
RST_STREAM on an already closed stream
fd_h2_stream_erroralways sent RST_STREAM, so fd_grpc_client sent one on a closed stream when an oversized message or an unparsable header block arrived with END_STREAM after the request was fully sent.Zero-increment WINDOW_UPDATE on an open stream
rst_streamruns once. Later frames are dropped as for any released stream.DATA on a stream ID that was never opened
HEADERS opening a server-initiated stream on a client
stream_create; fd_grpc_client refused it with RST_STREAM(REFUSED_STREAM) and kept the connection.conn->allow_server_requestsrestores the old nonstandard behavior (see the h2 README).fd_grpc_client
END_STREAM on a HEADERS frame continued by CONTINUATION frames
The server ends a request the client has not half-closed (a streaming request, or a body still waiting for flow control or TX space)
rx_endand freed its stream object without closing the HTTP/2 stream, leaking a concurrent stream slot, and ignored the server's later RST_STREAM. Once the leaks reached the server's MAX_CONCURRENT_STREAMS, every new request blocked.fd_h2_rxreserves, so callbacks cannot crowd it out. Timeouts and oversized messages use the same order.Stream deadlines while a connection flag is set
fd_grpc_client_service_streamsreturned early on any connection flag. A server that sent HEADERS without END_HEADERS and then stalled, or a connection WINDOW_UPDATE stuck behind a TX buffer with under 128 bytes free, froze every deadline and stream WINDOW_UPDATE.Other changes
fd_h2_rxstops at a pending GOAWAY, not only once the connection is dead. It also drops the wrapped tail of a DATA chunk if the first callback raised a connection error.fd_h2_tx_rst_streamis no longer inline, which keeps its stack frame and stack protector canary out of the DATA receive path.fuzz_h2_actor, andfuzz_h2on half of its client-side inputs, setallow_server_requests, so the fuzzers still reach server-initiated streams on a client.Notes
rx_hpackfield name and hands discarded field blocks to the app.Testing
Each numbered fix and the first two "Other changes" have a unit test that fails when only that change is reverted. The h2, padded DATA and gRPC unit tests, test_bundle_client and test_event_client pass with gcc, clang and clang ASan+UBSan (test_event_client's existing test-only leaks aside), and firedancer-dev builds with both compilers. fuzz_hpack_rd, fuzz_h2, fuzz_h2_actor, fuzz_grpc_client, fuzz_grpc_actor and fuzz_bundle_client found nothing under ASan+UBSan.
fd_hpack_skipmatchedfd_hpack_rdand an independent RFC 7541 decoder on about 18M inputs, split at every byte boundary. In a microbenchmark of small DATA frames and gRPC messages, the gcc-built receive path runs 3 to 4 more instructions per frame, with no measurable change in cycles.RFC 9113: