Repository navigation
@remotion/renderer: Don't crash the process when the compositor quits while receiving commands - #12592
@remotion/renderer: Don't crash the process when the compositor quits while receiving commands#12592parbhatkapila4 wants to merge 2 commits into
@remotion/renderer: Don't crash the process when the compositor quits while receiving commands#12592Conversation
…ts while receiving commands
There was a problem hiding this comment.
ℹ️ No critical issues — one test-strength suggestion inline and one optional consistency note.
Reviewed changes
Reviewed the stdin error handling change in startCompositor() and the accompanying regression test.
- stdin
errorlistener:child.stdin.on('error', ...)now logs the failure withLog.verbose()instead of letting Node emit an unhandled'error'event and kill the process (packages/renderer/src/compositor/compositor.ts:223). executeCommandwrite: the write callback that rejected a command with the rawEPIPEis removed; a failed write now falls through to theclosehandler, which rejects every pending waiter with the compositor's exit reason (packages/renderer/src/compositor/compositor.ts:405).- Regression test: sends
Echocommands concurrently with aDeliberatePanicand asserts every rejection starts withCompositor(packages/renderer/src/test/rust-errors.test.ts:73).
The reasoning holds: EPIPE on the compositor's stdin only occurs once the read end is closed, i.e. the process is exiting, so the close handler is the right place to surface the real error, and the listener keeps the event loop from throwing.
ℹ️ finishCommands() still surfaces a raw EPIPE
executeCommand() no longer rejects with EPIPE, but the sibling write in finishCommands() (packages/renderer/src/compositor/compositor.ts:328) still passes a write callback that rejects with the raw pipe error. If the compositor dies while the EOF sentinel is being written, the caller sees EPIPE: broken pipe, write rather than the compositor's exit reason — the outcome this PR removes from the command path. This no longer crashes (the new listener covers it) and runningStatus short-circuits most cases, so it is optional, but aligning it would make the error surface consistent.
Technical details
# Align finishCommands() EPIPE handling with executeCommand()
## Affected sites
- packages/renderer/src/compositor/compositor.ts:328 — `child.stdin.write('EOF\n', (e) => { if (e) { rej(e); ... } })` rejects with the raw write error.
## Required outcome
- A failed `EOF` write during shutdown should reject with the compositor's exit reason (as the `close` handler already does for pending waiters) instead of `EPIPE`, or the difference should be a deliberate, documented choice.
## Suggested approach (optional)
- Drop the callback here too and rely on the `close`/`runningStatus` paths, matching the new `executeCommand()` behavior.deepseek-v4.1-flash | 𝕏
|
Follow-up on the review above — here is the inline suggestion it referenced, on The loop only validates the rejections that exist; it never asserts that any command was actually rejected. If the compositor processes all
|
|
Added the assertion from the inline suggestion. I left finishCommands() as it is on purpose. It's also called on its own (for example, in get-video-metadata.ts), so it should still reject if it can't write EOF, and it can't crash now that stdin has an error listener. Rejecting with the exit reason would mean calling waitForDone() from inside it, and waitForDone() only keeps one resolve/reject, so that could leave another caller waiting forever. |
|
Thanks for the PR! Before we review the fix, can you tell us more about the problem itself?
We prefer to merge fixes that are tied to a problem someone actually hit, so we can confirm that the fix solves it. |
|
Honest answer: no, I didn't hit it in a real render. I found it while reading compositor.ts. stdin has no 'error' listener, so if the compositor dies mid-write (an OOM kill, say), Node crashes on an unhandled EPIPE instead of surfacing the real exit reason. No repro or user report, so totally fine to close if you only take reported issues. Happy to pick up something from the tracker instead. |
|
Thanks for understanding, please file an issue first next time! |

If the compositor quits while commands are still being written to it, for example, after a panic or when the OS kills it for using too much memory, the write to its stdin fails with EPIPE.
startCompositor()has noerrorlistener onchild.stdin, so Node throws an unhandled'error'event, and the whole process exits, instead of the render failing with the compositor's error. ffmpeg doesn't have this problem because execa adds that listener.Now stdin errors are logged with
Log.verbose()instead. The command whose write failed is also no longer rejected withEPIPE: broken pipe, write. Theclosehandler rejects it with the reason the compositor quit, like the other pending commands.Added a test next to "Handle panics" that keeps sending
Echocommands while the compositor panics. On main it fails with an unhandled EPIPE, and some commands get rejected withEPIPE: broken pipe, write, on both Linux and Windows. With the fix, every rejected command gets the compositor's error. I also tried it in Node, killing the compositor with SIGKILL while commands were being sent: on main, the Node process crashes withwrite EPIPE, and with the fix, every command is rejected with the compositor's SIGKILL error.