Skip to content

Upgrade reported Node.js version to 26.3.0#31818

Open
Jarred-Sumner wants to merge 29 commits into
mainfrom
claude/upgrade-nodejs-26
Open

Upgrade reported Node.js version to 26.3.0#31818
Jarred-Sumner wants to merge 29 commits into
mainfrom
claude/upgrade-nodejs-26

Conversation

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

What

Bumps the Node.js compatibility target from 24.3.0 to 26.3.0 (V8 14.6.202.34-node.20, NODE_MODULE_VERSION 147), and syncs node:stream/node:http with the behavioral changes upstream made between v24.x and v26.x.

Native / addon ABI

  • Headers, bootstrap pins, flake, process.versions updated
  • V8 shim updated for 14.6: Isolate roots layout, flattened FunctionCallbackInfo exit frame, String Write*V2/Utf8LengthV2 (legacy exports kept for older binaries), External::New pointer-tag overload, Number::NewFromInt32/NewFromUint32, HandleScope::Extend/DeleteExtensions — with Itanium + MSVC symbol exports
  • Fixed a napi_get_value_string_* panic when querying length with a null buffer
  • v8/napi fixtures migrated to the new header APIs

test/v8: 50 pass / 1 skip · test/napi: 99 pass / 0 fail

stream/http sync

