spawn: reject ReadableStream, Request and Response for stdout/stderr
RO
1mo ago+106+106+012345678901234567890123456789-42-42-01234567890123456789
Problem
Bun.spawn(cmd, { stdout: new ReadableStream() })is accepted.proc.stdoutisundefinedand the child's fd 1 is a socket that nothing reads. A child that fills the socket buffer blocks forever.- Cause: the
ReadableStreambranch ofStdio::extract(src/runtime/api/bun/spawn/stdio.rs) never rejects slot 1 or 2.Readable::initthen drops the parent end. - A
RequestorResponsewith no body at stdout silently becomes"ignore". An unknown value throws thestdio-array message for every slot.
Fix
reject_as_output()rejectsReadableStream,RequestandResponseat stdout/stderr:ReadableStream cannot be used for stdout. To read the subprocess's stdout, use 'pipe' and read from subprocess.stdout.- The fallthrough error names the slot and lists the kinds it accepts.
- Correct because the parent reads stdout/stderr.
Spawn.Readableinbun.d.tsalready excludes these kinds. - Verified:
test/js/bun/spawn/spawn.test.ts(two new tests fail on 1.4.3 and pass here, the whole file passes).
Background
Stdio::extract(i, value)parses one stdio slot. Slot 0 is stdin, 1 is stdout, 2 is stderr.- For stdin, the parent pipes the stream into the child through a socketpair. Only stdin attaches a writer to the parent end.
- Considered treating the stream as
'pipe'. Nothing can write into the caller'sReadableStreamfrom outside, so the output would still reach nobody.
Downsides
stdout: new Response(null)ornew Request(url)was accepted as"ignore". It now throws.- Code that matches the old message texts breaks:
stdio must be an array of 'inherit', 'ignore', or null, andBlobs are immutable, and cannot be used for stdout/stderrfor aResponsewith a body.
Notes
- The unknown values in the tests are
"bogus",true,{}, aURLand aProxyaroundBun.file(). - Repro:
Bun.spawn(['sh','-c','head -c 1000000 /dev/zero; echo done >&2'], { stdout: new ReadableStream(), stderr: 'inherit' })printedchild fd1 -> socket:[...]and was still running after 3 s. - Callers that pass
'pipe','inherit','ignore',null, a number orBun.file()run the same code as before. The new check runs only on theRequest,ResponseandReadableStreambranches. - Binary size: the message literals grow by 248 bytes net (counted from the source, not measured on a release build).
- The check runs after the existing
to_any_blob()shortcut, so a blob-backed stream still goes throughextract_blob(). Index 3 and up keepsReadableStream cannot be used for stdio[N] yet.Readable::init'sStdio::ReadableStreamarm becomesunreachable!(), like theSocketFdarm next to it. - The stdin list for
Bun.spawnSyncomitsReadableStream, and thestdio[N]list omits'socket-fd', becausespawnSyncrejects both. - Not addressed here:
stdin: s3Filewith an unreachable endpoint gives the child EOF and the connection error surfaces nowhere. That is the S3 download path, not the option parser. spawn-stdin-readable-stream*.test.tson the debug build: 63 tests pass. Three time out at 5 s, with and without this change (ReadableStream object type count,does not leak native FileSink when ReadableStream is used as stdin,ReadableStream with spawn options variations). Each starts several debug-build child processes.- Rebased on main. The two runner commits that were on this branch landed on main separately.
no test proof · iteration 5 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/spawn/spawn.test.ts