Repository navigation
[Next.js/dylib] Preserve NextRequest and nextUrl.searchParams across App Route imports #8036
Description
What happened?
The Perch Next.js lower-bound fixture on Perry 0.5.1503 could construct a NextRequest in one module and call an imported route function, but could not rely on the callee receiving a usable request argument. NextRequest.nextUrl.searchParams was also not reliable through that path, so the benchmark had to ignore the request and hard-code its workload.
This remains a distinct end-to-end gap even though important lower-level fixes have landed: #7252 roots cross-module call arguments and #6738 backs URLSearchParams subclasses. The missing regression is the actual production Next request shape across the generated App Route/import boundary.
Reproduction app (provided)
Use the exact pinned app and verifier attached to #8034. Do not create a new app. Its route is deliberately re-exported across a module boundary:
AppRouteRouteModule.handle
-> app/api/benchmark/route.ts
-> lib/route-impl.ts::GET/POST
Build with:
bash make-next-app-route-fixture.sh
cd perry-next-app-route-fixture
npm install --package-lock-only
npm ci
npm run buildThe focused request is:
curl -i \
-H 'x-request-id: request-7' \
'http://127.0.0.1:3100/api/benchmark?id=request-7&iterations=7'The returned JSON must contain:
{
"method": "GET",
"pathname": "/api/benchmark",
"id": "request-7",
"iterations": 7,
"beforeAwait": "request-7",
"afterAwait": "request-7",
"requestBody": ""
}The attached verifier also sends a POST and requires requestBody === "perry-request-body".
Required implementation behavior
- Preserve the
NextRequestobject passed by the generated App Route module into the re-exported user handler. - Preserve method, URL,
nextUrl,ReadonlyURLSearchParams, headers, and body state across the imported call. - Keep the request live across allocations, dynamic import, promise continuation, timer continuation, and GC movement.
- Do not special-case the fixture or lower
NextRequestto a partial plain object. - Keep direct calls, re-exports, sync handlers, and async handlers on the same correct path.
Regression coverage
Add a small compiler fixture for a request-shaped class passed through a re-exported imported function, plus the production integration test from #8034. Run the small fixture with forced evacuation / GC verification so a stale-but-lucky pointer cannot pass.
Acceptance criteria
- All request-field assertions in [Next.js/dylib] Add a pinned production App Route parity fixture and CI gate #8034's
verify.mjspass through the productionrouteModule.handlepath. - GET query data and POST body data are correct on the first cold call and subsequent warm calls.
- The 20 concurrent requests never exchange IDs or headers.
- The test passes with forced/verified GC and with normal GC.
- No direct call to the userland
GET, hard-coded query value, response fabrication, or other compatibility fallback exists in the Perry test path.
Baseline / references
- Observed baseline: Perry
0.5.1503at564c56308d221a51b50308d9165578fbb176e877 - Fixture and full commands: [Next.js/dylib] Add a pinned production App Route parity fixture and CI gate #8034
- Related groundwork: fix(url): route URLSearchParams subclass instances to their native backing #6738, fix(gc): root every argument of a cross-module direct call (#7240, by @jdalton) #7252
Activity
fix(codegen): optnone post-RS4GC relocation-bloated functions
feat(gc): sharpen the from-space scan and stack-map walk instruments
fix(codegen): optnone post-RS4GC relocation-bloated functions
feat(gc): sharpen the from-space scan and stack-map walk instruments
fix(codegen): unbreak in-process RS4GC on inline asm and relocation-g…
feat(gc): name the HOLDERS of a stale from-space address, not just th…
feat(gc): sharpen the from-space scan and stack-map walk instruments
fix(gc): root heap values held across JS-running calls in runtime and…
feat(gc): name the HOLDERS of a stale from-space address, not just th…
fix(next): pass production App Route dylib gate (#8082)
proggeramlug commentedon Aug 16, 2026
Closing: everything this issue asked for has landed on
mainexcept the forced-evacuation arm, which was split out as #8163 and stays open there.PR #8082's body says "Closes #8034 and #8036", but GitHub only links the first reference in that phrasing (each issue needs its own keyword), so #8034 auto-closed on merge and this one did not. Closing manually per that intent.
What landed, and where it is gated
fe0d49792):test-files/test_gap_gc_next_request_import.ts+test-files/fixtures/gc_next_request_import/(request-shaped class through a re-exported imported handler; allocation, dynamic import, promise + timer continuation, body read; 20 concurrent handlers started before the first await; explicitgc()while all 20 are suspended). Registered intest-parity/gc_repsel_corpus.txt, soscripts/gc_repsel_matrix.shruns it under theforce_verify/verify_evac/cons_scan_offarms ingc-moving-witnesses— green onmain(latest scheduled run 31932625278, 2026-08-16 06:56 UTC,3be2016c1).e75191b93):tests/fixtures/next-app-route/with therouteModule.handlebypass guard armed,tests/test_next_app_route_dylib.shgrepping the host log for the guard (its signal cannot reachverify.mjs's exit code), wired asnext-app-route.yml(workflow_dispatch+ nightly17 3 * * *UTC; merged after today's slot, so its first scheduled run is 2026-08-17).53e8a21e3):tests/release/packages/next-app-route/fixture.sh(Node production oracle, separate runtime/stdlib provider images, untouched webpack output compiled as an app-only dylib, 10 cold processes × 2 verifier passes) — normal arm PASS on that head; plus the stacked defects it took to get there: Scope split unknown-function fallback wrappers by module #8078, fix(codegen): unbreak in-process RS4GC on inline asm and relocation-grown functions #8128, fix(gc): root heap values held across JS-running calls in runtime and ext-http #8131, fix(next): make computed relative chunk requires resolve in a compiled App Route #8146, fix(hir): a nested class's own name beats an enclosing binding innew#8153, fix(gc): prune FUNCTION_CLASS_IDS keys whose closure died (#8040) #8168.What is NOT closed here — acceptance bullet 4, "passes with forced/verified GC": the production route under
PERRY_GC_FORCE_EVACUATE=1 … PERRY_GC_SCHEDULE_SEED=8036still dies withTypeError: value is not a functionfrom a holder outside the GC heap. That is #8163, opt-in in the fixture behindPERRY_NEXT_ROUTE_FORCED_GC=1(off, fails loudly when set — not a SKIP, notcontinue-on-error).One reconciliation note for whoever touches the fixtures next: the release-tier copy's
perry-host.jsdoes not carry the bypass guard thattests/fixtures/next-app-route/perry-host.jshas; it relies on the forbidden-diagnostic grep instead. Both copies should converge on the armed host.turnloop P3: JS timers on a per-agent heap, Node's event-loop phase o…