Audited every behavioral diff in upstream lib/internal/streams/*, lib/_http_*, webstreams adapters, and http2 between v24.3.0 and v26.3.0, and ported the ones we were carrying v24 (or divergent) behavior for — highlights:

  • Writable.toWeb sync-drain hang fix; fromWeb writev rejection no longer crashes the error path
  • read() one-chunk semantics in paused mode (semver-major in 26), compose() on the prototype
  • Duplex.from(async gen) destroy-during-idle hang fix; BYOB Readable.toWeb({type:'bytes'}); DEP0201
  • writeHeader removed (DEP0063 EOL); upgrade requests without a listener no longer disappear; set-cookie getHeader/setHeader edge cases; http2 respond() raw-array rejection and session error codes
  • Also fixes a pending BYOB reader.read() never settling after reader.cancel() (spec step 5)

Vendored the matching upstream tests; updated v24-era vendored tests whose expectations legitimately changed and verified each new expectation against real Node 26.3.0.

Known-unrelated failures

ASAN-debug only, reproduced identically at base: BIO_ctrl SEGV on aggregate http test runs, http2 maxSessionMemory timeout, streams-leak absolute-RSS bound.

Not in scope (follow-ups)

New Node 26 APIs (fs.Utf8Stream, req.signal, writeInformation, http2 options), full env-proxy support, upgrade-with-body UpgradeStream, flag-gated stream/iter family.

Bump the compat target from Node 24.3.0 to 26.3.0:
- nodejs-headers 26.3.0, NODE_MODULE_VERSION 147, bootstrap/flake pins
- process.versions.v8 = 14.6.202.34-node.20, modules = 147
- V8 shim updated for 14.6: Isolate roots layout, flattened
  FunctionCallbackInfo exit frame, String Write/WriteOneByte/WriteUtf8 V2 +
  Utf8LengthV2 (legacy exports kept), External::New pointer-tag overload,
  Number::NewFromInt32/NewFromUint32, HandleScope::Extend/DeleteExtensions
- napi_get_value_string_* no longer forms a slice from a null buffer
  pointer when only querying the encoded length
- v8/napi test fixtures migrated to the V2 string APIs
Per https://streams.spec.whatwg.org/#readable-stream-cancel step 5, a BYOB
reader's pending read requests must be closed with {value: undefined,
done: true} when the stream is cancelled. readableStreamClose only settles
default-reader read requests, so a pending byob read() hung forever after
reader.cancel().
Port the observable behavior changes between Node v24.3.0 and v26.3.0:

streams:
- read() with no size returns one buffered chunk at a time in paused mode
- pause()/resume() are no-ops on destroyed streams; compose() moved to the
  prototype and returns the composed Duplex directly
- Duplex.from(async generator) destroy aborts with the error and unblocks a
  parked iterator; duplexPair destroy propagates to the other side
- eos()/finished(): conditional AsyncLocalStorage binding, immediate-result
  precompute for already-settled streams, real errors override AbortError in
  pipeline()
- webstreams adapters: Writable.toWeb no longer hangs on synchronous drain,
  fromWeb writev failures error the stream instead of an unhandled rejection,
  Readable.toWeb supports type 'bytes' (BYOB), Duplex.toWeb readableType
  (DEP0201 for the old alias), ArrayBuffer/SharedArrayBuffer chunk handling,
  brotli decoder errors surface as TypeError with the original code
- write(string, 'buffer') throws ERR_UNKNOWN_ENCODING

http:
- ServerResponse.writeHeader removed (DEP0063 end-of-life)
- upgrade requests with no 'upgrade' listener no longer vanish
- getHeader('set-cookie') returns undefined when absent, and
  setHeader('set-cookie', []) round-trips as a present-but-empty array
- remove dead half-ported proxy symbols in Agent, https.Agent no longer
  shadows createConnection, drain is only emitted when needed
- http2: respond() rejects raw-header arrays instead of sending garbage
  frames, request() on closed sessions uses the right error codes, option
  range errors use the options. prefix, initialWindowSize is capped

Vendored the matching upstream tests and updated the v24-era ones whose
expectations changed.
@robobun

robobun commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator
Updated 2:19 PM PT - Jun 7th, 2026

@Jarred-Sumner, your commit 6077779 has 6 failures in Build #61314 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 31818

That installs a local version of the PR into your bun-31818 executable, so you can run:

bun-31818 --bun

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Pins Node/V8 to 26.3.0 and updates build/tooling; rewrites WebStreams adapters and compression builtins; refactors stream EOS, BYOB, and compose semantics; tightens HTTP/HTTPS/HTTP2 header/session handling and outgoing buffering; updates V8 ABI/bindings and exported symbols; and adds/updates tests to match Node v26 behavior.

Changes

Version & build

Layer / File(s) Summary
Bootstrap and toolchain pins
scripts/bootstrap.ps1, scripts/bootstrap.sh, flake.nix, scripts/build/deps/nodejs-headers.ts, scripts/build/flags.ts, scripts/build/config.ts, scripts/build/codegen.ts
Node.js and ABI pinned to 26.3.0/147, V8 reported version surfaced to build defines/config; codegen inputs updated (ErrorCode added).

WebStreams adapters & Compression

Layer / File(s) Summary
Adapter core and buffer-source transforms
src/js/internal/webstreams_adapters.ts, src/js/builtins/CompressionStream.ts, src/js/builtins/DecompressionStream.ts
Adapters gain kValidateChunk/kDestroyOnSyncError, newBufferSourceTransformPairFromDuplex, SafePromiseAllReturnVoid usage, BYOB/bytes support; compression builtins now use adapter transform pairs.

Streams core and EOS

Layer / File(s) Summary
Streams internals and operators
src/js/internal/streams/end-of-stream.ts, src/js/internal/streams/readable.ts, src/js/internal/streams/writable.ts, src/js/internal/streams/duplex.ts, src/js/internal/streams/duplexify.ts, src/js/internal/streams/operators.ts, src/js/builtins/ReadableStreamInternals.ts
eos() migrated to AsyncResource/AsyncContext wiring with a synchronous-callback symbol; BYOB cancel fulfills pending readIntoRequests; Readable adds compose() and resume/pause removed early-return on destroyed streams; writable rejects string+"buffer" encoding; operators standardize validation and drop compose export.

HTTP / HTTPS / HTTP2

Layer / File(s) Summary
HTTP parser, server, outgoing, https agent, http2
src/js/internal/http.ts, src/js/node/_http_common.ts, src/js/node/_http_server.ts, src/js/node/_http_outgoing.ts, src/js/node/https.ts, src/js/node/http2.ts
Parser now clamps header appends by remaining maxHeaderPairs; OutgoingMessage tracks empty set-cookie and uses per-instance buffering with _flushOutput; upgrade handling requires an upgrade listener; HTTPS Agent normalizes options and uses tls.connect; HTTP/2 tightened validation, rejects array raw-headers, and adds GOAWAY-specific error handling (ERR_HTTP2_GOAWAY_SESSION).

V8 binding & ABI changes

Layer / File(s) Summary
V8 handle-scope, FunctionCallbackInfo, String V2, symbols
src/jsc/bindings/v8/*, src/jsc/ErrorCode.rs, src/jsc/bindings/ErrorCode.*, src/symbols.*, src/runtime/napi/napi_body.rs
Introduce HandleScopeData accessor and refactor EscapableHandleScope/HandleScope/HandleScopeBuffer/FunctionCallbackInfo for V8 inline ABI; add Number/External overloads and String *V2 APIs; extend exported symbol lists and add ERR_HTTP2_GOAWAY_SESSION mapping.

N-API and safety

Layer / File(s) Summary
N-API and build/runtime guards
src/jsc/bindings/napi.cpp, scripts/build/flags.ts
Adds defensive bufsize clamping for string encoding and exposes reported V8 version via build defines.

Tests

Layer / File(s) Summary
Stream, webstreams adapters, compression, HTTP, HTTP2, V8 tests
test/js/node/stream/*, test/js/node/test/parallel/*, test/js/node/http/*, test/js/node/http2/*, test/js/web/streams/*, test/v8/*, test/js/node/process/process.test.js, test/napi/*
Adds and updates many tests to match Node v26 semantics: BYOB read/cancel behavior, adapter buffer-source handling, compression chunk validation, read() chunking expectations, HTTP/HTTP2 regressions, V8 WriteUtf8V2/Utf8LengthV2 expectations, and updated process.version assertions.

Possibly related PRs

Suggested reviewers

  • alii

Comment thread src/js/node/_http_outgoing.ts
Comment thread test/js/node/stream/node-stream.test.js

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/js/builtins/CompressionStream.ts (1)

8-30: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix CompressionStream chunk validation to reject non-BufferSource writes, not only SharedArrayBuffer-backed ones.

src/js/builtins/CompressionStream.ts’s [kValidateChunk] only throws when the chunk’s backing store is a SharedArrayBuffer; it still allows other spec-invalid chunk types (e.g., strings/plain objects) to reach the underlying zlib stream. The Compression Streams spec (and Node’s enforcement) requires CompressionStream.writable chunks to be BufferSource (ArrayBuffer or ArrayBufferView) and disallows SharedArrayBuffer-backed sources up front.

🤖 Prompt for 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.

In `@src/js/builtins/CompressionStream.ts` around lines 8 - 30, The kValidateChunk
implementation in CompressionStream currently only rejects
SharedArrayBuffer-backed sources; update the [kValidateChunk]
(validateBufferSourceChunk) used in newReadableWritablePairFromDuplex to first
ensure the chunk is a BufferSource — i.e., either an ArrayBuffer (instanceof
ArrayBuffer) or an ArrayBufferView (use isArrayBufferView) — and if not, throw
$ERR_INVALID_ARG_TYPE("chunk", ["ArrayBuffer", "Buffer", "TypedArray",
"DataView"], chunk); then, for ArrayBuffer or ArrayBufferView, continue to
reject cases whose backing store is a SharedArrayBuffer by using
isSharedArrayBuffer on the appropriate .buffer. Keep the error type/message
consistent with existing throws in this file and update only the
validateBufferSourceChunk function referenced by
CompressionStream/kValidateChunk.
🤖 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 `@src/js/internal/streams/end-of-stream.ts`:
- Around line 165-172: The synchronous-callback path can call the callback
before eos() returns so the later "cleanup" isn't initialized; to fix, declare
and initialize cleanup to a no-op before the immediateResult branch (e.g. let
cleanup = () => {};), then later overwrite cleanup with the real return value
from eos(...). Update the code paths in finished() that reference
options[kEosNodeSynchronousCallback], immediateResult, callback.$call and the
eventual assignment from eos(...) so the early return safely returns the
initialized no-op cleanup.

In `@src/js/node/_http_outgoing.ts`:
- Line 33: The sentinel kEmptySetCookie used to mark an explicit empty
"set-cookie" must be cleared/recomputed whenever the entire header bag
(this[headersSymbol]) is replaced; update code paths that reassign the header
store (e.g., where msg.headers = new Headers(...), or any direct assignment to
this[headersSymbol]) to reset or recompute kEmptySetCookie so subsequent calls
to getHeader("set-cookie"), hasHeader(), and getHeaderNames() reflect the new
Headers instance; specifically, in the header-replacement code clear the
kEmptySetCookie flag (or recompute from the new this[headersSymbol]) so
setHeader, getHeader and related methods behave correctly.

In `@src/js/node/http2.ts`:
- Around line 384-389: The GOAWAY handling currently destroys the
ClientHttp2Session immediately (making this.destroyed true) so request() never
hits the new ERR_HTTP2_GOAWAY_SESSION branch; instead add and use a separate
"gotGoaway" flag on ClientHttp2Session (set inside the `#Handlers.goaway` handler
before any destroy) and update request() to check that flag and throw the
ERR_HTTP2_GOAWAY_SESSION error (created by goawaySessionError) when appropriate;
defer or conditionally call destroy() so destroy does not preempt the GOAWAY
check, and update any logic that relied on destroyed to also consider gotGoaway
where necessary.

In `@test/js/node/http/node-http-parser.test.ts`:
- Around line 254-255: Hoist the require of "node:_http_common" to module scope
instead of inside the test: move the const { parsers } =
require("node:_http_common") out of the test body so the test only calls
parsers.alloc() (creating parser via parser = parsers.alloc()) at runtime;
update references to parsers and parser to use the module-scoped parsers
variable and keep the test focused on behavior, not dynamic loading.

In `@test/js/node/stream/node-stream.test.js`:
- Around line 798-1004: The file contains an exact duplicate of the describe
block "node v26 stream semantics" (the second occurrence repeats all tests such
as the it("read() with no size returns one buffered chunk at a time in paused
mode"), it("read() with no size still concatenates when setEncoding is active"),
the pause/resume destroyed test, compose tests, and write(string,'buffer')
test). Remove the entire second duplicate describe("node v26 stream semantics",
...) block (the duplicate that repeats the same it(...) tests) so only a single
describe block and its matching closing brace remain.

---

Outside diff comments:
In `@src/js/builtins/CompressionStream.ts`:
- Around line 8-30: The kValidateChunk implementation in CompressionStream
currently only rejects SharedArrayBuffer-backed sources; update the
[kValidateChunk] (validateBufferSourceChunk) used in
newReadableWritablePairFromDuplex to first ensure the chunk is a BufferSource —
i.e., either an ArrayBuffer (instanceof ArrayBuffer) or an ArrayBufferView (use
isArrayBufferView) — and if not, throw $ERR_INVALID_ARG_TYPE("chunk",
["ArrayBuffer", "Buffer", "TypedArray", "DataView"], chunk); then, for
ArrayBuffer or ArrayBufferView, continue to reject cases whose backing store is
a SharedArrayBuffer by using isSharedArrayBuffer on the appropriate .buffer.
Keep the error type/message consistent with existing throws in this file and
update only the validateBufferSourceChunk function referenced by
CompressionStream/kValidateChunk.
🪄 Autofix (Beta)

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: dcaf62e9-ee50-4b2e-8c62-7c2c241fa415

📥 Commits

Reviewing files that changed from the base of the PR and between a3464c6 and f6fd2e0.

📒 Files selected for processing (48)
  • scripts/bootstrap.ps1
  • scripts/bootstrap.sh
  • src/js/builtins/CompressionStream.ts
  • src/js/builtins/DecompressionStream.ts
  • src/js/builtins/ReadableStreamInternals.ts
  • src/js/internal/http.ts
  • src/js/internal/primordials.js
  • src/js/internal/streams/duplex.ts
  • src/js/internal/streams/duplexify.ts
  • src/js/internal/streams/end-of-stream.ts
  • src/js/internal/streams/operators.ts
  • src/js/internal/streams/pipeline.ts
  • src/js/internal/streams/readable.ts
  • src/js/internal/streams/writable.ts
  • src/js/internal/webstreams_adapters.ts
  • src/js/node/_http_common.ts
  • src/js/node/_http_outgoing.ts
  • src/js/node/_http_server.ts
  • src/js/node/http2.ts
  • src/js/node/https.ts
  • src/jsc/bindings/NodeHTTP.cpp
  • src/jsc/bindings/v8/v8_handle_scope_data.h
  • test/js/node/http/node-http-parser.test.ts
  • test/js/node/http/node-http.test.ts
  • test/js/node/http2/node-http2.test.js
  • test/js/node/stream/node-stream-uint8array.test.ts
  • test/js/node/stream/node-stream.test.js
  • test/js/node/test/parallel/test-crypto-cipheriv-decipheriv.js
  • test/js/node/test/parallel/test-http2-getpackedsettings.js
  • test/js/node/test/parallel/test-stream-compose.js
  • test/js/node/test/parallel/test-stream-push-strings.js
  • test/js/node/test/parallel/test-stream-readable-emittedReadable.js
  • test/js/node/test/parallel/test-stream-readable-infinite-read.js
  • test/js/node/test/parallel/test-stream-readable-needReadable.js
  • test/js/node/test/parallel/test-stream-readable-to-web-byob.js
  • test/js/node/test/parallel/test-stream-readable-to-web-termination-byob.js
  • test/js/node/test/parallel/test-stream-readable-to-web-termination.js
  • test/js/node/test/parallel/test-stream-typedarray.js
  • test/js/node/test/parallel/test-stream-uint8array.js
  • test/js/node/test/parallel/test-stream2-transform.js
  • test/js/node/test/parallel/test-webstreams-adapters-writable-buffer-sources.js
  • test/js/node/test/parallel/test-webstreams-compression-bad-chunks.js
  • test/js/node/test/parallel/test-webstreams-compression-buffer-source.js
  • test/js/node/test/parallel/test-webstreams-duplex-fromweb-writev-unhandled-rejection.js
  • test/js/node/test/parallel/test-whatwg-webstreams-compression.js
  • test/js/node/test/parallel/test-zlib-flush-write-sync-interleaved.js
  • test/js/web/streams/compression.test.ts
  • test/js/web/streams/streams.test.js

Comment on lines +165 to +172
if (immediateResult !== undefined && options[kEosNodeSynchronousCallback]) {
if (immediateResult === null) {
callback.$call(stream);
} else {
callback.$call(stream, immediateResult);
}
return cleanup;
}

@coderabbitai coderabbitai Bot Jun 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make finished() safe before exposing synchronous EOS callbacks.

This path can invoke the callback before eos() returns. finished() below still does const cleanup = eos(...), so finished(stream, { cleanup: true, [eos.kEosNodeSynchronousCallback]: true }) on an already-settled stream will hit cleanup before initialization and throw.

💡 One way to harden `finished()`
 return new Promise<void>((resolve, reject) => {
-  const cleanup = eos(stream, opts, err => {
+  let cleanup = nop;
+  let cleanupAfterAssign = false;
+  cleanup = eos(stream, opts, err => {
     if (autoCleanup) {
-      cleanup();
+      if (cleanup === nop) cleanupAfterAssign = true;
+      else cleanup();
     }
      if (err) {
        reject(err);
      } else {
        resolve();
      }
    });
+  if (cleanupAfterAssign) cleanup();
 });

Also applies to: 371-371

🤖 Prompt for 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.

In `@src/js/internal/streams/end-of-stream.ts` around lines 165 - 172, The
synchronous-callback path can call the callback before eos() returns so the
later "cleanup" isn't initialized; to fix, declare and initialize cleanup to a
no-op before the immediateResult branch (e.g. let cleanup = () => {};), then
later overwrite cleanup with the real return value from eos(...). Update the
code paths in finished() that reference options[kEosNodeSynchronousCallback],
immediateResult, callback.$call and the eventual assignment from eos(...) so the
early return safely returns the initialized no-op cleanup.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not applicable to the current code: cleanup is initialized (let cleanup = () => { callback = nop; }) before the immediateResult branch, so the synchronous-callback path always returns an initialized cleanup.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Jarred-Sumner, you're right — I missed that let cleanup = () => { callback = nop; } is declared and initialized before the immediateResult branch, so the synchronous-callback path always returns a valid cleanup function. My concern was incorrect. Sorry for the noise!


🧠 Learnings used
Learnt from: cirospaciari
Repo: oven-sh/bun PR: 31830
File: src/js/internal/streams/iter/transform.ts:0-0
Timestamp: 2026-06-05T21:35:42.657Z
Learning: In `src/js/internal/streams/iter/transform.ts` (oven-sh/bun PR `#31830`, commit 7088bada7), the `*Sync` zlib transform variants (e.g., `makeZlibTransformSync`) are NOT buffering implementations — they stream incrementally chunk-by-chunk using node:zlib's native handle `writeSync` method plus the shared `Uint32Array` write-state, which Bun's runtime exposes identically to Node's `internalBinding('zlib')` surface. This is a direct port of Node's `makeZlibTransformSync` loop including no-copy buffer handoff rules; output is yielded as the engine produces it, not accumulated until EOF. Do NOT flag these sync transforms as O(total input) buffering implementations.

Learnt from: robobun
Repo: oven-sh/bun PR: 28430
File: src/js/internal/fs/glob.ts:67-80
Timestamp: 2026-03-22T15:00:33.251Z
Learning: For oven-sh/bun files under `src/js/internal/`, use `new Promise(...)` as the established way to construct promises. Treat `$Promise` as an intrinsic used only for prototype method access (e.g. `$Promise.prototype.$then`), not as a replacement constructor. In this directory, don’t recommend changing `new Promise(...)` to any `$`-prefixed constructor or `$Promise(...)`-style alternative.

Learnt from: robobun
Repo: oven-sh/bun PR: 28480
File: src/js/node/repl.ts:441-489
Timestamp: 2026-03-23T20:24:12.665Z
Learning: In src/js/{builtins,node,bun,thirdparty,internal}/**/*.ts, the review rule about “builtin functions must include `this` parameter typing” should be applied only to functions that are directly bound/called from native code (e.g., via mechanisms like `$newZigFunction` or similar C++/Zig interop). Do not require typed `this` for unrelated JavaScript functions such as prototype methods on function constructors (e.g., `REPLServer.prototype.defineCommand`, `Interface.prototype.question`), which should follow the existing untyped-`this` conventions used in files like `readline.ts`.

