Repository navigation
Conversation
The inbound engine wrote Connection::last_stream_id into the GOAWAY it sends on a locally detected connection error. That mark also counts locally initiated streams (a client's own requests, once their response arrives), so a client's GOAWAY could name one of its own odd ids, which RFC 9113 6.8 forbids and nghttp2 rejects as a connection PROTOCOL_ERROR. Connection now tracks last_peer_stream_id (request ids on a server, refused ones included; promised ids on a client), uses it for its own GOAWAYs, and pushes it to the embedder through a new Sink callback. The embedder's copy backs session.goaway()/close()/destroy(), the rejection budget GOAWAYs and state.lastProcStreamID, so all of them now report the same value node does (previously the copy never saw client pushes and the budget GOAWAYs used the mixed mark).
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
WalkthroughThe HTTP/2 connection engine now tracks processed peer streams separately from all streams. The frame parser uses that state to select GOAWAY last-stream IDs, enforce non-increasing IDs, and report the selected ID in callbacks and current state. New tests cover these behaviors. ChangesHTTP/2 GOAWAY stream tracking
Possibly related PRs
Suggested reviewers: Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to No actionable GOAWAY issue remains identified; the change is mergeable after normal checks. Pre-merge checks |
|
|
Status: reproduced on bun 1.4.0 with a raw |
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Because it changes wire-level HTTP/2 GOAWAY semantics in the native engine and overlaps with two other in-flight PRs (#37563, #36343) that add the same last_peer_stream_id field, a human look would still be worthwhile to sequence the merges.
What was reviewed:
note_peer_streamadvances both marks and pushes to the Sink before the stream is surfaced, so callback-triggered GOAWAYs see the updated value; ordering looks correct.- The remaining
self.last_stream_id.get()calls inh2_frame_parser.rsare either in the dead legacy inbound parser (per the file header) or in paths where the value is informational (on_too_many_invalid_frames, unsolicited-PING-ACK) and the actual GOAWAY comes fromdestroy()→goaway()which already readslast_peer_stream_id. - The seven wire-level tests await concrete frame conditions (no sleeps), use
port: 0, wire error handlers, and were verified against node v26.3.0 per the description.
Extended reasoning...
Overview
The PR adds Connection::last_peer_stream_id to the HTTP/2 engine (src/runtime/api/bun/h2/connection.rs), tracking the highest peer-initiated stream id (request ids on a server, promised ids on a client) separately from the existing last_stream_id which also counts locally-initiated streams. local_connection_error now writes the peer mark into every engine-generated GOAWAY, and a new Sink::on_last_peer_stream_id callback keeps the embedder's mirror (H2FrameParser.last_peer_stream_id) in sync so its own GOAWAYs (session.goaway()/close()/destroy(), budget-triggered ones, state.lastProcStreamID) carry the same value. Eight call sites in h2_frame_parser.rs are switched from last_stream_id to last_peer_stream_id; the parity-based advance in handle_received_stream_id is removed in favor of the engine callback. Four Rust #[test]s and seven wire-level TS tests are added.
Security risks
None identified. The change only affects which stream id is written into an outbound GOAWAY frame's last-stream-id field — a 4-byte informational value the peer uses to decide which requests to retry. There is no new parsing of untrusted input, no allocation sizing driven by peer data, and no auth/crypto surface. The refused-stream path already advanced last_stream_id for §5.1 checks; adding the peer-mark advance there is monotonic and bounded by the u32 stream id space.
Level of scrutiny
High. This is protocol-critical native code on the HTTP/2 wire path used by both client and server sessions. RFC 9113 §6.8 conformance affects interop with nghttp2-based peers (which reject a GOAWAY naming the sender's own stream with a connection PROTOCOL_ERROR). While the change itself is narrow and mechanically clear (one new field, one new callback, a handful of last_stream_id → last_peer_stream_id swaps), a wrong value here silently breaks graceful shutdown against real servers. The PR description is unusually thorough — every expected value was reproduced against node v26.3.0, and both the client-with-push and server-refused-stream edge cases are covered — but that thoroughness itself argues for a maintainer confirming the semantics against nghttp2's last_proc_stream_id.
Other factors
- Overlapping PRs: the description explicitly flags #37563 and #36343 as adding an engine-side
last_peer_stream_idwith the same definition for §5.1 checks. A human needs to decide merge order and reconcile. - Remaining
last_stream_iduses: I checked the ~60 remainingself.last_stream_id.get()sites inh2_frame_parser.rs. Nearly all are in the legacy inbound parser marked dead at the file header. The two live-looking ones in the Sink impl (on_too_many_invalid_framesat line ~5938, unsolicited PING ACK at ~6043) pass the value to JSonErrorbut the resultingdestroy()writes its GOAWAY viagoaway()which readslast_peer_stream_id— so the wire is correct even if those args are stale.highest_started_stream_id()andget_next_stream_id()correctly keep using the mixed mark. - Test quality: the new tests follow harness conventions (raw sockets,
waitForon concrete frame predicates, error handlers wired,port: 0, cleanup infinally). Six of seven are stated to fail onUSE_SYSTEM_BUN=1, andnode-http2.test.jswas rerun.
…e overall high-water mark The push scenarios now deliver the peer-initiated id after a locally initiated stream with a higher id, which is the case the mixed mark gets wrong; the describe comment no longer presents bun's rejection-budget GOAWAYs as node behaviour.
|
Updated 7:17 AM PT - Oct 10th, 2026
✅ @robobun, your commit 2109f5f5ad297542bcea23f398483f5c1097135a passed in 🧪 To try this PR locally: bunx bun-pr 37588That installs a local version of the PR into your bun-37588 --bun |
…ter, NodeHTTPResponse, and three JS binding objects (oven-sh#42682) ### Problem - Some native members exist only for built-in JS to call, and no built-in JS calls them. The largest is `H2FrameParser.setStreamPriority` (`src/runtime/api/bun/h2_frame_parser.rs`). `Http2Stream#priority()` is a no-op since RFC 9113. - Three binding objects (`node:vm`, `node:crypto`, `bun:sql`) carry properties that their only JS consumer never reads. - `packages/bun-usockets` keeps a commented-out `bsd_udp_packet_buffer_ecn` from 2024-04. Nothing reads `completions/spec.yaml`, a 2021 appspec file. ### Fix - Delete each item: 15 files, 535 lines removed. The Notes list every symbol. - Correct because every removed name has zero references in `src/`, `packages/`, `scripts/`, `test/` and `build/debug/codegen/` outside its own definition. No removed member is documented or typed API. User code reaches the h2 parser handle only through the undocumented `Symbol.for("::bunhttp2native::")` key. - The `-D dead-code` lint then reported three more items in `h2_frame_parser.rs`, and two `Stream` fields became write-only. They are removed too. - Verified: `bun bd` and `bun run rust:check-all` (12 targets) pass. The http2, shell, vm, crypto, `node:http`, sql, udp and streams test files pass (list in the Notes). ### Background - A `*.classes.ts` file declares the methods of a native class. The generator emits the C++ wrapper and the Rust glue from it. A `proto` entry that no JS reads is dead, and so is its Rust function. - A binding object is a plain object that native code fills and one built-in module destructures once. A property that the module does not destructure is never read. - 28 other dead-code pull requests are open. This one deletes nothing that they delete. oven-sh#40240 and oven-sh#37588 edit the body of `set_stream_priority`. Each conflict resolves by taking the deletion. <details><summary>Notes</summary> Removed, one line each: - `H2FrameParser.setStreamPriority` / `set_stream_priority` (109 lines). No `.setStreamPriority(` in `src/js` or `test/`. - `H2FrameParser.isStreamAborted` / `is_stream_aborted`. Same check. - `H2FrameParser.hasNativeRead` / `has_native_read`. Same check. - `FrameType::HTTP_FRAME_PRIORITY`, `ErrorCode::PROTOCOL_ERROR`, `SignalRef::is_aborted`. Reported by the dead-code lint once the three functions above were gone. Both enums already list only the wire values that the file uses. - `Stream::stream_dependency`, `Stream::exclusive`. Their only reads were in `set_stream_priority`. The declarations, the initializers and the two stores in `request()` go. The locals that `request()` writes to the wire stay, and so does `Stream::weight` (read by `getStreamState`). - `ShellInterpreter.isRunning` / `is_running` and `.started` / `get_started`. `src/js/builtins/shell.ts` only calls `interp.run()`. - `Interpreter::started`. The field was only read by `get_started`. The two stores and the `AtomicBool` import go with it. - `NodeHTTPResponse.dumpRequestBody` / `dump_request_body`. Added in oven-sh#17093, never called from JS at any commit. - `createNodeVMBinding`: `kUnlinked`, `kLinking`, `kEvaluating`, `kSourceText`, `kSynthetic`. `src/js/node/vm.ts` destructures `kLinked`, `kEvaluated`, `kErrored` and never touches the binding object again. - `createNodeCryptoBinding`: `SecretKeyObject`, `PublicKeyObject`, `PrivateKeyObject`. Not destructured in `src/js/node/crypto.ts`. The classes stay, only the unread properties go. - `bun_sql_jsc::mysql::create_binding`: `MySQLConnection`. `bun_sql_jsc::postgres::create_binding`: `PostgresSQLConnection`. `src/js/internal/sql/{mysql,postgres}.ts` destructure `createConnection`, `createQuery`, `init` only. - `packages/bun-usockets`: the commented-out `bsd_udp_packet_buffer_ecn` (`bsd.c`), its commented-out wrapper `us_udp_packet_buffer_ecn` (`udp.c`), and the two commented-out declarations (`libusockets.h`, `internal/networking/bsd.h`). `git blame`: 589f941, 2024-04-26. The `libusockets.h` lines are context lines of a hunk in oven-sh#40294. - `completions/spec.yaml`. No hit for `spec.yaml` or `appspec` in the repo. It still lists the removed `bun dev` subcommand. The shell completions are hand-maintained and embedded with `include_bytes!`. Tests run with the debug build: `test/js/node/http2/` (7 files, `node-http2.test.js` 387 pass, `h2-conformance.test.ts` 70 pass), the four `test-http2-*priority*` Node tests, `test/js/bun/shell/` (4 files), `test/js/node/vm/vm.test.ts`, `crypto.key-objects.test.ts`, five `test/js/node/http/` files, five `test/js/sql/` files, `udp_socket.test.ts`, `streams.test.js`. How the candidates were found: - A repo-wide identifier index (definitions with zero other mentions, with and without comments). - A relink of the debug binary with `--gc-sections --print-gc-sections`. Almost every real hit from that pass is already deleted by one of the open pull requests. The rest were inlined functions, `const fn`s used only at compile time, and Windows or macOS paths. - A per-class comparison of every `*.classes.ts` member against `src/js`, `packages/bun-types` and `test/`. The generated thunk for a `proto` entry is `#[no_mangle]` and `#[allow(dead_code)]`, so neither rustc nor the linker can flag these members. - Checks for `.rs` files outside every `mod` tree, headers that nothing includes, C/C++ files outside the build, Cargo features that nothing enables, and long commented-out blocks. All clean apart from the usockets block. Found, not deleted here: - `TCPSocket`/`TLSSocket` `endBuffered` (`$end`): no JS caller, but its removal leaves `write_or_end_buffered::<IS_END>` with one instantiation. That is a refactor, not a deletion. - `*InternalReadableStreamSource.isClosed` getter: no reader, but its removal leaves `NewSource::is_closed` write-only, and the stores are in files that oven-sh#41088 touches. - `NodeHTTPResponse.onwritable`: no reader in `src/js` today, but oven-sh#41822 starts to use it. - The HEADERS+PRIORITY emitter and the native `SignalRef` abort path in `H2FrameParser::request()`. `http2.ts` warns with DEP0194 for the priority options and handles `options.signal` itself. Whether these native paths still run needs a separate look. - The `IPV6_RECVTCLASS` / `IP_RECVTOS` `setsockopt` calls in `packages/bun-usockets/src/bsd.c` ("used for getting the ECN"). Nothing reads the ECN now, but their removal changes socket options, so it is not a pure deletion. - `macro(mockedFunction)` and `macro(writer)` in `src/js/builtins/BunBuiltinNames.h` (the builtin-name entries, not the live `mockedFunction` string in `BunCommonStrings.h`): no `mockedFunctionPrivateName` or `writerPrivateName` user, but two open pull requests edit adjacent lines. - `scripts/packer/build-image.pkr.hcl`, `scripts/lldb-inline.sh`, `scripts/lldb-inline-tool.cpp`, `scripts/github-metrics.ts`, `scripts/gamble.ts`: nothing references them, but they are standalone tools that a person can run by hand. Self-reviewed: 12 concerns raised, 10 addressed (the two `Stream` fields, the header comment lines, and the body corrections above). Rejected: a split into three pull requests, because every deletion is verified the same way and a split triples the CI and review rounds. Deferred: a caller lint for `*.classes.ts` `proto` entries, as a follow-up that does not gate this change. </details>
After this side sent a GOAWAY, a stream that it does not open no longer moves the mark: a request that the server refuses, and a stream that a PUSH_PROMISE promises to a client. The id in a later GOAWAY then cannot exceed the id in an earlier one (RFC 9113 6.8). send_go_away takes Option<u32> and reads the mark itself. Only session.goaway(code, lastStreamID) passes an id. The test for the server's budget GOAWAY now uses a malformed request after two pushes, so that the server's own id 4 is above the request on 3.
…the GOAWAY mark A match with no wildcard arm: a new kind of refusal has to say whether the stream counts as processed.
RFC 9113 6.8: an endpoint must not increase the last stream id that it sends. Both GOAWAY writers now take the id from H2FrameParser::next_goaway_last_stream_id. It lowers the id to the id of the last GOAWAY that the session wrote, as nghttp2 does with local_last_stream_id.
…n open nghttp2_submit_goaway refuses such an id and node ignores the error, so session.goaway(code, id) sends nothing. Without this rule, the id clamp puts the sender's own stream id in every later GOAWAY. Add a test for the GOAWAY of the rstStream refusal budget and a test for the GOAWAY of the pending SETTINGS limit.
The mark is not the highest stream id that the peer used: a stream that is refused or promised after a GOAWAY of ours does not raise it. The new name keeps it apart from a highest-received id (nghttp2's last_recv_stream_id). The engine field is private, so only note_processed_stream can raise it.
…t GOAWAY id in an Option node reads lastStreamID with ToInt32, so 2**31 and Infinity mean the last processed stream and 2**32+2 is the id 2. The saturating conversion made every such value 2147483647, which a client took for an id of its own. The tests now write three GOAWAYs for the clamp, end the refused-after- GOAWAY case with a connection error, cover a request with a lower id, and reject a wait when the stream or session closes first.
There was a problem hiding this comment.
I reviewed the latest push (ToInt32 coercion and the own-parity no-op in goaway(), plus the Option<u32> rework of send_go_away) and found no bugs; since this changes what node:http2 puts on the wire in GOAWAY and makes session.goaway(code, id) a silent no-op for an id of the sender's own parity, a maintainer look at that behavioral choice is still worthwhile.
What was reviewed:
goaway()in h2_frame_parser.rs:is_number()is checked beforecoerce_to_i32, so no user JS runs between coercion andsend_go_away; the parity gate (is_multiple_of(2) == is_server) matches "peer-initiated only" for both roles.- Monotonic clamp: both the engine path (
Sink::clamp_goaway_last_stream_id) and every embedder sender go throughnext_goaway_last_stream_id, so no GOAWAY writer bypasses it. note_processed_streamruns beforeon_stream_open/header delivery inhandle_headersandhandle_push_promise, so a synchronousgoaway()/destroy()from JS already sees the advanced mark.- Tests use
port: 0, boundedwaitFor/waitCloseddeadlines rather than sleeps, and assert exact last-stream-id values per GOAWAY.
Extended reasoning...
The diff touches the H2 engine (src/runtime/api/bun/h2/connection.rs), its embedder (src/runtime/api/bun/h2_frame_parser.rs), and adds 17 raw-socket tests plus 7 engine unit tests for GOAWAY Last-Stream-ID semantics. It touches no auth, crypto, or injection surface, but it changes wire-visible protocol behavior for every node:http2 session and makes session.goaway(code, id) silently send nothing for a same-parity id. The bug hunt ran dry and my own read of the coercion ordering, the clamp coverage, and the mark-before-surface ordering found nothing, but the size (~950 lines) and the user-facing behavioral choice mean a human maintainer should weigh it rather than this review approving alone.
…ay-last-peer-stream-id # Conflicts: # test/js/node/http2/h2-conformance.test.ts
h2-conformance.test.ts holds cases that fail now and then on a debug build (the stream-reset floods, the cases that count live stream objects). The new file has only the 20 GOAWAY cases. It uses node:test and node:assert, so `node --test` runs it as it is.
|
A note for this branch. CI now refuses a branch that does not contain main commit b7efd52 (#46227). The pipeline step prints "This branch is too far behind main to build. Merge main into it." and stops. This branch is behind that commit, so its next build will stop there too. #43475 is stacked on this PR and hit that check. Its base is now One more point from the work on #43475. It changes the |
Problem
last_stream_id=1after one request (node: 0). nghttp2 answersGOAWAY: invalid last_stream_id, a connection error.session.goaway(0, 1),destroy()sends 3 (node: 1).local_connection_error(src/runtime/api/bun/h2/connection.rs:448) and four senders inh2_frame_parser.rswritelast_stream_id, which counts both sides.Fix
Connectionownslast_proc_stream_id: requests on a server, promised streams on a client. Every automatic GOAWAY carries it.next_goaway_last_stream_idlowers each id to the last GOAWAY's id.goaway(code, id)sends nothing when only the sender can openid.test/js/node/http2/h2-goaway-last-stream-id.test.ts, 20 tests. All pass withnode --teston v26.3.0, 18 fail on main. Also 261 nodetest-http2-*files and grpc-js suites.Background
Connectionparses inbound frames.H2FrameParserwrites the GOAWAY frames that JS asks for, so it keeps a copy.Downsides
state.lastProcStreamIDis 0 until a push arrives (main: the last request id), andgoaway(code, id)with the sender's own id sends nothing.maxConcurrentStreams(fix in node:http2: refuse a stream over the advertised limit before it is opened, on maxSessionInvalidFrames #44337), a push overmaxReservedRemoteStreams..textgrows 256 bytes.Notes
Which stream node names. A server has an open request on stream 1. A second request arrives on stream 3. Then the session closes. The table shows the Last-Stream-ID of the GOAWAY.
:path)maxSessionMemorymaxConcurrentStreams'stream'handlernghttp2 sets
last_proc_stream_idafter its stream-limit checks and before theon_begin_headerscallback. Node refuses formaxSessionMemoryin that callback. So node counts a stream that it refused for memory, and does not count a stream over the limit.The report about the rejected-stream limit. A server has
maxConcurrentStreams: 1andmaxSessionRejectedStreams: 3. Stream 1 is open. HEADERS arrive on 3, 5 and 7. Main and this PR send GOAWAY(ENHANCE_YOUR_CALM) with Last-Stream-ID 7. Node names 1: it sends GOAWAY(PROTOCOL_ERROR) with id 1 when the client ACKed the limit. When the client did not, node sends three RST_STREAM(REFUSED_STREAM) and no GOAWAY. The report is correct. The cause is that the engine opens each of those streams before JS refuses it. #44337 refuses the stream before it opens, and its testa refused stream is not the last stream that the session processedcovers the id. This PR does not do that work a second time. ThematchonBlockDispositioninhandle_headershas no wildcard arm. A new refusal variant does not compile until it says whether the stream counts.The client has the same gap for a push. With
maxReservedRemoteStreams: 1and promises of 2 and 4, JS cancels stream 4 after the engine reserved it. Node names 2, this PR names 4, bun 1.4.2 names 0. That refusal must move into the engine too. It is separate work.The id does not rise. Three rules keep the id of a later GOAWAY at or below the id of an earlier one.
goaway(), then the server sends PUSH_PROMISE(2) and a PING with a bad length. Ids of the client's GOAWAY frames: node 0, 0. Main 0, 2, 0. This PR 0, 0, 0.H2FrameParser::next_goaway_last_stream_idlowers each id to the id of the last GOAWAY (nghttp2'slocal_last_stream_id). The engine reaches it throughSink::clamp_goaway_last_stream_id. A server with requests on 1 and 3 callssession.goaway(0, 1). Ids afterclose(),destroy(), a secondgoaway()or a connection error: node 1, 1. Bun 1.4.2: 1, 3. This PR 1, 1. A client with pushed streams 2 and 4 callsgoaway(0, 2): node 2, 2. Bun 1.4.2: 2, 0 (2, 4 after a connection error). This PR 2, 2.goaway(code, id)with an id that only the sender can open sends nothing. nghttp2 returnsNGHTTP2_ERR_INVALID_ARGUMENTand node ignores it. Bun 1.4.2 sends that GOAWAY. With rule 2 alone, every later GOAWAY would then carry the sender's own id. The id is read with ToInt32, as node reads it:2 ** 31andInfinitymean the last processed stream, and2 ** 32 + 2is the id 2. For 11 ids outside the int32 range, on a client and on a server, the frames are the same as node's.Node applies rule 2 when it writes the frame. So
goaway(0, 1)andclose()in one tick give 1, 3 in node. Bun applies the rule at the call and gives 1, 1.Bun still serves a request that opens after the server's GOAWAY (#43475 covers it). Such a request raises
state.lastProcStreamID, but the next GOAWAY keeps the earlier id. A server sends GOAWAY with id 1, then a request arrives on 3, thenclose(): node does not serve it and sends 1, 1. Bun 1.4.2 serves it and sends 1, 3. This PR serves it and sends 1, 1. #43612 discards the late PUSH_PROMISE itself. Node also closes the open streams above the id of a GOAWAY that it sent. Bun leaves them open. That is separate work.The 20 tests. All are in
test/js/node/http2/h2-goaway-last-stream-id.test.tsand use a raw socket as the peer. On a debug build of main (71d0d43) 18 fail and tests 8 and 9 pass. The value in parentheses is what bun 1.4.2 gives.close()after a request refused formaxSessionMemory: names 3 (1).state.lastProcStreamIDis 0 (1).close()after a push: names 2 (0).maxSessionRejectedStreams, after a refused response: each GOAWAY names 0 (1).maxOutstandingSettings: 0, secondsettings()call: each GOAWAY names 0 (1).state.lastProcStreamIDstays 3 andclose()names 3. Passes without the fix.close(): each GOAWAY names 3 (4).maxConcurrentStreams: 1that pushed 2 and 4, then a request on 3: no GOAWAY names an even id (4).maxSessionMemoryaftersession.goaway(), then a connection error: names 1 (3).client.goaway():state.lastProcStreamIDstays 0 and the next GOAWAY names 0 (1, then 2).goaway(0, 1),goaway(0, 3),destroy(): three GOAWAY frames, each names 1 (1, 3, 3).goaway(0, 1), then a connection error: names 1 (3).goaway(0, 2)after pushes 2 and 4,close(),destroy(err): three GOAWAY frames, each names 2 (lastProcStreamIDis 1).goaway(0, 2): no GOAWAY.destroy()then names 3 (2, 3).goaway(0, 1): no GOAWAY.close()then names 4 (1, 0).goaway(0, 2 ** 32 + 2): no GOAWAY.goaway(0, 2 ** 31)names 3 (2147483647).goaway(0, 2 ** 31)after pushes 2 and 4: names 4 (2147483647).Tests 5, 7, 10 and 11 assert the id of every GOAWAY and not the number of frames. Node and bun write a different set of GOAWAY frames when a session budget is used up. Test 10 uses a malformed request, because #44248 stops the count of a stream that the handler refuses. Tests 7 and 11 cover the GOAWAY of the pending SETTINGS limit and of the
rstStreamrefusal budget. Tests 14 and 16 write three frames, so a helper that keeps the last requested id and not the lowest sent id fails. A wait for a stream or session event rejects when that stream or session closes first.The file uses
node:testandnode:assert, sonode --test test/js/node/http2/h2-goaway-last-stream-id.test.tsruns it as it is. The cases are not inh2-conformance.test.ts, because that file holds cases that fail now and then on a debug build: on main, a stream-reset flood case timed out in 1 of 2 runs.Engine unit tests.
connection.rshas 7 new#[test]s against a bareConnection. The workspace cannot run the unit tests ofbun_runtime. A standalone crate that compiles theh2/files with lshpack runs them: 31 tests pass. Four mutations fail the matching tests: count a stream refused after our GOAWAY (1 fails), count a stream promised after our GOAWAY (1 fails), writelast_stream_idinto the GOAWAY (5 fail), skip the clamp hook (1 fails).Cost.
Connectionis 448 bytes on main and 456 bytes here.H2FrameParseris 1504 bytes on main and 1512 bytes here (Cell<Option<u32>>for the id of the last GOAWAY). Both from the debug binary with gdb..text58,161,589 and 58,161,845 bytes,.rodata19,804,908 bytes in both, stripped binary 80,844,360 bytes in both.handle_received_stream_idare gone.handle_headersdoes one compare, and oneCellstore when the mark rises. Per GOAWAY: oneminand oneCellstore. No allocation and no JS call is new.Other suites. On a debug build of this branch, after the merge of main: the 10 other test files of
test/js/node/http2/, 261 nodetest-http2-*files and the grpc-js server, server-errors, retry, idle-timer, server-deadlines and channelz suites pass.h2-conformance.test.tspassed in 1 of 2 runs with--timeout 30000: a stream-reset flood case timed out, as it does on main.Self-review. Two reviews ran: one on the mark change, one on the pushed diff with probes on node v26.3.0 and on a build of this PR. They raised 15 concerns.
goaway(code, id)sent. Addressed: rule 2.rstStreambudget and of the pending SETTINGS limit had no test. Addressed: tests 7 and 11.last_proc_stream_idnow.goaway(code, 2 ** 31). Addressed: ToInt32, tests 19 and 20.once()did not end when the stream closed first. Addressed.maxReservedRemoteStreamsstill counts. Separate work.goaway()with more than 16376 octets of opaque data sends a frame that node does not send. Not from this diff: node:http2: send nothing from goaway() when opaqueData does not fit a GOAWAY frame #42461.#[test]s ofconnection.rsand does not run them. Rejected: no crate test ofbun_runtimeruns in CI, and the ten older tests of this module are in the same state. The JS tests cover each rule.The second review also ran 700 random sequences of requests, pushes, resets,
goaway(code, id)calls and shutdowns on node and on the previous head of this PR. The GOAWAY ids andstate.lastProcStreamIDwere the same in all 700.Open PRs that touch the same lines.
on_stream_rejectedand the budget block inrst_stream. Their GOAWAY call passesself.last_stream_id.get(). After this PR it passesNone.BlockDispositionvariants for a stream that never opens. Each needs an arm in the newmatchthat does not count the stream. A push that node:http2: refuse a PUSH_PROMISE on a request stream that user code already closed #43465 cancels must not reachnote_processed_stream.sent_goaway_last_stream_id: Cell<Option<u32>>, and lowers the id inlocal_connection_erroronly. Keep one field and the helper of this PR.Connection::last_peer_stream_id, the highest id that the peer used (nghttp2'slast_recv_stream_id). That is a different value fromlast_proc_stream_id. Keep both.goawayhost function next to the new check of the id.[human-review] gate passed · iteration 2 · 3 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 2 passed · 1 rejected · iteration 2
evidence per changed file