fix(streams): a throwing ReadableStream pull() errors the stream, not crash#6476
Open
proggeramlug wants to merge 1 commit into
Open
fix(streams): a throwing ReadableStream pull() errors the stream, not crash#6476proggeramlug wants to merge 1 commit into
proggeramlug wants to merge 1 commit into
Conversation
… crash Perry invokes a ReadableStream's `pull()` from a Rust-owned microtask (`readable_pull_microtask`). If the pull callback threw, the exception `longjmp`ed straight out to the microtask loop — past this Rust frame — skipping the `pulling = false` reset and leaving the stream wedged; under concurrency this corrupted stream state and crashed the process. (Found driving a real Next.js app whose byte-stream pull hit a cold-path throw.) Per the Web Streams spec a throwing pull must run ReadableStreamDefaultControllerError: the stream errors and its pending reads reject with the thrown value. Add a public `exception::js_call_catching(f) -> Result<f64, f64>` (setjmp-based Rust try/catch, mirroring `combinator_catch_js`) and wrap the pull invocation with it; on a throw, call `error_readable_stream(...)` so `reader.read()` rejects and the consumer's own handler observes it — exactly as Node does. Gap test `test_gap_readable_stream_pull_throws.ts` covers the default and byte-stream pull paths: read() rejects with the thrown message and the process survives. Claude-Session: https://claude.ai/code/session_01KD4GTmiwZjRrxShzQydJpF
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughAdds runtime exception capture for Rust closures and uses it in ChangesReadableStream pull error handling
Estimated code review effort: 3 (Moderate) | ~20 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A
ReadableStream'spull()callback that throws no longer crashes the process — it errors the stream per the Web Streams spec (pendingread()s reject with the thrown value), exactly as Node does.The bug
Perry drives a stream's
pull()from a Rust-owned microtask (streams::readable_pull_microtask, invokingjs_closure_call1/pull_deferred_byte_chunk). If the pull callback threw, perry'ssetjmp/longjmpexception unwind jumped straight out to the microtask loop — pastreadable_pull_microtask's frame — skipping thepulling = falsereset and leaving the stream wedged. Under concurrency this corrupted stream state and killed the process, instead of surfacing a normal rejectedread().Found while running a real Next.js app: its byte-stream content-length pull hits a cold-path throw under concurrent load, which took down the whole server rather than failing the one request.
The fix
exception::js_call_catching(f) -> Result<f64, f64>(newpub): asetjmp-based Rust try/catch that runsf(which may call into JS andjs_throw) and returnsOk(value)orErr(exception). Thesetjmplives in its own frame, so a throw unwinds only up to there — not past the caller. Mirrors the existingcombinators::combinator_catch_js.streams::readable_pull_microtasknow wraps the pull invocation injs_call_catching; on a throw it calls the existingerror_readable_stream(...)(spec'sReadableStreamDefaultControllerError), soreader.read()rejects and the consumer's own handler observes the error.Tests
New gap test
test_gap_readable_stream_pull_throws.ts(default +type:"bytes"pull paths):read()rejects with the thrown message and the process survives — byte-for-byte withnode --experimental-strip-types. Verified normal (non-throwing) stream reads/tees are unchanged.Scope note
This is a general robustness fix (a throwing pull should never crash the server). It is not the complete fix for the Next.js app that surfaced it: that app also hits an underlying concurrency race that makes its pull read a null in the first place (a separate microtask-ordering issue, filed separately). This PR removes the crash-on-throw failure mode.
https://claude.ai/code/session_01KD4GTmiwZjRrxShzQydJpF
Summary by CodeRabbit
Bug Fixes
ReadableStreambehavior whenpull()throws.Tests