Skip to content

[Next.js/dylib] Preserve returned NextResponse headers, cookies, and stream across module boundaries #8038

Description

@proggeramlug

What happened?

The current Perry/Perch Next.js adapter can execute NextResponse.json(...) inside an imported route, but it cannot consume the returned Response reliably. 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.body surface. This issue is the remaining composed case: a NextResponse returned 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:

  • status 207;
  • content-type: application/json; charset=utf-8;
  • x-perry-repro: <request id>;
  • Set-Cookie: perry_ctx=<request id>; ... via NextResponse.cookies;
  • a ReadableStream<Uint8Array> that emits the JSON in two chunks, with the second chunk queued in a microtask.

Focused oracle:

BASE_URL=http://127.0.0.1:3100 npm run verify

Required implementation behavior

For both Response and the NextResponse subclass:

  • preserve the native response identity/brand across a cross-module return and an async Promise<Response> return;
  • preserve status, status text, headers, and cookie mutations;
  • preserve body: ReadableStream | null, including the queued second chunk and close signal;
  • keep the response, Headers backing, stream handle, controller, queued chunks, and reader rooted for the full drain;
  • make repeated property reads and body reads resolve the same backing record;
  • surface a thrown/rejected stream error instead of hanging or returning an empty body.

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 Response both synchronously and asynchronously and module B validates status/headers/cookies and drains body.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

  • [Next.js/dylib] Add a pinned production App Route parity fixture and CI gate #8034's verifier receives exact status, response header, cookie, and JSON for all 21 requests.
  • Both stream chunks are consumed and EOF is observed; an empty, truncated, buffered substitute does not pass the minimal stream test.
  • Sync and async cross-module returns pass with normal and forced/verified GC.
  • App-only dylib mode has the same result as a classic Perry executable.
  • The fixture contains no hard-coded status/body/headers and no ignored GET(...) return value.

Baseline / references

Activity

  1. added
    bugConfirmed defect or regression
    parityCompatibility gap with Node.js, ECMAScript, or the supported ecosystem
    on Aug 13, 2026
  2. proggeramlug commented on Aug 16, 2026

    @proggeramlug
    ContributorAuthor

    Closing — every item here has landed on main and is gated:

    Fix — #8045 (merged 2026-08-13): Response / Response-subclass native handles, Headers identity + mutations, and body streams are preserved across sync and Promise<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 streamed Response synchronously, asynchronously, as a Response subclass (NextLikeResponse), and as an errored stream; module B checks brand, status/statusText, header + set-cookie values, headers/body identity stability across collections, drains body.getReader(), and asserts chunks=2 eof=true and error:surfaced=Error: stream-boom. Runs under PERRY_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.sh compiles the same fixture as an app-only dylib against separately loaded runtime + stdlib providers, roots the exported Promise in a native host, and diffs 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 in gc-native-roots.yml (PR + every 6 h). Latest main run (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 real handler → routeModule.handle (bypass guard armed), returning a real streamed NextResponse with cookies.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-small adapter that discards GET()'s return value lives in the Perch repo and can now be switched to consuming the real response.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regressionparityCompatibility gap with Node.js, ECMAScript, or the supported ecosystem

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions