Skip to content

Commit 0d4089e

Browse files
Fixes #20753 (#20789)
Co-authored-by: Jarred-Sumner <709451+Jarred-Sumner@users.noreply.github.com>
1 parent 27c9791 commit 0d4089e

File tree

2 files changed

+119
-2
lines changed

2 files changed

+119
-2
lines changed

src/js/node/child_process.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,12 +272,13 @@ function execFile(file, args, options, callback) {
272272
// merge chunks
273273
let stdout;
274274
let stderr;
275-
if (child.stdout?.readableEncoding) {
275+
if (encoding || child.stdout?.readableEncoding) {
276276
stdout = ArrayPrototypeJoin.$call(_stdout, "");
277277
} else {
278278
stdout = BufferConcat(_stdout);
279279
}
280-
if (child.stderr?.readableEncoding) {
280+
281+
if (encoding || child.stderr?.readableEncoding) {
281282
stderr = ArrayPrototypeJoin.$call(_stderr, "");
282283
} else {
283284
stderr = BufferConcat(_stderr);

test/regression/issue/20753.test.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { isWindows } from "harness";
3+
import { execFile } from "node:child_process";
4+
import { promisify } from "node:util";
5+
6+
const execFileAsync = promisify(execFile);
7+
8+
describe.skipIf(isWindows /* accessing posix-specific paths */)("stdout should always be a string", () => {
9+
test("execFile returns string stdout/stderr even when process fails to spawn", done => {
10+
// Test case that would cause the issue: non-existent command
11+
execFile("/does/not/exist", [], (err, stdout, stderr) => {
12+
expect(err).toBeTruthy();
13+
expect(err.code).toBe("ENOENT");
14+
15+
// These should never be undefined - they should be strings by default
16+
expect(stdout).toBeDefined();
17+
expect(stderr).toBeDefined();
18+
expect(typeof stdout).toBe("string");
19+
expect(typeof stderr).toBe("string");
20+
expect(stdout).toBe("");
21+
expect(stderr).toBe("");
22+
23+
// This is what claude-code was trying to do that failed
24+
expect(() => stdout.trim()).not.toThrow();
25+
expect(() => stderr.trim()).not.toThrow();
26+
27+
done();
28+
});
29+
});
30+
31+
test("execFile returns string stdout/stderr for permission denied errors", done => {
32+
// Another edge case: file exists but not executable
33+
execFile("/etc/passwd", [], (err, stdout, stderr) => {
34+
expect(err).toBeTruthy();
35+
expect(err.code).toBe("EACCES");
36+
37+
expect(stdout).toBeDefined();
38+
expect(stderr).toBeDefined();
39+
expect(typeof stdout).toBe("string");
40+
expect(typeof stderr).toBe("string");
41+
expect(stdout).toBe("");
42+
expect(stderr).toBe("");
43+
44+
done();
45+
});
46+
});
47+
48+
test("execFile returns Buffer stdout/stderr when encoding is 'buffer'", done => {
49+
execFile("/does/not/exist", [], { encoding: "buffer" }, (err, stdout, stderr) => {
50+
expect(err).toBeTruthy();
51+
expect(err.code).toBe("ENOENT");
52+
53+
expect(stdout).toBeDefined();
54+
expect(stderr).toBeDefined();
55+
expect(Buffer.isBuffer(stdout)).toBe(true);
56+
expect(Buffer.isBuffer(stderr)).toBe(true);
57+
expect(stdout.length).toBe(0);
58+
expect(stderr.length).toBe(0);
59+
60+
done();
61+
});
62+
});
63+
64+
test("execFile promisified version includes stdout/stderr in error object", async () => {
65+
try {
66+
await execFileAsync("/does/not/exist", []);
67+
expect.unreachable("Should have thrown");
68+
} catch (err) {
69+
expect(err.code).toBe("ENOENT");
70+
71+
// Promisified version attaches stdout/stderr to the error object
72+
expect(err.stdout).toBeDefined();
73+
expect(err.stderr).toBeDefined();
74+
expect(typeof err.stdout).toBe("string");
75+
expect(typeof err.stderr).toBe("string");
76+
expect(err.stdout).toBe("");
77+
expect(err.stderr).toBe("");
78+
}
79+
});
80+
81+
test("execFile returns stdout/stderr for process that exits with error code", done => {
82+
execFile(
83+
process.execPath,
84+
["-e", "console.log('output'); console.error('error'); process.exit(1)"],
85+
(err, stdout, stderr) => {
86+
expect(err).toBeTruthy();
87+
expect(err.code).toBe(1);
88+
89+
expect(stdout).toBeDefined();
90+
expect(stderr).toBeDefined();
91+
expect(typeof stdout).toBe("string");
92+
expect(typeof stderr).toBe("string");
93+
expect(stdout).toBe("output\n");
94+
expect(stderr).toBe("error\n");
95+
96+
done();
97+
},
98+
);
99+
});
100+
101+
test("execFile handles fast-exiting processes correctly", done => {
102+
// Process that exits immediately
103+
execFile("true", [], (err, stdout, stderr) => {
104+
expect(err).toBeNull();
105+
106+
expect(stdout).toBeDefined();
107+
expect(stderr).toBeDefined();
108+
expect(typeof stdout).toBe("string");
109+
expect(typeof stderr).toBe("string");
110+
expect(stdout).toBe("");
111+
expect(stderr).toBe("");
112+
113+
done();
114+
});
115+
});
116+
});

0 commit comments

Comments
 (0)