Repository navigation
test(console-write): accept ENOTCONN on macOS when the reader hangs up - #44072
Conversation
The macOS kernel fails a write to a socket with ENOTCONN, not EPIPE, when the peer closes while that write copies its bytes in. Every later write fails with EPIPE. Bun passes the kernel errno through, as Node does (#40935). The case "an awaited console.write rejects with EPIPE when the reader has hung up" closes the reader while the child writes, so on macOS the child can catch either code. The test accepts both there and still requires that the promise rejects and that nothing else reaches stderr.
|
Updated 4:16 PM PT - Sep 26th, 2026
❌ @robobun, your commit d43b6b7 has 2 failures in
🧪 To try this PR locally: bunx bun-pr 44072That installs a local version of the PR into your bun-44072 --bun |
|
Status: ready for review. This PR changes one test and no file under How I reproduced the failure:
The CI, build 121049: the changed test passes on each lane that ran it, darwin aarch64 and darwin x64 included. The build is red on two tests that this PR does not touch:
PR: #44072 |
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: oven-sh/bun/.coderabbit.yaml Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (1)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review. WalkthroughThe hung-up-reader test now accepts ChangesConsole write test
Suggested reviewers: Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to The test accepts either reported macOS write error while retaining its existing failure and output checks. No actionable merge risk remains. 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
Problem
test/js/bun/console/console-write.test.ts:134fails on macOS with- "caught EPIPE/+ "caught ENOTCONN.ENOTCONN, notEPIPE.EPIPEonly. Builds 121009 and 120123 were red: each attempt failed with this or with the unhandled rejection that console.write: no unhandled rejections from a broken pipe; FileSink write counts #43679 fixes.Fix
caught EPIPEorcaught ENOTCONN. Other platforms acceptcaught EPIPEonly.maincanary, 150 runs of the file.caught ENOTCONNfails 7 runs of the old file and 0 runs of the new file.bun bd test test/js/bun/console/console-write.test.tspasses on Linux.Background
Bun.spawngives a child withstdout: "pipe"one end of an AF_UNIX socketpair.sosend()tests forEPIPE, then unlocks the socket to copy the bytes.uipc_send()then tests "connected" first and returnsENOTCONN. Each later write returnsEPIPE.try_write_with_write_fn(src/io/PipeWriter.rs:84) so that the writer reportsEPIPE. child_process: report a failed child stdin write with syscall write, as Node does #40935 decided that Bun passes the kernel errno through, as Node does. Node gaveENOTCONNin 65 of 150 runs of this scenario.Notes
This PR changes one test. No file under
src/changes.Kernel path (xnu-12377,
bsd/kern):sosendcheck()(uipc_socket.c) testsSS_CANTSENDMOREfirst and returnsEPIPE.sosend()callssocket_unlock()before theuiomovecopy andsocket_lock()after it. It callspru_sendwith no second state test.uipc_send()(uipc_usrreq.c,SOCK_STREAMbranch) testsSS_ISCONNECTEDfirst (ENOTCONN) andSS_CANTSENDMOREsecond (EPIPE).soisdisconnected()(uipc_socket2.c) clearsSS_ISCONNECTEDand setsSS_CANTSENDMOREin one step.write(2)on a socket takes the same path (soo_write()callssosend()).C probe, no Bun and no Node. An 8 MB write on an AF_UNIX socketpair with 512 KB buffers. The peer reads one chunk and closes. 500 runs each.
send()write()ENOTCONN141ENOTCONN396ENOTCONN442ENOTCONN392ENOTCONN1ENOTCONN287ENOTCONN0 of 300ENOTCONN0 of 200All other runs gave
EPIPE. So the errno is not specific to macOS 26 or to one machine. With the default socket buffer size the M2 mini gaveEPIPEin 500 of 500 runs.One more write directly after the
ENOTCONNreturnedEPIPEin 1659 of 1659 runs. A socket that was never connected returnsENOTCONNon each write.The scenario of the test, on the M2 mini (macOS 26.6.1), Bun
1.4.3-canary.1+37da174d5, Node v26.3.0:EPIPEENOTCONNawait console.write(big)process.stdout.write(big, cb)process.stdout.write(big, cb)process.stdout.write(big, cb)On macOS 14.8.9 x64, node parent and node child:
ENOTCONNin 18 of 200 runs.Runs of the test file on the M2 mini:
maincanarymaincaught ENOTCONN(one argument), 69 unhandled rejection (several arguments)maincanarycaught ENOTCONN(one argument)The test on
mainstays flaky on macOS until #43679 merges.The decision in #40935. An earlier revision of #40935 folded
ENOTCONNintoEPIPEinwrite_to_socket. The merged commit (118fdd2) dropped the fold: "The kernel errno is passed through unchanged." Node accepts both codes on macOS in its own test (test/js/node/test/parallel/test-cluster-concurrent-disconnect.js:27-33). At that time the race was not reproduced (1100 runs, allEPIPE). The probes above reproduce it. This PR keeps the decision.process.platformand notisMacOS. #43679 changes theharnessimport line of this file. The test readsprocess.platformso that the two PRs merge in either order with no conflict (checked withgit merge-tree).The same errno in other tests. In the annotations of the last 60 finished builds,
ENOTCONNappears in the output of three test files: this one,test/js/bun/util/filesink.test.ts(#43794 removes the race there) andtest/js/node/process/process-stdin.test.ts(#42260 ignores the code on macOS).no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/console/console-write.test.ts