Summary
Under concurrent request load, perry runs a Next.js app's byte-stream pull() callback at a point where the Flight runtime's internal state isn't ready, so the pull hits a cold path that reads .byteLength on null/undefined. This is a microtask-ordering / reentrancy divergence from Node (same class as the #6360 tick-parity work) — Node runs the same pull with the state populated.
With #6476 the throw no longer crashes the process (the stream errors and the request 500s), but the underlying null still makes that request fail where Node returns 200.
Repro
- App: a Next.js 16.1.1 standalone server (RSC/Flight,
type:"bytes" render stream), compiled with perry.
PERRY_GC_FORCE_EVACUATE=1 + concurrent load (≈8 parallel requests to a page route) triggers it within 1–2 rounds. Single / sequential requests are far less likely to hit it. A single request to any route is fine; the deterministic tee-drain / Buffer.from / reader paths are all byte-identical to Node in isolation.
Diagnosis (symbolized backtrace, PERRY_DEBUG_SYMBOLS=1 + PERRY_TRACE_THROW=1)
js_throw_type_error_property_access (Cannot read properties of null (reading 'byteLength'))
→ <Next Flight closures: __284.cold.7 → __284 → __544 → __552 → __609> (the pull callback chain)
→ perry_stdlib::streams::readable_pull_microtask (js_closure_call1 → the pull cb)
→ run_microtasks → js_callback_timer_tick → run_microtasks → js_node_http_server_process_pending → main
The .byteLength-on-null is internal to Next's pull (a cold/error path), not a chunk perry delivers — instrumenting push_chunk / tee_deliver / reader-read / Buffer.from for null never fires, and PERRY_GC_VERIFY_EVACUATION=1 stays clean (not a stale-pointer / GC-rewrite miss). So perry is invoking the pull microtask when Next's per-request/module state is null — a scheduling divergence.
Suspected area
streams::maybe_pull_inner pull scheduling vs Node's — likely perry fires the byte-pull relative to Next's render microtasks at a point Node defers past. Compare when perry vs Node schedules/runs the pull microtask across the Flight render pipeline (cf. the #6360 promise-hop/stream-cadence harnesses).
Impact
Real-app (gscmaster, 64-route Next.js) intermittent request failures / (pre-#6476) server crash under concurrent load. Visible DOM is otherwise byte-identical to Node.
Summary
Under concurrent request load, perry runs a Next.js app's byte-stream
pull()callback at a point where the Flight runtime's internal state isn't ready, so the pull hits a cold path that reads.byteLengthonnull/undefined. This is a microtask-ordering / reentrancy divergence from Node (same class as the #6360 tick-parity work) — Node runs the same pull with the state populated.With #6476 the throw no longer crashes the process (the stream errors and the request 500s), but the underlying null still makes that request fail where Node returns 200.
Repro
type:"bytes"render stream), compiled with perry.PERRY_GC_FORCE_EVACUATE=1+ concurrent load (≈8 parallel requests to a page route) triggers it within 1–2 rounds. Single / sequential requests are far less likely to hit it. A single request to any route is fine; the deterministic tee-drain /Buffer.from/ reader paths are all byte-identical to Node in isolation.Diagnosis (symbolized backtrace,
PERRY_DEBUG_SYMBOLS=1+PERRY_TRACE_THROW=1)The
.byteLength-on-null is internal to Next's pull (a cold/error path), not a chunk perry delivers — instrumentingpush_chunk/tee_deliver/ reader-read /Buffer.fromfor null never fires, andPERRY_GC_VERIFY_EVACUATION=1stays clean (not a stale-pointer / GC-rewrite miss). So perry is invoking the pull microtask when Next's per-request/module state is null — a scheduling divergence.Suspected area
streams::maybe_pull_innerpull scheduling vs Node's — likely perry fires the byte-pull relative to Next's render microtasks at a point Node defers past. Compare when perry vs Node schedules/runs the pull microtask across the Flight render pipeline (cf. the #6360 promise-hop/stream-cadence harnesses).Impact
Real-app (gscmaster, 64-route Next.js) intermittent request failures / (pre-#6476) server crash under concurrent load. Visible DOM is otherwise byte-identical to Node.