Conversation
|
Updated 11:25 PM PT - Jul 9th, 2026
❌ @robobun, your commit 498f8ff has 6 failures in
🧪 To try this PR locally: bunx bun-pr 33304That installs a local version of the PR into your bun-33304 --bun |
|
Found 1 issue this PR may fix:
🤖 Generated with Claude Code |
|
Not adding #31767 is about the The The two PRs touch adjacent lines in |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
|
Not a duplicate of #31768. The two are adjacent in What #31768 does: early-returns Node's "never started" shape from the What this PR does:
Neither PR makes the other redundant, and they compose: after both land, a failed spawn returns They do both edit the |
WalkthroughThis PR updates Bun’s Node.js Changeschild_process spawn and spawnSync fixes
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
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)
src/js/node/child_process.ts (1)
496-514: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winKeep
outputaligned in the batch-file error path. This early return still hardcodes 3 slots, sospawnSync()can return a shorteroutputarray than the caller’s normalizedstdiowhenwindowsBatchFileErrorhits with extra descriptors.🤖 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/node/child_process.ts` around lines 496 - 514, The `spawnSync()` early return in the `windowsBatchFileError` branch is hardcoding a 3-entry `output` array, which can get out of sync with the normalized `stdio` length. Update the `spawnSync`/`windowsBatchFileError` path in `child_process.ts` to build `output` using the same normalized stdio size as the rest of `spawnSync()` instead of returning a fixed three-slot array, while keeping the rest of the error object unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/js/node/child_process/child_process.test.ts`:
- Around line 377-395: The “stdio that is neither a string nor an array” test
only covers spawn, but it should also verify spawnSync since both paths use
normalizeStdio and should throw the same ERR_INVALID_ARG_VALUE. Update the
child_process test case to add a spawnSync expectation alongside the existing
spawn assertion, using the same invalid stdio value and matching error shape as
the neighboring “unknown stdio string” test.
---
Outside diff comments:
In `@src/js/node/child_process.ts`:
- Around line 496-514: The `spawnSync()` early return in the
`windowsBatchFileError` branch is hardcoding a 3-entry `output` array, which can
get out of sync with the normalized `stdio` length. Update the
`spawnSync`/`windowsBatchFileError` path in `child_process.ts` to build `output`
using the same normalized stdio size as the rest of `spawnSync()` instead of
returning a fixed three-slot array, while keeping the rest of the error object
unchanged.
🪄 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: d4263a40-88f9-4c71-bb56-2a33f38273ad
📒 Files selected for processing (2)
src/js/node/child_process.tstest/js/node/child_process/child_process.test.ts
|
Thanks @coderabbitai. Took the inline one, declining the outside-diff one. Taken: Declined: padding That branch is a pre-spawn failure: the child never starts, and Node reports a never-started child with It also isn't mechanically possible where the suggestion puts it: The |
There was a problem hiding this comment.
No issues found — the changes look correct and well-tested, but swapping the direct close emit for #maybeClose() in the deferred-error path relies on #closesNeeded staying at 1 there, which is a non-local invariant worth a maintainer's confirmation given how many packages depend on this event contract.
Extended reasoning...
Overview
Three targeted Node-compat fixes in src/js/node/child_process.ts plus tests:
- The deferred spawn-error branch (ENOENT/EACCES/EAGAIN/EMFILE/ENFILE) now sets
this.exitCode = errnoand routesclosethrough#maybeClose()instead of emitting it directly with one argument, so listeners receive(errno, null)andchild.exitCodeis populated — matching Node's_handle.onexit(err)behavior. spawnSync().outputis padded withnullfor stdio slots beyond stderr sooutput.length === stdio.length.normalizeStdio()throwsERR_INVALID_ARG_VALUEinstead of the retiredERR_INVALID_OPT_VALUE, and the dead helper is deleted.
Security risks
None. No untrusted-input parsing, auth, crypto, or filesystem-path handling is introduced; the changes only adjust event arguments, an output-array length, and an error code on paths that already existed.
Level of scrutiny
Moderate. Changes #2 and #3 are mechanical and low-risk. Change #1 is small but semantically meaningful: it replaces a direct emit("close", errno) with #maybeClose(), which only fires when #closesGot === #closesNeeded. I traced the invariant — in the deferred-error path #handle is nulled before any stdio getter can create a real pipe (so the #closesNeeded++ in #getBunSpawnIo never runs), and the IPC increment only executes after Bun.spawn() succeeds — so #closesNeeded stays at 1 and close fires. The PR description confirms this via test-child-process-spawn-error.js, which touches .stdin/.stdout/.stderr/.stdio synchronously on a failed spawn. Still, child_process event lifecycle is a load-bearing Node-compat surface that a lot of the npm ecosystem sits on, so a maintainer familiar with this file should sign off.
Other factors
- Bug-hunting system found nothing; CodeRabbit's one nit (spawnSync coverage for non-string/non-array stdio) was addressed in 17a1a04 and the thread is resolved.
- Tests follow harness conventions (
tempDir,bunEnv, event-driven awaits, exact-value assertions) and include fail-before/pass-after verification. - The author has already flagged a one-line rebase interaction with #31768 in the
spawnSyncresult block; whichever lands second will need to reconcile. - No CODEOWNERS entry for this path.
|
Fair call on the The risk is narrow: That reasoning is now a test. If someone later adds a |
There was a problem hiding this comment.
I didn't find any bugs, but the switch from a direct emit("close", errno) to #maybeClose() on the deferred-error path makes close firing depend on #closesNeeded staying at 1 when Bun.spawn throws — I traced it and the tests cover it, but it's worth a maintainer's eye given how many packages depend on child_process failure semantics.
Extended reasoning...
Overview
Three targeted Node-compat fixes in src/js/node/child_process.ts, all with new tests in test/js/node/child_process/child_process.test.ts:
- Deferred spawn-error lifecycle (the headline change): when
Bun.spawnthrows a deferred error (ENOENT/EACCES/EAGAIN/EMFILE/ENFILE), the catch block now setsthis.exitCode = errnoinside thenextTickand routesclosethrough#maybeClose()instead of emitting it directly with a single argument. This makesclosefire with(errno, null)instead of(errno, undefined)and populateschild.exitCode. spawnSync().outputlength: pads the output array withnullfor stdio slots beyond index 2, sooutput.length === stdio.length.- Retired error code:
normalizeStdio()now throws$ERR_INVALID_ARG_VALUEinstead of the localERR_INVALID_OPT_VALUEhelper (removed from Node in v15). The dead helper is deleted; I confirmed it had no other callers in this file.
Security risks
None. No untrusted input parsing changes, no privilege/permission logic. The changes only affect error-code strings, event-argument shape, and array padding with null.
Level of scrutiny
Medium-high. node:child_process spawn-failure semantics are load-bearing for process supervisors, task runners, and CI tooling. The subtle part is change (1): previously close was emitted unconditionally; now it goes through the #closesNeeded/#closesGot counter. I traced the failure path — #closesNeeded starts at 1, the IPC increment is inside the try after Bun.spawn succeeds so it never runs, and the stdio getters don't increment when #handle is null (they short-circuit to destroyed streams / null) — so the single #maybeClose() call correctly emits close. The new "close still fires after the stdio streams are materialized" test locks this in for both default stdio and an extra pipe, and the PR description reports Node's ported test-child-process-spawn-error.js still passes. That said, this is exactly the kind of counter-based event bookkeeping where a maintainer with domain context should confirm no other path can bump #closesNeeded before the nextTick.
Other factors
- The bug hunter found nothing.
- The one CodeRabbit inline nit (add
spawnSynccoverage for non-string/non-array stdio) was addressed in 17a1a04 and the thread is resolved. - No CODEOWNERS for these files.
- The PR author flagged an adjacent-line interaction with #31768 (the
spawnSyncfailed-spawn result shape); whichever lands second needs a small rebase of theconst result = { ... }block. - Changes (2) and (3) are mechanical and low-risk on their own.
CI status: the diff is green, the red lanes are repo-wide flake(Updated for builds 71295 and 71315 on the rebased branch.) Build 71315 on
The previous run, build 71295, had 276 pass / 2 fail (same The postgres/mysql failures are Verification recapBoth review bots are satisfied (claude[bot]: "no bugs found"; CodeRabbit: "No actionable comments"), and all review threads are resolved. Ready for a maintainer. The one thing worth a second pair of eyes, raised by claude[bot] and now covered by the |
A spawn that fails with ENOENT/EACCES/EAGAIN/EMFILE/ENFILE now sets `exitCode` to the negative errno and routes 'close' through `#maybeClose()`, so the event carries `(exitCode, signalCode)` instead of just the errno. The signal argument is now `null` rather than `undefined`. `spawnSync().output` gets one entry per stdio slot instead of always three, and stdio validation throws node's `ERR_INVALID_ARG_VALUE` instead of the retired `ERR_INVALID_OPT_VALUE`.
392cf34 to
8209c7e
Compare
|
Rebased onto The The same 3 container-environment failures in the full file ( |
There was a problem hiding this comment.
No bugs found. The change is small and well-tested, but routing the deferred spawn-error path through #maybeClose() depends on #closesNeeded staying at 1 when #handle is null — the author already flagged that as worth a maintainer glance, and I agree.
Extended reasoning...
Overview
Three Node.js compatibility fixes in src/js/node/child_process.ts, plus tests in test/js/node/child_process/child_process.test.ts:
- The deferred spawn-error branch (ENOENT/EACCES/EAGAIN/EMFILE/ENFILE) now sets
this.exitCode = errnoand emitsclosevia#maybeClose()so listeners receive(errno, null)instead of(errno, undefined), matching Node's_handle.onexitcontract. spawnSync().outputis padded withnullfor every stdio slot past index 2, sooutput.length === stdio.length.normalizeStdio()throwsERR_INVALID_ARG_VALUEinstead of the retiredERR_INVALID_OPT_VALUE; the local helper is deleted.
Security risks
None. No auth, crypto, path handling, or untrusted-input parsing is touched. The changes only adjust event arguments, an output-array shape, and an error code on already-validated inputs.
Level of scrutiny
Moderate. child_process is widely used and the deferred-error path is exactly what process supervisors observe. Changes (2) and (3) are mechanical. Change (1) is small but relies on a non-local invariant: #maybeClose() only fires when #closesGot === #closesNeeded, so correctness depends on nothing bumping #closesNeeded when #handle is null. I traced it — the only increment is in #getBunSpawnIo case 1/2 after the if (!value) guard, which short-circuits when handle is null; the IPC += 1 is inside the successful-spawn try block. The new "close still fires after the stdio streams are materialized" tests encode that invariant for both default stdio and an extra pipe.
Other factors
- Fail-before / pass-after verified in the PR body and again after rebase.
- CI green on all lanes that run
child_processtests; the 4 red lanes were shown to be unrelated (artifact-download timeout, a pty test, anode:netGC flake also failing on other branches). - CodeRabbit's only nit (add
spawnSyncto the non-string/non-array stdio test) was applied; its outside-diff suggestion was correctly declined with reasoning. - No CODEOWNERS entry for this path.
I'm not auto-approving because the author themselves called out the #maybeClose() routing as the one thing worth a second pair of eyes, and it's the kind of private-state coupling a maintainer should sign off on rather than a bot.
|
Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-10, it conflicts with main, and its last CI run failed. This is not a judgment on the fix itself. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main. |
Repro
Cause
All three live in
src/js/node/child_process.ts.1. Spawn-failure lifecycle. The deferred-error branch in
ChildProcess#spawn()emittedclosedirectly with a single argument:so listeners got
signal === undefined, andthis.exitCodewas never assigned. Node's_handle.onexit(err)setsthis.exitCode = err(the negative errno) and then callsmaybeClose(), which emitsclosewith(this.exitCode, this.signalCode). The divergence is not ENOENT-specific; it covers every code in the deferred list (EACCES givesclose(-13, undefined)withexitCode === null).This matters for supervision code, which is exactly the code that reads these fields:
if (signal === null && code !== 0)takes the wrong branch onundefined, andchild.exitCode ?? fallbackhides the errno.2.
spawnSync().output. The result was hardcoded to three entries regardless of how many stdio slots the caller asked for, sooutput.lengthdisagreed withstdio.lengthand any slot past stderr vanished.3. Retired error code.
ERR_INVALID_OPT_VALUEwas removed from Node in v15. Node'sgetValidStdio/stdioStringToArraythrowERR_INVALID_ARG_VALUEfor an unknown stdio string and for a stdio value that is neither a string nor an array. (validateMaxBuffer/validateTimeoutalready used$ERR_OUT_OF_RANGEcorrectly.)Fix
this.exitCode = errnoinside thenextTick(same ordering as Node: theerrorlistener already sees it) and emitsclosethrough#maybeClose(), which supplies(exitCode, signalCode).signalCodestaysnull, and noexitevent is emitted, matching Node.spawnSync()buildsoutputwith one entry per normalized stdio slot. Slots the parent does not read from arenull, which is what Node reports forignore/inherit/fd entries.normalizeStdio()throws$ERR_INVALID_ARG_VALUE, whose message is already byte-identical to Node's (The argument 'stdio' is invalid. Received 'bogus'). The now-dead localERR_INVALID_OPT_VALUEhelper is deleted.Not fixed here
A literal
pipeatstdio[3]or beyond still reportsnullinoutput, becauseBun.spawnSyncdoes not buffer the extra pipes (the pre-existingTODOaboveoutput). That slot also deadlocks today once the child writes more than the pipe buffer, so closing the gap properly needs native work inBun.spawnSyncrather than a change in this layer. Every other stdio shape (ignore,inherit, fd numbers) now matches Node exactly.Verification
New tests in
test/js/node/child_process/child_process.test.ts:spawn() failure lifecycle > ENOENT/> EACCES: assert the event order is["error", "close"](noexit), thatexitCode === err.errnoboth whenerrorfires and afterclose, thatclosereceives(errno, null), and thatsignalCode/killedare unchanged.spawnSync() > output has one entry per stdio slotand> output has three entries for the default stdio.spawn() > stdio > an unknown stdio string throws ERR_INVALID_ARG_VALUE(async and sync) and> stdio that is neither a string nor an array throws ERR_INVALID_ARG_VALUE, asserting the code and Node's exact message.Node differential, before and after
Node's own ported tests still pass: every
test/js/node/test/parallel/test-child-process-*.jsthat passes onmainstill passes, includingtest-child-process-spawn-error.js(which toucheschild.stdin/stdout/stderr/stdiosynchronously on a failed spawn, the case where#maybeClose()'s#closesNeededbookkeeping could have swallowed theclose).no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/node/child_process/child_process.test.ts