mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
## Summary
- Fixes the broken pipe behavior for `process.stdout.write()` to match
Node.js
- When writing to a broken pipe (stdout destroyed), the process now
properly exits with code 1 instead of 0
- EPIPE errors are now properly propagated to JavaScript via the
stream's error event
## Test plan
- [x] Added regression test `test/regression/issue/1632.test.ts`
- [x] Verified test fails with system bun (exit code 0) and passes with
debug build (exit code 1)
- [x] Verified `console.log` still ignores errors (uses `catch {}`) and
doesn't crash
- [x] Verified callback-based `process.stdout.write()` receives EPIPE
error
## Changes
1. **`src/io/PipeWriter.zig`**: Return EPIPE as an error instead of
treating it as successful end-of-file (`.done`)
2. **`src/shell/IOWriter.zig`**: Track `broken_pipe` flag when EPIPE is
received via `onError` callback, and propagate error properly
3. **`src/js/internal/fs/streams.ts`**: When a write fails without a
callback, emit the error on the stream via `this.destroy(err)` to match
Node.js behavior
Fixes #1632
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
132 lines
4.1 KiB
TypeScript
132 lines
4.1 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { spawn } from "child_process";
|
|
import { bunEnv, bunExe } from "harness";
|
|
|
|
describe("issue #1632 - broken pipe behavior for process.stdout.write()", () => {
|
|
test("process.stdout.write() should exit non-zero on broken pipe", async () => {
|
|
// Use child_process.spawn to get proper Node-style streams with destroy()
|
|
const child = spawn(bunExe(), ["-e", 'process.stdout.write("testing\\n");'], {
|
|
env: bunEnv,
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
});
|
|
|
|
// Destroy stdout immediately to create a broken pipe
|
|
child.stdout!.destroy();
|
|
|
|
const exitCode = await new Promise<number | null>(resolve => {
|
|
child.on("exit", resolve);
|
|
});
|
|
|
|
// The process should exit with a non-zero code due to the unhandled EPIPE error
|
|
// Node.js exits with code 1 in this case
|
|
expect(exitCode).not.toBe(0);
|
|
});
|
|
|
|
test("console.log should not panic on broken pipe", async () => {
|
|
// console.log should ignore errors (uses catch {}) and not crash
|
|
const child = spawn(bunExe(), ["-e", 'console.log("testing");'], {
|
|
env: bunEnv,
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
});
|
|
|
|
// Destroy stdout immediately
|
|
child.stdout!.destroy();
|
|
|
|
let stderr = "";
|
|
child.stderr!.on("data", data => {
|
|
stderr += data.toString();
|
|
});
|
|
|
|
await new Promise<void>(resolve => {
|
|
child.on("exit", resolve);
|
|
});
|
|
|
|
// console.log ignores errors, so the process shouldn't panic
|
|
expect(stderr).not.toContain("panic");
|
|
});
|
|
|
|
test("matches Node.js behavior - broken pipe causes exit code 1", async () => {
|
|
// This test spawns a subprocess that tries to write to a destroyed stdout
|
|
// using child_process.exec pattern from the original issue
|
|
await using proc = Bun.spawn({
|
|
cmd: [
|
|
bunExe(),
|
|
"-e",
|
|
`
|
|
const { exec } = require("child_process");
|
|
const child = exec(process.execPath + ' -e "process.stdout.write(\\'testing\\\\n\\')"', (err) => {
|
|
if (err) {
|
|
console.log("exit_code:" + err.code);
|
|
console.log("killed:" + err.killed);
|
|
console.log("signal:" + err.signal);
|
|
} else {
|
|
console.log("no_error");
|
|
}
|
|
});
|
|
child.stdout.destroy();
|
|
`,
|
|
],
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
// The parent process should complete successfully
|
|
expect(exitCode).toBe(0);
|
|
|
|
// The child should have exited with an error (code 1) due to EPIPE
|
|
// Node.js behavior: "1 false null" - exit code 1, not killed, no signal
|
|
// If it says no_error, the write completed before stdout was destroyed (timing)
|
|
if (stdout.includes("exit_code:")) {
|
|
expect(stdout).toContain("exit_code:1");
|
|
}
|
|
});
|
|
|
|
test("process.stdout.write() callback receives EPIPE error", async () => {
|
|
// Test that the write callback receives the EPIPE error
|
|
const child = spawn(
|
|
bunExe(),
|
|
[
|
|
"-e",
|
|
`
|
|
// Handle the error via callback
|
|
process.stdout.write("testing\\n", (err) => {
|
|
if (err) {
|
|
// Error should have code EPIPE
|
|
console.error("ERROR_CODE:" + err.code);
|
|
process.exit(42);
|
|
}
|
|
process.exit(0);
|
|
});
|
|
`,
|
|
],
|
|
{
|
|
env: bunEnv,
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
},
|
|
);
|
|
|
|
// Destroy stdout immediately to create broken pipe
|
|
child.stdout!.destroy();
|
|
|
|
let stderr = "";
|
|
child.stderr!.on("data", data => {
|
|
stderr += data.toString();
|
|
});
|
|
|
|
const exitCode = await new Promise<number | null>(resolve => {
|
|
child.on("exit", resolve);
|
|
});
|
|
|
|
// Either:
|
|
// 1. The error callback was called with EPIPE and process exited with 42, or
|
|
// 2. The write completed before stdout was destroyed and process exited with 0
|
|
// Both are acceptable - we mainly want to verify it doesn't exit 0 silently when there IS an error
|
|
if (exitCode === 42) {
|
|
expect(stderr).toContain("ERROR_CODE:EPIPE");
|
|
}
|
|
});
|
|
});
|