mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 02:48:50 +00:00
## Summary - Fixes #20321 - spawnSync crashes with RangeError when stdio is set to process.stderr - Handles file descriptors in stdio array correctly by treating them as non-captured output ## Problem When `spawnSync` is called with `process.stderr` or `process.stdout` in the stdio array, Bun.spawnSync returns the file descriptor number (e.g., 2 for stderr) instead of a buffer or null. This causes a RangeError when the code tries to call `toString(encoding)` on the number, since `Number.prototype.toString()` expects a radix between 2 and 36, not an encoding string. This was blocking AWS CDK usage with Bun, as CDK internally uses `spawnSync` with `stdio: ['ignore', process.stderr, 'inherit']`. ## Solution Check if stdout/stderr from Bun.spawnSync are numbers (file descriptors) and treat them as null (no captured output) instead of trying to convert them to strings. This aligns with Node.js's behavior where in `lib/internal/child_process.js` (lines 1051-1055), when a stdio option is a number or has an `fd` property, it's treated as a file descriptor: ```javascript } else if (typeof stdio === 'number' || typeof stdio.fd === 'number') { ArrayPrototypePush(acc, { type: 'fd', fd: typeof stdio === 'number' ? stdio : stdio.fd, }); ``` And when stdio is a stream object (like process.stderr), Node.js extracts the fd from it (lines 1056-1067) and uses it as a file descriptor, which means the output isn't captured in the result. ## Test plan Added comprehensive regression tests in `test/regression/issue/20321.test.ts` that cover: - process.stderr as stdout - process.stdout as stderr - All process streams in stdio array - Mixed stdio options - Direct file descriptor numbers - The exact AWS CDK use case All tests pass with the fix. 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
96 lines
3.1 KiB
TypeScript
96 lines
3.1 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { spawnSync } from "child_process";
|
|
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
|
|
|
|
test("spawnSync should not crash when stdout is set to process.stderr (issue #20321)", () => {
|
|
// Test with process.stderr as stdout
|
|
const proc1 = spawnSync(bunExe(), ["-e", 'console.log("hello")'], {
|
|
encoding: "utf-8",
|
|
stdio: ["ignore", process.stderr, "inherit"],
|
|
env: bunEnv,
|
|
});
|
|
|
|
expect(proc1.error).toBeUndefined();
|
|
expect(proc1.status).toBe(0);
|
|
// When redirecting to a file descriptor, we don't capture the output
|
|
expect(proc1.stdout).toBeNull();
|
|
});
|
|
|
|
test("spawnSync should not crash when stderr is set to process.stdout", () => {
|
|
// Test with process.stdout as stderr
|
|
const proc2 = spawnSync(bunExe(), ["-e", 'console.log("hello")'], {
|
|
encoding: "utf-8",
|
|
stdio: ["ignore", "pipe", process.stdout],
|
|
env: bunEnv,
|
|
});
|
|
|
|
expect(proc2.error).toBeUndefined();
|
|
expect(proc2.status).toBe(0);
|
|
expect(proc2.stdout).toBe("hello\n");
|
|
// When redirecting to a file descriptor, we don't capture the output
|
|
expect(proc2.stderr).toBeNull();
|
|
});
|
|
|
|
test("spawnSync should handle process.stdin/stdout/stderr in stdio array", () => {
|
|
// Test with all process streams
|
|
const proc3 = spawnSync(bunExe(), ["-e", 'console.log("test")'], {
|
|
encoding: "utf-8",
|
|
stdio: [process.stdin, process.stdout, process.stderr],
|
|
env: bunEnv,
|
|
});
|
|
|
|
expect(proc3.error).toBeUndefined();
|
|
expect(proc3.status).toBe(0);
|
|
// When redirecting to file descriptors, we don't capture the output
|
|
expect(proc3.stdout).toBeNull();
|
|
expect(proc3.stderr).toBeNull();
|
|
});
|
|
|
|
test("spawnSync with mixed stdio options including process streams", () => {
|
|
// Mix of different stdio options
|
|
const proc4 = spawnSync(bunExe(), ["-e", 'console.log("mixed")'], {
|
|
encoding: "utf-8",
|
|
stdio: ["pipe", process.stderr, "pipe"],
|
|
env: bunEnv,
|
|
});
|
|
|
|
expect(proc4.error).toBeUndefined();
|
|
expect(proc4.status).toBe(0);
|
|
// stdout redirected to stderr fd, so no capture
|
|
expect(proc4.stdout).toBeNull();
|
|
// stderr is piped, should be empty for echo
|
|
expect(proc4.stderr).toBe("");
|
|
});
|
|
|
|
test("spawnSync should work with file descriptors directly", () => {
|
|
// Test with raw file descriptors (same as what process.stderr resolves to)
|
|
const proc5 = spawnSync(bunExe(), ["-e", 'console.log("fd-test")'], {
|
|
encoding: "utf-8",
|
|
stdio: ["ignore", 2, "inherit"], // 2 is stderr fd
|
|
env: bunEnv,
|
|
});
|
|
|
|
expect(proc5.error).toBeUndefined();
|
|
expect(proc5.status).toBe(0);
|
|
expect(proc5.stdout).toBeNull();
|
|
});
|
|
|
|
test("spawnSync should handle the AWS CDK use case", () => {
|
|
// This is the exact use case from AWS CDK that was failing
|
|
const dir = tempDirWithFiles("spawnsync-cdk", {
|
|
"test.js": `console.log("CDK output");`,
|
|
});
|
|
|
|
const proc = spawnSync(bunExe(), ["test.js"], {
|
|
encoding: "utf-8",
|
|
stdio: ["ignore", process.stderr, "inherit"],
|
|
cwd: dir,
|
|
env: bunEnv,
|
|
});
|
|
|
|
expect(proc.error).toBeUndefined();
|
|
expect(proc.status).toBe(0);
|
|
// Output goes to stderr, not captured
|
|
expect(proc.stdout).toBeNull();
|
|
});
|