Repository navigation
[Next.js/dylib] Preserve returned NextResponse headers, cookies, and stream across module boundaries #8038
Description
Activity
- addedbugConfirmed defect or regressionConfirmed defect or regressionparityCompatibility gap with Node.js, ECMAScript, or the supported ecosystemCompatibility gap with Node.js, ECMAScript, or the supported ecosystem
on Aug 13, 2026 Closing — every item here has landed on
mainand is gated:Fix — #8045 (merged 2026-08-13):
Response/Response-subclass native handles,Headersidentity + mutations, and body streams are preserved across sync andPromise<Response>module boundaries; stream roots and runtime-owned allocations go through the shared provider ABI; GC maps are loaded from app dylibs.Two-module minimal fixture —
test-files/test_issue_8038_cross_module_response_stream.ts+test-files/_helpers/issue_8038_response_producer.ts(#8045). Module A returns a streamedResponsesynchronously, asynchronously, as aResponsesubclass (NextLikeResponse), and as an errored stream; module B checks brand, status/statusText, header +set-cookievalues,headers/bodyidentity stability across collections, drainsbody.getReader(), and assertschunks=2 eof=trueanderror:surfaced=Error: stream-boom. Runs underPERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1 PERRY_GC_SCHEDULE_SEED=8038 PERRY_GC_SCHEDULE_RATE=1(parity-env header). No hard-coded status/body/headers, no ignored return values.App-only dylib == executable — #8089 (merged 2026-08-14):
scripts/gc_provider_dylib_gate.shcompiles the same fixture as an app-only dylib against separately loaded runtime + stdlib providers, roots the exported Promise in a native host, anddiffs both the normal and forced/verified-GC arms against the same expected file the executable oracle uses, with a positive evacuation-liveness assert. It runs ingc-native-roots.yml(PR + every 6 h). Latestmainrun (31933223109, 2026-08-16 07:32 UTC):#8038 provider-dylib Response: evacuation live — 147 copying minor(s), 32194 objects copied, exact output.#8034 production route — #8082 (merged): the pinned fixture (
tests/fixtures/next-app-route/,tests/release/packages/next-app-route/) serves 21/21 requests with exact status/header/cookie/JSON, cold + warm, through Next's realhandler→routeModule.handle(bypass guard armed), returning a real streamedNextResponsewithcookies.set(...)— nothing fabricated in the caller. #8034 is closed.Not covered here, tracked separately: the forced-evacuation arm of the production route (a stale closure from a holder outside the GC heap) is #8163. The Perch
benchmarks/next-smalladapter that discardsGET()'s return value lives in the Perch repo and can now be switched to consuming the real response.
What happened?
The current Perry/Perch Next.js adapter can execute
NextResponse.json(...)inside an imported route, but it cannot consume the returnedResponsereliably. It therefore discards the real return value and fabricates deterministic response bytes in the caller. That makes the RSS/CPU result a lower bound rather than an apples-to-apples Next.js result.Closed work such as #237 and #1650 established the Web Streams /
Response.bodysurface. This issue is the remaining composed case: aNextResponsereturned through an imported async App Route boundary, then inspected and drained by the app-library entry point.Reproduction app (provided)
Use the exact pinned app and verifier attached to #8034. Do not replace the streamed response with
NextResponse.json()or a hard-coded Buffer.The supplied route returns:
207;content-type: application/json; charset=utf-8;x-perry-repro: <request id>;Set-Cookie: perry_ctx=<request id>; ...viaNextResponse.cookies;ReadableStream<Uint8Array>that emits the JSON in two chunks, with the second chunk queued in a microtask.Focused oracle:
Required implementation behavior
For both
Responseand theNextResponsesubclass:Promise<Response>return;body: ReadableStream | null, including the queued second chunk and close signal;The host ABI can serialize the final response after Perry has exposed these fields; that serialization is not a reason to fabricate a different response inside the test.
Regression coverage
Add a two-module minimal fixture in which module A returns a streamed
Responseboth synchronously and asynchronously and module B validates status/headers/cookies and drainsbody.getReader(). Then keep #8034 as the production Next subclass/composition test.Run both tests in executable mode and app-only dylib mode against the shared runtime/stdlib providers; this catches native-handle registries accidentally duplicated or hidden across images.
Acceptance criteria
GET(...)return value.Baseline / references
0.5.1503at564c56308d221a51b50308d9165578fbb176e877