Learnt from: dylan-conway
Repo: oven-sh/bun PR: 28863
File: scripts/build/deps/webkit.ts:149-161
Timestamp: 2026-04-04T19:43:49.607Z
Learning: When reviewing Node/TypeScript code that uses `node:path.join()`, do not treat a later path segment that starts with `/` as a Windows/absolute-path override bug. `path.join()` concatenates segments and normalizes; it only resets the root when using `path.resolve()` (e.g., when it encounters an absolute-looking segment). Therefore, patterns like `join(base, "/relPath")` or `join(homedir(), env.slice(1))` where `env.slice(1)` becomes `"/WebKit"` are expected to produce `base/relPath` (cross-platform). Only flag cases where `path.resolve()` (or other root-resetting logic) is used in a way that could unintentionally ignore the base path.

Learnt from: robobun
Repo: oven-sh/bun PR: 29215
File: src/js/node/worker_threads.ts:245-305
Timestamp: 2026-04-12T08:49:31.996Z
Learning: In the JS builtin/workers area (e.g., `src/js/node/worker_threads.ts` and other `src/js` builtin context code), remember that `WeakMap` does not have intrinsic `$`-prefixed method variants (do not expect or require `.$get`, `.$set`, `.$has`, `.$delete`). Only plain `Map` supports these intrinsic `$`-prefixed variants. When reviewing `src/js` files, do not flag usage of the normal WeakMap prototype methods `.get`, `.set`, `.has`, or `.delete` as a guideline violation.

