Repository navigation
Conversation
…'s rstCode ClientHttp2Session#destroy() cancels the requests still queued in #pendingRequests (made before the socket connected, or waiting for a SETTINGS_MAX_CONCURRENT_STREAMS slot) with ERR_HTTP2_STREAM_CANCEL, but stamped NGHTTP2_CANCEL on them whenever destroy() was called without a code. Node's Http2Stream._destroy gives such a stream the session's code (a received GOAWAY's, else the destroy code) and otherwise falls back to NGHTTP2_INTERNAL_ERROR; NGHTTP2_CANCEL is only used for AbortSignal cancellations. It also leaves the code of a stream that already close()d alone. Only pre-set rstCode when there is a session code and the stream has none yet, and let Http2Stream#_destroy derive NGHTTP2_INTERNAL_ERROR from the cancel error otherwise, which is the same derivation node uses.
|
Warning Review limit reached
Next review available in: 11 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
Comment |
|
Updated 3:05 AM PT - Aug 12th, 2026
❌ @robobun, your commit 1d40bae has some failures in 🧪 To try this PR locally: bunx bun-pr 37744That installs a local version of the PR into your bun-37744 --bun |
|
Status Reproduced on the current release and on main: a request queued before the session connected (or behind Fix is in CI (build 92834): every lane that ran is green (192/192, the new tests included; the handful of retried tests are known flaky ones unrelated to http2 teardown, plus the pre-existing 10k-request |
There was a problem hiding this comment.
LGTM — small Node-compat fix that mirrors the existing destroyStreamForSessionDestroy rule for pending requests.
What was reviewed:
- Traced all seven
destroy()shapes plus close-first and GOAWAY-first through the newsessionCode = this[kGoawayCode] || code/if (sessionCode && !req.rstCode)logic and the_destroyfallback at http2.ts:2793-2805 — each yields the node-matching rstCode from the PR table. - Confirmed the guard is byte-identical to
destroyStreamForSessionDestroy(http2.ts:2454) and the active-stream teardown'sthis[kGoawayCode] || codeexpression (http2.ts:4967). - Tests: hermetic (local
http2.createServer, port 0), await thecloseevent via a resolver, clean up in try/finally, and cover both queue reasons plus the two precedence cases.
Extended reasoning...
Overview
Two-line behavioral change in ClientHttp2Session#destroy() (src/js/node/http2.ts): pending (never-sent) requests torn down by session destroy now get rstCode = this[kGoawayCode] || code only when that value is truthy and the stream hasn't already recorded one, instead of unconditionally code ?? NGHTTP2_CANCEL. When no session code applies, rstCode is left unset so Http2Stream#_destroy (lines 2793-2805) derives NGHTTP2_INTERNAL_ERROR from the non-abort cancel error — exactly what node does. The comment is updated to explain the derivation. A new 4-test describe.concurrent block in node-http2.test.js exercises seven destroy() argument shapes across two pending-queue reasons, plus close(code)-then-destroy and GOAWAY-precedence.
Security risks
None. This adjusts which numeric constant is reported on stream.rstCode for already-cancelled client requests. No parsing, no auth, no resource limits, no new external input handling.
Level of scrutiny
Low-to-medium. The change is a Node-compat correctness fix in a JS builtin, and the new guard is the same if (rstCode && !stream.rstCode) pattern already used by destroyStreamForSessionDestroy (line 2454) and the same this[kGoawayCode] || code precedence used for active streams (line 4967). The error object emitted (ERR_HTTP2_STREAM_CANCEL with cause) is unchanged. I traced every row of the PR's behavior table through the code path (including the typeof error === 'number' and code === undefined && error != null normalization at lines 5995-6001) and each produces the stated node-matching value.
Other factors
Tests are well-structured per the repo guidelines: local server with port: 0, resolve on the close event (no sleeps), try/finally cleanup, describe.concurrent for independent cases, and an expect(active.pending).toBe(false) precondition assert that proves the concurrency-queued case is actually queued. The PR description documents that all four new tests fail on the current release and that the surrounding suites (node-http2.test.js, h2-conformance.test.ts, and the vendored node http2 tests) still pass. The bug-hunting system found nothing.
Problem
node:http2client request that is still queued (no stream id yet) whensession.destroy()is called with no code reportsrstCode8 (NGHTTP2_CANCEL); node reports 2 (NGHTTP2_INTERNAL_ERROR). The error,ERR_HTTP2_STREAM_CANCEL, already matches.close(code)d, so both report the destroy code where node reports the GOAWAY or close code.NGHTTP2_CANCEL) instead of letting the stream's own teardown pick one.Fix
_destroyderivesNGHTTP2_INTERNAL_ERRORfrom the cancel error, as node does and as bun'sdestroy(0)anddestroy(err)already did. Active streams already follow this rule on session destroy.ERR_HTTP2_STREAM_CANCELwith the session error as itscause.destroy()shapes plus theclose(code)and GOAWAY cases; all four fail on current bun and pass with this change. The existing http2 suites and the vendored node teardown tests still pass on a debug build.Background
rstCodeis the RST_STREAM error code anHttp2Streamended with. Codes seen here: 2INTERNAL_ERROR, 7REFUSED_STREAM, 8CANCEL, 11ENHANCE_YOUR_CALM.SETTINGS_MAX_CONCURRENT_STREAMSslot (node hands the latter to nghttp2). Both reportpending === trueand go through the changed path.AbortErrorgetsNGHTTP2_INTERNAL_ERRORunless it already has a code;NGHTTP2_CANCELis reserved for AbortSignal cancellations.destroy(code).close(code)on a pending request records the code and waits for a stream id before sending RST_STREAM, so the request is still queued when the session goes away.Original description
Repro
A request that is still pending (no stream id yet) when its client session is destroyed without a code reports
rstCode8 (NGHTTP2_CANCEL); node reports 2 (NGHTTP2_INTERNAL_ERROR). The error object already matches (ERR_HTTP2_STREAM_CANCELon both).Full table for a pending request, bun main vs node v26.3.0 (the same script with the
destroy()arguments varied; the close()d and GOAWAY rows are separate scripts):destroy()destroy(null)destroy(0)destroy(7)/destroy(null, 7)/destroy(err, 7)destroy(err)req.close(5)first, thendestroy()ordestroy(7)'goaway'listener callsdestroy()ordestroy(7)Cause
ClientHttp2Session#destroy()(src/js/node/http2.ts) cancels the requests still queued in#pendingRequestswithNode's
closeSessiondestroys its pending streams withnew ERR_HTTP2_STREAM_CANCEL(error)and letsHttp2Stream._destroypick the code: the session's code (goawayCode || destroyCode) when non-zero, otherwiseNGHTTP2_INTERNAL_ERRORbecause the stream is being destroyed with an error that is not anAbortError(NGHTTP2_CANCELis reserved for AbortSignal cancellations). It also never overwrites the code of a stream that was alreadyclose()d. Our unconditional assignment hard-codedCANCELfor the no-code case, ignored a received GOAWAY's code, and clobbered aclose(code).Fix
Only pre-set
rstCodewhen there is a session code (this[kGoawayCode] || code, the expression the active-stream teardown already uses) and the stream has none yet; otherwise leave it unset soHttp2Stream#_destroyderivesNGHTTP2_INTERNAL_ERRORfrom the cancel error, which is the same derivation node performs (and the onedestroy(0)anddestroy(err)were already going through). This is the sameif (rstCode && !stream.rstCode)rule asdestroyStreamForSessionDestroy.The reported error is unchanged: every shape still gets
ERR_HTTP2_STREAM_CANCELwith the session error as itscause. Bun also queues requests that are waiting for aSETTINGS_MAX_CONCURRENT_STREAMSslot in#pendingRequests(node hands those to nghttp2, where they already have an id); they reportpending === trueand get the cancel error here today, so they follow the same rule. TherstCodethey get matches what node reports for those streams as well (the table's GOAWAY row was measured that way).Related open PRs touch neighbouring paths but not this one: #33802 changes how active streams are torn down by the client
destroy(), and #37698 makesdestroy(undefined, code)behave likedestroy(); this change composes with both (once #37698 lands,destroy(undefined, 7)also yields 2 here, as in node).Verification
New
describeintest/js/node/http2/node-http2.test.jscovers both queue reasons (request before connect, request behindmaxConcurrentStreams) for sevendestroy()shapes, plus theclose(code)-then-destroy and GOAWAY-precedence cases. All four tests fail on the current release (rstCode8/7/11 where node gives 2/5/11 as in the table) and pass with this change.node-http2.test.js(360 pass),h2-conformance.test.ts(61 pass) and the vendored node tests that exercise pending streams and session teardown (test-http2-client-destroy,test-http2-client-stream-destroy-before-connect,test-http2-client-rststream-before-connect,test-http2-stream-removelisteners-after-close,test-http2-propagate-session-destroy-code,test-http2-max-concurrent-streams,test-http2-goaway-delayed-request, ...) still pass with a debug build.