Repository navigation
Conversation
…ad of resetting or re-opening it
WalkthroughChangesHTTP/2 closed stream handling
Possibly related PRs
Suggested reviewers: Mergeability Score: 🔵 Low · up to The PR changes closed-stream HTTP/2 HEADERS handling to discard late blocks while preserving connection state; the remaining merge-readiness issue is confined to the test helper, where a connection failure can leave the server listening and cause a timeout instead of reporting the original error. This is mergeable with explicit follow-up to close the server on that failure path. 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/js/node/http2/h2-conformance.test.ts`:
- Around line 1303-1320: Update recordingSession to close the server when
RawH2.connect rejects, while preserving the successful return path. Wrap the
connection attempt in failure handling that awaits server.close() before
rethrowing the original error, ensuring the listening handle is released.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 04efe4f8-6c52-45b4-8fc2-e84586321919
📒 Files selected for processing (2)
src/runtime/api/bun/h2/connection.rstest/js/node/http2/h2-conformance.test.ts
|
Updated 10:56 AM PT - Aug 13th, 2026
✅ @alii, your commit 18c2e930f75e385e809a0aca973a273cdd3234c9 passed in 🧪 To try this PR locally: bunx bun-pr 37985That installs a local version of the PR into your bun-37985 --bun |
…he previous one RFC 9113 5.1.1 numbers every new promised stream above all earlier ones. The client only caught a reused or lower id while the earlier promised stream still had an engine entry; once that stream closed and was evicted the promise was accepted and delivered again. Track the highest promised id (the peer high-water mark, declared as in #37985 so the two merge cleanly in either order) and fail the session like nghttp2 does.
|
@alii #44337 (the stream limit, enforced in #44337 refuses a stream over the limit before it makes a stream entry. So a later HEADERS or DATA frame on that id has nothing to find. To give such a frame no second answer, #44337 keeps a sorted list of the refused ids ( This branch conflicts with
|
Problem
node:http2server, Rust h2 engine (src/runtime/api/bun/h2/connection.rs). When a client sends HEADERS on a stream id that is already closed, bun did one of two wrong things depending on timing:RST_STREAM(STREAM_CLOSED). RFC 9113 §5.1 says an endpoint "MUST NOT send frames other than PRIORITY on a closed stream"; the RST reply was RFC 7540's rule, dropped in 9113.streams, sohandle_headerssaw!streams.contains_key(id)and treated it as a brand-new request: firedon_stream_open, delivered it to JS as a'stream'event, and answered 200. Same for a never-used lower id (HEADERS(3) after stream 5 was opened), which §5.1.1 says is implicitly closed. That is stream-id reuse being accepted.Measured against node v22.20.0 and v26.4.0 (nghttp2 1.69.0) with a raw-socket probe: in every one of these cases node decodes the block (HPACK stays in sync), sends nothing, surfaces nothing to JS, and keeps the connection up — nghttp2's
NGHTTP2_ERR_IGN_HEADER_BLOCKpath insession_on_request_headers_received.'stream'id 1 to JS, 200 sent'stream'id 1 to JS, 200 sent'stream'id 3 to JS, 200 sentFix
last_peer_stream_id: the highest client-initiated stream id an inbound HEADERS has opened (refused streams included; on a client, the highest promised id) — nghttp2'slast_recv_stream_id. Inhandle_headers, a server-side id with nostreamsentry at or below that mark is a closed stream, not a new one: no entry is created, nothing is opened/refused/counted, and the block getsBlockDisposition::StreamClosed.BlockDisposition::StreamClosed(also reached when an existing entry isState::Closed) now means §5.1 minimal processing:finish_header_blockstill HPACK-decodes the whole block, then returns without sending a frame or calling the sink. HEADERS on a half-closed (remote) stream still escalates to a connection errorSTREAM_CLOSED, as nghttp2 does.last_stream_id(also raised by the engine's ownsend_header_block/send_push_promise) is left as-is for GOAWAY and the RST_STREAM idle check; a separate peer-only mark is what keeps a server that has promised even ids from misreading a lower odd id as closed. Under today's embedder, which does not send through those engine APIs, the two marks hold the same value on a server.maxSessionMemory), trailers, DATA/RST_STREAM/WINDOW_UPDATE on closed streams: unchanged.Tests
test/js/node/http2/h2-conformance.test.ts,describe("inbound stream lifecycle"), raw-socket client against a bun server. Each closed-stream test sends the late block as HEADERS + CONTINUATION whose CONTINUATION insertsx-bun-sync: 1into the HPACK dynamic table, then opens a fresh stream referencing index 62 and uses a PING ack as the barrier. Asserts: nothing sent on the closed id, no GOAWAY, the probe stream is answered (so the discarded block was decoded), and only the probe stream reached JS.The first four fail on main (
bun-debugbuilt from 8a1cd8d) and pass with this change; the fifth passes on both today and guards the peer-only mark.Verification
Debug build on macOS arm64:
bun bd test test/js/node/http2/h2-conformance.test.ts: 66 pass.bun bd test test/js/node/http2/: 468 pass, 6 skip, 1 fail — the failure isnode-http2-upgrade.test.mts"tests should run on node.js" (spawns the localnode --teston an.mtsfile) and fails identically on main.test/js/node/test/parallel/test-http2-*and 5test/sequential/test-http2-*exit 0.cargo clippy -p bun_runtime -- -D warningsclean.Not in this PR (found while probing)
When RST_STREAM(1) arrives in the same read as the HEADERS that opened stream 1, bun still writes the response HEADERS/DATA for stream 1 that the JS
'stream'handler produced; node writes nothing. The handler runs synchronously fromon_headers_completeandrespond()serializes straight into the corked write buffer (h2_frame_parser.rsrequest/write_stream) before the RST later in the batch is parsed, andon_stream_resetcannot retract already-serialized bytes;requestalso lacks the closed-stream guardwrite_streamhas. Separate change.