Learnt from: robobun
Repo: oven-sh/bun PR: 30268
File: test/js/bun/net/named-pipe-listen-error.test.ts:137-137
Timestamp: 2026-05-05T02:16:13.255Z
Learning: When reviewing JavaScript/TypeScript regex literals, treat `\b` as an escaped backslash followed by `b` (i.e., it matches a literal backslash and then `b`), not the regex word-boundary metacharacter. The word-boundary metacharacter is an unescaped `\b` in the source code (i.e., `\b` in the pattern string/literal syntax), which has word-boundary semantics.

So: do not flag `\b` inside a regex as a word-boundary issue by default. Only flag `\b` when the intent is to match a literal backslash+`b` and word-boundary semantics would be incorrect. Example: `/^\\\.\\pipe\\/` (as written) matches the Windows named-pipe prefix `\\.\pipe\`.

Learnt from: robobun
Repo: oven-sh/bun PR: 30936
File: test/bundler/transpiler/runtime-transpiler.test.ts:225-225
Timestamp: 2026-05-17T19:03:05.577Z
Learning: This repo (oven-sh/bun) does not enforce Biome lint rules in CI because there is no root Biome config (`biome.json` or `.biome*`). Therefore, during code review do not suggest adding `// biome-ignore` (or similar) suppression comments for Biome rule violations.

Additionally, in test files under `test/bundler/transpiler/`, do not “fix” switch-case code by wrapping intentionally-bare (unwrapped) `const` declarations in `{}` blocks when the test is specifically asserting TDZ/const-inlining behavior across sibling cases (e.g., regression tests like issue `#30932`). Adding a `{}` block can interfere with the const-prefix inliner and the single-use substitution pass, causing the test to miss the intended failure mode.

Learnt from: robobun
Repo: oven-sh/bun PR: 31201
File: scripts/strip-long-rs-comments.ts:72-74
Timestamp: 2026-05-22T05:32:25.972Z
Learning: In this repo (oven-sh/bun), .gitattributes enforces LF line endings for tracked files, so CR characters from CRLF inputs should not be present. When reviewing TypeScript code that reads text and splits lines (e.g., using `split("\n")`), don’t flag CRLF/"trailing `\r`" concerns as issues, since tracked inputs are expected to contain only `\n` line endings.

Comment thread src/js/node/_http_outgoing.ts
Comment thread src/js/node/http2.ts Outdated
Comment thread test/js/node/http/node-http-parser.test.ts Outdated
Comment thread test/js/node/stream/node-stream.test.js
Jarred-Sumner and others added 5 commits June 4, 2026 21:17
… images]

The previous commit's message described the full compat bump but the tracked
file changes were lost to a stray hard reset before committing — only the new
header made it in. This restores them:

- nodejs-headers 26.3.0 / NODE_MODULE_VERSION 147, bootstrap + flake pins,
  process.versions.v8 = 14.6.202.34-node.20
- V8 shim for 14.6: Isolate roots layout, flattened FunctionCallbackInfo exit
  frame, String Write/WriteOneByte/WriteUtf8 V2 + Utf8LengthV2 (legacy exports
  kept), External::New pointer-tag overload, Number::NewFromInt32/NewFromUint32,
  HandleScope::Extend/DeleteExtensions; export lists updated for all platforms
- napi_get_value_string_* no longer forms a slice from a null buffer pointer
  when only querying the encoded length
- v8/napi test fixtures migrated to the V2 string APIs

On top of the restoration, review fixes to the shim:

- DeleteExtensions now actually reclaims the slots Extend granted inside a
  closing scope (per-iteration v8::HandleScopes in a long native call no
  longer accumulate handles until the callback returns), with a debug assert
  against out-of-LIFO scope destruction
- WriteUtf8V2 encodes UTF-16 in chunks so the 32-bit counts from the encoder
  can't wrap for >4GB outputs, and uses view() instead of flattening ropes
- Utf8LengthV2 skips the scalar surrogate scan for valid UTF-16 (SIMD
  validate first); WriteV2/WriteOneByteV2 use vectorized copies
- FunctionCallbackInfo exit frame stays on the stack for up to 16 arguments
- version strings come from a single source of truth: REPORTED_NODEJS_V8_VERSION
  is defined from scripts/build/deps/nodejs-headers.ts and used by both
  process-report objects, alongside the existing REPORTED_NODEJS_ABI_VERSION
- _http_outgoing: drop the lazy outputData prototype accessor — reading it
  with `this` set to the prototype (spread, Object.assign, inspection)
  defineProperty'd a single shared array onto the prototype, reinstating the
  cross-instance leak it was meant to prevent, and reads on frozen instances
  threw. Like Node, the prototype now has no outputData property; methods
  lazily init for subclasses that don't chain the constructor.
- _http_outgoing: getRawHeaderNames() now reports a present-but-empty
  set-cookie under its original casing (kEmptySetCookie stores the name);
  setHeader skips the per-call toLowerCase for names that can't be set-cookie.
- ERR_HTTP2_GOAWAY_SESSION moved into the ErrorCode table instead of a
  hand-rolled Error factory. Appended at the end of the list: the table's
  discriminants are index-aligned with the checked-in Rust mirror
  (src/jsc/ErrorCode.rs). Also declared ErrorCode.ts as an input of the
  bundle-modules codegen step — error indices are baked into the bundled JS
  by replacements.ts, and an ErrorCode.ts edit previously left stale numbers
  in the bundles (errors thrown from builtins came out as the wrong code).
- CompressionStream/DecompressionStream share one buffer-source validator via
  newBufferSourceTransformPairFromDuplex instead of two verbatim copies.
- primordials: SafePromiseAllReturnVoid/ReturnArrayLike share one scheduler.
- end-of-stream: deduplicate the immediate-result callback invocation.
- node-stream.test.js: remove a byte-identical duplicate of the
  "node v26 stream semantics" describe block.
