Skip to content

Concurrency race: Next.js byte-stream pull runs before Flight state is ready (null.byteLength) #6477

Description

@proggeramlug

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regression

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions