Skip to content

refactor: split empty data frame count from budget - #945

Merged
seanmonstar merged 1 commit into
masterfrom
sean/stmypoltrkzw
Aug 21, 2026
Merged

refactor: split empty data frame count from budget#945
seanmonstar merged 1 commit into
masterfrom
sean/stmypoltrkzw

Conversation

@seanmonstar

Copy link
Copy Markdown
Member

cc #944

@seanmonstar
seanmonstar merged commit 3850acd into master Aug 21, 2026
6 checks passed
@seanmonstar
seanmonstar deleted the sean/stmypoltrkzw branch August 21, 2026 20:19
Sruhvx-jpg added a commit to Sruhvx-jpg/h2 that referenced this pull request Aug 23, 2026
Problem

HTTP/2 flow control limits DATA payload bytes, but not the framing overhead
from excessive numbers of small frames. A peer could fragment data into many
tiny frames, causing disproportionate memory usage from queued events while
remaining within flow-control windows.

Solution

Backport the framing overhead budget and empty frame handling from master
(hyperium#935, hyperium#940, hyperium#942, hyperium#945, hyperium#946) to the 0.3.x maintenance branch.

Validation

Ran the full test suite and added regression integration tests in stream_states.rs.
lightsofapollo added a commit to gpu-cli/h2 that referenced this pull request Sep 1, 2026
…etime total

MAX_RECV_EMPTY_DATA_FRAMES (introduced in 0.4.19, hyperium#945) counts empty
non-final DATA frames over the LIFETIME of a connection and closes the
connection with GOAWAY ENHANCE_YOUR_CALM at 100. Some legitimate HTTP/2
stacks emit one empty non-final DATA frame per request while ending the
body — Bun's node:http2 does (HEADERS, DATA(payload), DATA(len=0),
DATA(len=0, END_STREAM) on every grpc-js call) — so the 100th call on a
long-lived connection gets the whole connection killed. Empty non-final
DATA frames are legal (RFC 9113 §6.1); the flood this cap defends
against is a run of them carrying no useful traffic.

Count consecutive empty non-final frames PER STREAM instead: the
stream's own non-empty DATA resets its run. Per stream, because
concurrent streams flush their empty frames in batches, so legitimate
empties from different streams arrive back to back and a
connection-wide run counter still trips. A pure empty-frame flood on
any one stream still kills the connection at 100, and total no-op
frames stay bounded by max_concurrent_streams x the cap.

Repro that motivated this (tonic 0.12 server, grpc-js 1.14.1 client
under Bun 1.3.5, one connection): stock 0.4.19 dies at exactly the
100th sequential unary call with "too_many_data_frames"; 200-wide
concurrent bursts die regardless of window sizing. With this change
500 sequential and 3x200 concurrent calls all complete.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2Nkt6B96LEFFSQLteSBXp
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