- flake.nix: fix stale Node 24 comment.
…tput [build images]

xwin's splat phase moves unpacked SDK files into place with rename(2). With
the cache under the download dir on tmpfs and /opt/winsysroot on the root
filesystem, every move fails with EXDEV ("Cross-device link"), killing the
linux image bakes. Cache next to the output instead and clean it up after.
The previous head commit came from the formatting bot, whose message lacks
the [build images] tag — the pipeline then asked for published v35 images
that don't exist yet.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 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 `@src/js/internal/webstreams_adapters.ts`:
- Around line 869-872: The validateBufferSourceChunk implementation keyed by
kValidateChunk currently only rejects SharedArrayBuffer-backed inputs but
doesn't enforce that non-SAB values are actually BufferSource types; update
validateBufferSourceChunk (kValidateChunk) to first verify the chunk is a
BufferSource by checking isArrayBufferView(chunk) or isArrayBuffer(chunk) and if
not throw $ERR_INVALID_ARG_TYPE("chunk", ["ArrayBuffer", "Buffer", "TypedArray",
"DataView"], chunk); retain the existing
isSharedArrayBuffer(isArrayBufferView(chunk) ? chunk.buffer : chunk) check and
keep throwing on SAB-backed buffers using $ERR_INVALID_ARG_TYPE. Ensure you
reference and use isArrayBufferView, isArrayBuffer, isSharedArrayBuffer and
$ERR_INVALID_ARG_TYPE in the validation.

In `@src/jsc/bindings/v8/shim/HandleScopeBuffer.cpp`:
- Around line 72-81: createRawHandleSlot currently calls createEmptyHandle()
outside the m_gcLock and then re-acquires the lock to append to m_rawGrants,
leaving a tiny race window; change it so both operations happen under one lock
by moving the createEmptyHandle() call inside the WTF::Locker scope (or provide
a createEmptyHandleWithLock helper that assumes the lock is held) and then
append { slot, m_storage.size() - 1 } before releasing m_gcLock in
HandleScopeBuffer::createRawHandleSlot to ensure atomicity; keep function
signature and return the same TaggedPointer* slot.

In `@test/js/node/http/node-http.test.ts`:
- Around line 2435-2443: The test spreads OutgoingMessage.prototype which
triggers enumerable getters (like socket) and mutates the prototype, creating
shared state; remove the unsafe spread expression (the line with "void {
...OutgoingMessage.prototype }") and either omit it entirely or replace it with
a no-op that doesn't enumerate prototype properties (e.g., a plain void 0),
leaving the checks around OutgoingMessage.prototype and the instances c/d and
their outputData assertions intact.
🪄 Autofix (Beta)

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: 3f439a28-c630-41e7-b702-de5723734229

📥 Commits

Reviewing files that changed from the base of the PR and between f6fd2e0 and bc2e101.

📒 Files selected for processing (52)
  • flake.nix
  • scripts/bootstrap.ps1
  • scripts/bootstrap.sh
  • scripts/build/codegen.ts
  • scripts/build/config.ts
  • scripts/build/deps/nodejs-headers.ts
  • scripts/build/flags.ts
  • src/js/builtins/CompressionStream.ts
  • src/js/builtins/DecompressionStream.ts
  • src/js/internal/primordials.js
  • src/js/internal/streams/end-of-stream.ts
  • src/js/internal/webstreams_adapters.ts
  • src/js/node/_http_outgoing.ts
  • src/js/node/http2.ts
  • src/jsc/ErrorCode.rs
  • src/jsc/bindings/BunProcess.cpp
  • src/jsc/bindings/BunProcessReportObjectWindows.cpp
  • src/jsc/bindings/ErrorCode.cpp
  • src/jsc/bindings/ErrorCode.ts
  • src/jsc/bindings/napi.cpp
  • src/jsc/bindings/v8/V8Array.cpp
  • src/jsc/bindings/v8/V8EscapableHandleScopeBase.cpp
  • src/jsc/bindings/v8/V8EscapableHandleScopeBase.h
  • src/jsc/bindings/v8/V8External.cpp
  • src/jsc/bindings/v8/V8External.h
  • src/jsc/bindings/v8/V8FunctionCallbackInfo.cpp
  • src/jsc/bindings/v8/V8FunctionCallbackInfo.h
  • src/jsc/bindings/v8/V8HandleScope.cpp
  • src/jsc/bindings/v8/V8HandleScope.h
  • src/jsc/bindings/v8/V8Isolate.cpp
  • src/jsc/bindings/v8/V8Isolate.h
  • src/jsc/bindings/v8/V8Number.cpp
  • src/jsc/bindings/v8/V8Number.h
  • src/jsc/bindings/v8/V8String.cpp
  • src/jsc/bindings/v8/V8String.h
  • src/jsc/bindings/v8/shim/FunctionTemplate.cpp
  • src/jsc/bindings/v8/shim/Handle.h
  • src/jsc/bindings/v8/shim/HandleScopeBuffer.cpp
  • src/jsc/bindings/v8/shim/HandleScopeBuffer.h
  • src/runtime/napi/napi_body.rs
  • src/symbols.def
  • src/symbols.dyn
  • src/symbols.txt
  • test/js/node/http/node-http.test.ts
  • test/js/node/process/process.test.js
  • test/js/node/stream/node-stream.test.js
  • test/napi/napi.test.ts
  • test/napi/node-napi-tests/harness.ts
  • test/v8/bad-modules/mismatched_abi_version.cpp
  • test/v8/bad-modules/no_entrypoint.cpp
  • test/v8/v8-module/main.cpp
  • test/v8/v8.test.ts
💤 Files with no reviewable changes (1)
  • test/js/node/stream/node-stream.test.js

Comment thread src/js/internal/webstreams_adapters.ts
Comment thread src/jsc/bindings/v8/shim/HandleScopeBuffer.cpp
Comment thread test/js/node/http/node-http.test.ts
Comment thread src/js/node/_http_outgoing.ts
…ild images]

The private S3 mirror has no v26.3.0 musl tarballs, which 403'd the Alpine
image bakes. nodejs/unofficial-builds publishes both linux-x64-musl and
linux-arm64-musl for current releases (the mirror predates arm64-musl being
available there), so pull straight from it and skip the manual re-upload on
every Node bump.
Comment thread src/jsc/bindings/v8/V8EscapableHandleScopeBase.cpp Outdated
Comment thread src/js/node/_http_outgoing.ts
…ild images]

- process.config.variables: add enable_thin_lto=false and lto_jobs="" to
  match Node 26. Its common.gypi evaluates these in MSVS settings conditions
  and gyp hard-fails on undefined variables, breaking every node-gyp build on
  Windows (v8/napi/dlopen suites).
- The cipher streaming tests assumed read() with no size returns the whole
  buffered ciphertext; since Node 26 it returns one chunk at a time, so the
  fixtures truncated the ciphertext and OpenSSL reported BAD_DECRYPT — real
  Node 26 fails the old fixtures identically. Drain read() in a loop and add
  the second cipher data event Node 26 emits (output verified against Node
  26.3.0 byte-for-byte modulo quote style); drop the stale TODO claiming the
  readable/prefinish order was a bug.
…tions [build images]

Deliberate divergence from Node 26 (nodejs/node#62557 made pause()/resume()
early-return on destroyed streams). Legacy Readable subclasses like fd-slicer
assign `this.destroyed = true` — which hits the prototype setter on modern
streams — right before push(null). With the upstream guard, a piped
destination's 'drain' can no longer resume the source, so the last buffered
chunk is silently dropped and the pipeline never finishes. In practice that
breaks yauzl → extract-zip → puppeteer browser installs and other zip/tar
tooling, and the same hang reproduces on Node 26.3.0 itself.

Keep the Node 24 behavior of letting such streams flush their buffer, and
replace the no-op assertion test with a regression test for the fd-slicer
pattern (verified: extract-zip now extracts a full chrome-headless-shell
archive instead of stopping after the first entries).
Comment thread test/js/node/stream/node-stream.test.js Outdated
…ed [build images]

The dev-server-puppeteer launcher already prefers a system Chromium when one
is installed (CI bootstraps one on every Linux flavor), and several CI
platforms have no Chrome for Testing build at all (linux-arm64,
windows-arm64). The unconditional download during bun install wasted ~150MB
per run and, worse, a half-finished download left in the shared agent cache
by an earlier failed run makes @puppeteer/browsers refuse every later
install ("browser folder exists but the executable is missing"). Set
PUPPETEER_SKIP_DOWNLOAD for the install step when a system browser exists or
the platform can't run the downloaded one.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/integration/next-pages/test/dev-server.test.ts (1)

8-8: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Remove unused imports.

isCI and isWindows are imported but never used. The code checks process.env.CI and process.platform === "win32" directly instead. Remove these from the import list.

Cleanup
-import { bunEnv, bunExe, isCI, isWindows, tmpdirSync, toMatchNodeModulesAt } from "../../../harness";
+import { bunEnv, bunExe, tmpdirSync, toMatchNodeModulesAt } from "../../../harness";
🤖 Prompt for 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.

In `@test/integration/next-pages/test/dev-server.test.ts` at line 8, The import
list includes unused symbols isCI and isWindows; remove them from the import
statement that currently imports bunEnv, bunExe, isCI, isWindows, tmpdirSync,
toMatchNodeModulesAt so only the actually used imports remain (bunEnv, bunExe,
tmpdirSync, toMatchNodeModulesAt) and run tests/lint to ensure no other
references to isCI/isWindows exist.
🤖 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/integration/next-pages/test/next-build.test.ts`:
- Around line 13-25: The duplicated environment-detection logic for skipping
Puppeteer browser downloads (the hasSystemChromium, skipBrowserDownload and
puppeteerInstallEnv logic) should be extracted into a shared helper to avoid
drift; add a new exported function getPuppeteerInstallEnv in the test harness
(e.g., harness.ts) that encapsulates the checks and returns the env dict, then
replace the in-file variables (hasSystemChromium, skipBrowserDownload,
puppeteerInstallEnv) in next-build.test.ts (and the corresponding blocks in
dev-server.test.ts and dev-server-ssr-100.test.ts) with a single call to
getPuppeteerInstallEnv().

---

Outside diff comments:
In `@test/integration/next-pages/test/dev-server.test.ts`:
- Line 8: The import list includes unused symbols isCI and isWindows; remove
them from the import statement that currently imports bunEnv, bunExe, isCI,
isWindows, tmpdirSync, toMatchNodeModulesAt so only the actually used imports
remain (bunEnv, bunExe, tmpdirSync, toMatchNodeModulesAt) and run tests/lint to
ensure no other references to isCI/isWindows exist.
🪄 Autofix (Beta)

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: db7f406d-96b8-472b-9b7c-b8282ef63d02

📥 Commits

Reviewing files that changed from the base of the PR and between 6abf57e and 95d7107.

📒 Files selected for processing (3)
  • test/integration/next-pages/test/dev-server-ssr-100.test.ts
  • test/integration/next-pages/test/dev-server.test.ts
  • test/integration/next-pages/test/next-build.test.ts

Comment thread test/integration/next-pages/test/next-build.test.ts Outdated
…, goaway semantics [build images]

- v8: reserve the EscapableHandleScope escape slot at construction (like real
  V8) instead of allocating it inside Escape(). Allocated at Escape() time it
  sits above any in-scope inline handle grants, so the inline ~HandleScope's
  DeleteExtensions swept the just-escaped handle together with the scope. The
  reservation lives in a side registry keyed by the scope's address because
  the V8 ABI leaves exactly one usable word in the object; entries are purged
  when their buffer clears, and a reused stack address overwrites its stale
  entry. New fixture test creates inline Local copies inside the scope before
  escaping. Also: createRawHandleSlot now does its two bookkeeping steps under
  one lock.
- _http_outgoing: the headers getter/setter, appendHeader and
  getRawHeaderNames now agree with the kEmptySetCookie marker — the getter
  mirrors getHeaders(), replacing the header bag or appending a cookie drops
  the marker, and getRawHeaderNames can't report set-cookie twice.
- FetchHeaders.getRawKeys: the HTTPHeaderMap iterator only walks the common
  and uncommon segments while size() also counts set-cookie values, so the
  returned array had trailing holes whenever set-cookie was present; size for
  unique names and append "Set-Cookie" explicitly.
- http2: receiving GOAWAY now mirrors Node — NGHTTP2_NO_ERROR begins a
  graceful close() (request() then throws ERR_HTTP2_GOAWAY_SESSION), any
  other code destroys the session with ERR_HTTP2_SESSION_ERROR. The
  rejected-stream test asserted the old behavior; its expectation now matches
  what a real Node 26 client reports for the same goaway.
- tests: hoist the _http_common require to module scope, drop a redundant
  in-test require, probe the prototype's outputData directly instead of
  spreading the prototype (which invoked unrelated deprecated getters), and
  share the puppeteer skip-download env via a harness helper.
Comment thread scripts/build/deps/nodejs-headers.ts
Comment thread test/napi/node-napi-tests/harness.ts
Comment thread src/jsc/bindings/v8/V8HandleScope.cpp
…uest [build images]

Two fallouts from mirroring Node's GOAWAY handling surfaced by CI:

- After a graceful close(), the socket dying left active streams hanging
  forever: #onClose ran stream.close(NGHTTP2_CANCEL) and then detached the
  parser, so the writable side could never finish and 'close' never fired
  (test-http2-server-shutdown-options-errors timed out). Mirror Node's
  closeSession: hard-destroy every stream that survives the cancel pass, on
  both client and server sessions.
- A request created with an already-aborted signal went through the native
  parser, which RST'd a stream the peer never saw — a connection error that
  makes conforming servers reply GOAWAY(INTERNAL_ERROR). The old code
  swallowed that session error; now that goaway destroys the session with
  ERR_HTTP2_SESSION_ERROR like Node, the noise became visible. Like Node,
  never touch the wire: create an id-less stream and destroy it with an
  AbortError on the next tick (the stream reports aborted=true and rstCode
  CANCEL but does not emit 'aborted' since the request never started).

Also for CI on Windows: pass -Denable_lto=false -Denable_thin_lto=false
-Dlto_jobs= to node-gyp in the v8/napi/dlopen test builds. A clang-cl-built
Node 26 reports thin-LTO build flags in process.config, node-gyp copies them
into addon builds, and MSVC's link.exe hard-fails on /opt:lldltojobs. The
defines are no-ops elsewhere. And when puppeteer's browser download can't be
skipped, install into a fresh per-run PUPPETEER_CACHE_DIR (forwarded to the
launcher) so a half-extracted download left in the shared agent cache by an
earlier failed run can't block the install.
The exported ~HandleScope popped the Bun scope stack and cleared its buffer
but left the isolate's HandleScopeData alone. If Extend had granted slots
from that buffer while the scope was current (an addon's inline Local::New
inside e.g. an Array::Iterate callback), next/limit kept pointing into the
cleared buffer; the next inline v8::HandleScope then snapshotted that stale
limit as prev_limit_, and its DeleteExtensions ran deleteGrantsBack against
the enclosing buffer where no grant matches a foreign address — popping every
grant, including handles of still-open outer scopes. Snapshot next/limit at
scope push (stored in the scope's buffer cell — the 3-word V8 ABI leaves no
room in the frame itself) and write them back on pop. New fixture test drives
exactly this through Array::Iterate.

Also gate the duckdb smoke test on NODE_MODULE_VERSION <= 137: the deprecated
duckdb package publishes no prebuilts past ABI 137 (checked npm.duckdb.org
for 1.3.1-1.4.1), and node-pre-gyp's fallback source build is not viable in
CI. The gate can go away if the test migrates to the N-API based
@duckdb/node-api.
Comment thread src/js/node/http2.ts Outdated
Matches node's ordering and semantics (verified against v26.3.0):
validateAbortSignal accepts any object with an 'aborted' property, so a
duck-typed { aborted: true } takes the abort fast path — but objects
without 'aborted' and non-objects throw ERR_INVALID_ARG_TYPE
synchronously, before the fast path can read .aborted.
Azure repeatedly failed to allocate Standard_D4ds_v6 in-region
(AllocationFailed) during image bakes; D4as_v7 is one of the alternatives
Azure's allocation guidance suggests. Build-only VM — CI runner sizes are
unchanged in ci.mjs.
A build without the [build images] tag fell back to published v35 images,
and stale v35 alpine images exist from before the Node 26.3.0 install landed
(bootstrap already said Version: 35 while still installing 24.3.0) — agents
came up with Node 24, failing the version check and building ABI-137 addons
that an ABI-147 runtime rightly refuses to load. Move to v36/v21 so the
poisoned tags can never be selected again.
Comment thread src/js/node/http2.ts Outdated
…r bun [build images]

- Http2Session.close() (client and server) now sends GOAWAY immediately like
  Node (core.js marks the session closed and calls goaway() right away), so
  peers stop routing new work to a draining session; the client goaway()
  gained Node's default arguments. The grpc-js unbind test additionally
  accepts CANCELLED: a call that wins the race onto the draining session
  before the GOAWAY/socket-close is processed is torn down with
  NGHTTP2_CANCEL — the same in-flight teardown Node performs at socket close;
  which of the three statuses you get is pick-timing.
- Run every test-suite node-gyp build under bun (--bun): the gyp -D defines
  could not neutralize the thin-LTO flags a clang-cl-built Node 26 carries in
  process.config.target_defaults (node-gyp copies them into config.gypi and
  MSVC's link.exe fails on /opt:lldltojobs), and the system Node's ABI may
  not match ours at all. Bun reports the same ABI (147) with clean
  target_defaults, so the produced modules load in node 26 unchanged. The
  dlopen fixtures invoke the bun under test explicitly and their beforeAll
  hooks get a real timeout — the builds legitimately exceed the 5s default
  under a debug/ASAN binary.
Same rationale as the previous commit's dlopen/napi change — this invocation
was missed: gyp -D defines can't override the thin-LTO flags a clang-cl-built
Node carries in process.config.target_defaults, so build the node-side module
with bun (--bun) as well; it targets the same ABI (147) and loads in node 26
unchanged. Also drop the now-redundant defines from the bun-side invocation.
Comment thread src/js/node/http2.ts
…ness [build images]

common.gypi only applies CLANG_CXX_LANGUAGE_STANDARD (gnu++20) to addon
builds when the clang variable is 1, which real Node reports on macOS. With
bun running node-gyp and reporting clang=0, Apple clang compiled addons at
its ancient default standard and every node-gyp build on darwin failed on
<type_traits> (std::is_enum_v) in the Node 26 headers.

The v8 harness now includes the child's exit code in crash messages — the
Windows fixture crashes currently report nothing, and the code
distinguishes an access violation from a DLL load failure.
sharp has no win32-arm64 prebuilt, so its install script falls back to a
node-gyp source build under the system Node — which the clang-cl-built
Node 26 breaks (process.config leaks thin-LTO flags that MSVC's link.exe
rejects with LNK1117). The test exercises package-lock.json migration, not
lifecycle scripts, so pass --ignore-scripts there.
…[build images]

The puppeteer-based next-pages tests still hit flaky per-run Chrome for
Testing downloads on ubuntu/debian x64 (the only CI platforms with neither a
system browser nor a skip gate). Bake google-chrome-stable into those images
and teach the browser lookups about its binary names — with a system browser
present the tests skip the ~300MB download entirely and launch the system
binary, like they already do on alpine.
Comment thread src/js/node/http2.ts
These were in symbols.txt/.dyn (so Linux and macOS resolved them) but never
in the Windows .def. Node addons delay-load the host's exports, so the gap
only surfaces at the first call: the v8 fixture tests that exercise these
type checks died with the delay-load "procedure not found" SEH status
(0xC06D007F, reported as exit code 127) after printing their first lines.
Manglings verified against clang-cl and the remaining 83 v8 exports audited
for further gaps (none).
…[build images]

The exit-127 crashes on windows-aarch64 print nothing: stdout shows the test
started and stderr is empty. The exe exports every referenced v8 symbol
(checked both pre- and post-61197 artifacts against the addon's full import
set), so the delay-load theory needs narrowing. Markers go to stderr, which
checkSameOutput never compares but does include in crash messages, so the
next CI run names the exact call that dies.
Comment thread test/v8/v8-module/main.cpp Outdated
Root cause of the Windows --debug fixture crashes (exit 127), found by
reproducing under wine with the CI binary and a clang-cl-built debug addon:
V8_INLINE members whose bodies live at namespace scope in the headers —
HandleScope::CreateHandle(v8::Isolate*, Address), HandleScope::Initialize,
and Value::QuickIsUndefined/QuickIsNull/QuickIsNullOrUndefined/QuickIsString
— are imported rather than emitted by MSVC debug builds of a dllimport
class. Real Node exports its entire V8 surface so this works there; Bun's
curated export list didn't include them, and the delay-load helper's
"procedure not found" SEH status (0xC06D007F) surfaces as exit code 127 at
the first call. That also explains the seemingly random crash points across
tests: each died at its own first imported-inline call.

Implementations are thin: the CreateHandle overload forwards to the
internal::Isolate* variant (same object), Initialize performs V8's inline
frame setup (snapshot next/limit, level++ — never pushing a Bun scope, like
EscapableHandleScopeBase), and the QuickIs* checks are the corresponding
JSC value checks. Exported on all platforms for symmetry; ELF builds emit
inline copies locally so only Windows strictly needs them. Verified under
wine: with the missing import resolved, test_v8_global,
test_v8_escapable_handle_scope_inline_grants and
test_v8_locals_survive_nested_call all pass with a debug-CRT fixture.
…d images]

A graceful close() sends GOAWAY through the JS default lastStreamId = 0, and
the native parser only fell back to the session's real last stream id for
undefined/null — a literal 0 went on the wire, telling the peer per RFC 9113
§6.8 that no streams were processed and everything is retriable. Node's JS
layer has the same 0 default and relies on its native correction
(lastStreamID <= 0 → nghttp2_session_get_last_proc_stream_id); mirror that
in the parser so every caller (close(), destroy(), user goaway) is covered.

Also: the server session's goaway handler now matches the client's (and
Node's onGoawayData) — NO_ERROR begins a graceful close, any other code
destroys with ERR_HTTP2_SESSION_ERROR; corrected a comment that claimed
duck-typed { aborted } signals throw (validateAbortSignal accepts any object
with an 'aborted' property); and stripped the v8 fixture's temporary stderr
step markers now that the Windows exit-127 is root-caused.
@cirospaciari

Copy link
Copy Markdown
Member

Superseded by #31991, which merges current main (resolving the bootstrap.ps1 conflict and picking up the baked-in Intel SDE that fixes verify-baseline) and fixes the remaining red jobs: ABI-matching node resolution + toolchain capability skip for the macOS boxes, and a deterministic-finalizer fix for the napi abort on windows-11-aarch64.

cirospaciari added a commit that referenced this pull request Jun 16, 2026
Supersedes #31818 — same change set, rebased onto current main with
conflicts resolved and the remaining CI failures fixed.

## What

Bumps the Node.js compatibility target from 24.3.0 to 26.3.0 (V8
`14.6.202.34-node.20`, `NODE_MODULE_VERSION` 147), and syncs
`node:stream`/`node:http` with the behavioral changes upstream made
between v24.x and v26.x.

## Native / addon ABI

- Headers, bootstrap pins, flake, `process.versions` updated
- V8 shim updated for 14.6: Isolate roots layout, flattened
`FunctionCallbackInfo` exit frame, `String` `Write*V2`/`Utf8LengthV2`
(legacy exports kept for older binaries), `External::New` pointer-tag
overload, `Number::NewFromInt32/NewFromUint32`,
`HandleScope::Extend/DeleteExtensions` — with Itanium + MSVC symbol
exports
- Fixed a `napi_get_value_string_*` panic when querying length with a
null buffer
- v8/napi fixtures migrated to the new header APIs

## stream/http sync

Audited every behavioral diff in upstream `lib/internal/streams/*`,
`lib/_http_*`, webstreams adapters, and http2 between v24.3.0 and
v26.3.0, and ported the ones we were carrying v24 (or divergent)
behavior for — highlights:

- `Writable.toWeb` sync-drain hang fix; `fromWeb` writev rejection no
longer crashes the error path
- `read()` one-chunk semantics in paused mode (semver-major in 26),
`compose()` on the prototype
- `Duplex.from(async gen)` destroy-during-idle hang fix; BYOB
`Readable.toWeb({type:'bytes'})`; DEP0201
- `writeHeader` removed (DEP0063 EOL); upgrade requests without a
listener no longer disappear; `set-cookie` getHeader/setHeader edge
cases; http2 `respond()` raw-array rejection and session error codes
- Also fixes a pending BYOB `reader.read()` never settling after
`reader.cancel()` (spec step 5)

Vendored the matching upstream tests; updated v24-era vendored tests
whose expectations legitimately changed and verified each new
expectation against real Node 26.3.0.

## New in this PR (vs #31818)

The remaining red CI jobs on #31818 all came from test machines whose
environment lags the new ABI, plus one GC-timing-dependent fixture.
Fixed here:

- **Merged current main** (only conflict: the bootstrap.ps1 version
counter). This alone fixes the `windows-x64-baseline-verify-baseline`
failure: main now bakes Intel SDE into the Windows image (#31893)
instead of downloading it at job time, which the Intel mirror blocks.
- **`test/harness.ts`: `nodeExeMatchingAbi()`** — returns the system
`node` when its `process.versions.modules` matches what Bun reports;
otherwise downloads the matching official Node build once into a cached
temp directory (SHA-256-verified against the release SHASUMS).
`test/v8/v8.test.ts` and `test/napi/napi.test.ts` resolve `node` through
it, so the bun-vs-node comparison no longer fails with
`ERR_DLOPEN_FAILED` on machines that still have Node 24 installed (the
three darwin `v8.test.ts` failures on #31818).
- **`test/harness.ts`: `canBuildNodeAddons()`** — macOS-only probe for a
libc++ that ships `<source_location>`, which Node 26 headers include
unconditionally (real Node 26 has the same minimum-Xcode requirement).
Addon-compiling suites (`v8`, `napi`, the two `process.dlopen` tests)
skip on machines whose toolchain cannot compile the headers at all (the
`macOS-13` Intel box failures on #31818).
- **napi fixture: pin the `test_deferred_exceptions` wrapped object**
with a strong ref so its `napi_throw`-ing finalizer runs at env teardown
instead of from GC. The fixture builds with `NAPI_EXPERIMENTAL`, and
Node 26 aborts (`Finalizer is calling a function that may affect GC
state`) whenever GC happens to collect the object mid-process — that's
the windows-11-aarch64 `napi.test.ts` failure on #31818, reproducible on
Linux with `--expose-gc`. Pinning makes the finalizer timing
deterministic on both runtimes with unchanged expected output.

- **http2: close the stream when a headers-only END_STREAM response
completes** (`h2_frame_parser.rs`). The `respond()`/`request()` header
path set the stream to HALF_CLOSED_LOCAL unconditionally — regressing it
backwards when the peer had already half-closed — and never dispatched
`onStreamEnd`, unlike the `send_data`/`send_trailers` paths. A
`res.end()` with no body therefore leaked the stream and the session's
connection count; a session marked closed by a received GOAWAY then
waited forever for a destroy that couldn't come, and `server.close()`
hung. Linux masked the leak (the peer's FIN tears the socket down); the
macOS fleet doesn't redeliver that FIN once the session has written from
inside the read callback, which is why only darwin hung. This was the
`test-http2-compat-serverrequest-pipe.js` timeout — diagnosed with
instrumented CI builds (request/response completed, `streamEnd(7)` never
dispatched, server connection count stuck at 1), and verified fixed on
the same darwin agents that failed 4/4 before.
- **http2: destroy the server session when its socket closes**
(`http2.ts`), mirroring Node's `socketOnClose` and the client session's
own `#onClose` — `close()` alone early-returns once a received GOAWAY
has marked the session closed.

There is also a latent macOS kqueue FIN-redelivery quirk in uSockets
(observed with kernel-event tracing: after the h2 session writes from
inside the socket's data callback, the pending EOF event for that socket
is never redelivered). The stream-state fix makes it unobservable here;
filing it as a follow-up issue.

## Verification

Local (Linux x64 debug build, system Node 26.3.0):

- `test/napi/napi.test.ts`: 99 pass / 0 fail (including the previously
aborting fixture under forced GC, verified directly with `node
--expose-gc`)
- `test/v8/v8.test.ts`: 52 pass / 1 skip / 0 fail
- both `process.dlopen` suites: 5 pass / 0 fail
- `node-http2`, `node-stream`, `node-stream-uint8array`,
`web/streams/compression`: 362 pass / 1 fail — the failure is the `http2
maxSessionMemory` timeout that #31818 documents as debug-build-only and
reproduced at its base
- `test-http2-compat-serverrequest-pipe.js` (timed out once on darwin in
#31818): passes 10/10 consecutive local runs
- `nodeExeMatchingAbi()` download path exercised end-to-end by hiding
the system node from `PATH`

## Known-unrelated failures

Same as #31818: ASAN-debug only, reproduced identically at base —
`BIO_ctrl` SEGV on aggregate http test runs, http2 `maxSessionMemory`
timeout, streams-leak absolute-RSS bound.

## Not in scope (follow-ups)

New Node 26 APIs (`fs.Utf8Stream`, `req.signal`, `writeInformation`,
http2 options), full env-proxy support, upgrade-with-body
`UpgradeStream`, flag-gated `stream/iter` family.
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.

3 participants