Repository navigation
[Next.js/dylib] Preserve request/work AsyncLocalStorage through concurrent App Routes #8037
Description
Summary
Make the AsyncLocalStorage subset used by Next.js App Routes correct under the real AppRouteRouteModule.handle path, including concurrent requests and continuations. This is a scoped Next.js acceptance cut of the broader node:async_hooks backlog in #6764, not a duplicate request to complete all 113 parity failures.
The current lower-bound adapter calls the userland GET directly because Perry cannot yet rely on Next's private request/work-store machinery. That bypass must disappear before Perry can claim it runs Next.js.
Reproduction app (provided)
Use the exact pinned app and 21-request verifier attached to #8034. Do not invent a new context test.
The supplied handler reads Next's public headers() API before and after both:
- a dynamic
import()continuation; and - a
setTimeout(..., 1)continuation.
The verifier launches 20 requests concurrently, each with a distinct x-request-id, and requires both reads to equal that request's ID. It then sends a POST as an additional lifecycle check.
BASE_URL=http://127.0.0.1:3100 npm run verifyRequired implementation behavior
- Enter through the generated production
routeModule.handle; callingGETdirectly is invalid because it bypasses the stores being tested. - Support the AsyncLocalStorage instances Next wires for action, request, work, and work-unit state.
- Preserve each store through direct promises,
async/await, dynamic import initialization, microtasks, timers, stream callbacks, and route completion. - Restore the prior store after nested
run()/exit()and after throws or rejections. - Clear request-owned state after the response completes; do not retain a request or leak its store into the next call.
- Isolate concurrent requests even when they share one process, runtime provider, stdlib provider, and event loop.
Regression coverage
Add one focused parity fixture mirroring #8034's continuation sequence, then make the production fixture a dylib integration gate. The focused test should include nested stores, a rejected branch, and two interleaved IDs so failures localize below Next.js.
Update/check off the matching propagation/provider cases in #6764 as they become covered; this issue is complete when the Next route cut passes even if unrelated async_hooks provider tests remain.
Acceptance criteria
- [Next.js/dylib] Add a pinned production App Route parity fixture and CI gate #8034's
beforeAwaitandafterAwaitfields equal the correct request ID in all 20 concurrent responses. - 100 repetitions show zero cross-request contamination, missing store values, hangs, or retained requests.
- A rejected request does not poison the next request's store.
- The production test invokes
AppRouteRouteModule.handleand contains no direct-handler compatibility path. - Relevant focused tests pass in executable and app-only dylib modes.
Baseline / references
- Reproduction app and commands: [Next.js/dylib] Add a pinned production App Route parity fixture and CI gate #8034
- Broad parity inventory: [parity] node:async_hooks — 113 failing node-suite tests (2026-07-22 baseline) #6764
- Next production module groundwork: Next.js 16 standalone app-router bring-up — walls 1–5 (codegen/runtime/HIR fixes) #5438
Activity
proggeramlug commentedon Aug 16, 2026
Status against the production route on
main3c95020f8(2026-08-16, details in the #8040 comment of today): across 500 batches of the 20-concurrent-request verifier (each request reads Next's ALS-backedheaders()before and after a dynamic-import and a timer continuation),beforeAwait/afterAwaitwere never wrong and never missing — zero cross-request contamination, zero hangs. What did fail (8 batches) is a lost response after a default-mode copying minor: #8163's stale closure, not an ALS defect.So the ALS cut itself holds up (focused four-store fixture from #8042 + the production route through the armed
routeModule.handleguard, 0 bypasses in 520 batches). The one bullet this issue cannot claim yet is "100 repetitions … zero retained requests": RSS climbs 160 → ~600 MB and heap-in-use 5 → 40 MB over 100 batches with no plateau, which reads as retention. Leaving open pending #8163 and that retention question; if you consider retention out of this issue's scope, this can close on the evidence above.proggeramlug commentedon Aug 17, 2026
Closing: every acceptance criterion here is met, and the one loose thread has its own issue and owner.
Fix: #8042 (merged) moved the
AsyncLocalStoragecontext ops behind a C ABI owned by the runtime provider, keepingrun()/exit()throw-safe restoration in the runtime image so promise, timer, microtask, dynamic-import and stream schedulers all snapshot the sameACTIVE_CONTEXT.Focused coverage:
test-parity/node-suite/async_hooks/propagation/next-route-continuations.ts(#8042) — action/request/work/work-unit stores, interleaved IDs, nested throw/rejection, post-rejection reuse, lifecycle cleanup. Validated in both executable and app-only dylib modes, 100/100 executable repetitions against the Node oracle. It is a step innext-app-route.yml.Production coverage against the pinned #8034 fixture, where
verify.mjsassertsbeforeAwait/afterAwait— Next's ALS-backedheaders()read either side of a dynamic-import continuation and asetTimeoutcontinuation — equal the request's own ID, for all 20 concurrent requests plus a POST:695daf302bINPROCESS=off183d30c53aAgainst the acceptance criteria as written:
beforeAwait/afterAwaitcorrect in all 20 concurrent responses — across 810 passes on two backends.AppRouteRouteModule.handlewith no direct-handler path: the bypass guard is armed in bothperry-host.jscopies and fired 0 times in every run.The one criterion I am explicitly not claiming, and why it does not block this issue: "zero retained requests". The warm process does retain —
post_in_use5.2 → 130.3 MB and RSS ~882 MB over 300 passes. That is real, but it is not an async-context defect: it was root-caused in #8213 to ~1,100 async box cells per request plus (before #8219) an append-only proxy registry, neither of which is ALS state. No store leaked, no request's context outlived it, and no context was ever observed on the wrong request. Retention is tracked in #8213 with a named mechanism and an owner; leaving this issue open for it would attribute a footprint defect to the async-context subsystem.Also worth recording: the earlier concern that the lower-bound adapter had to call userland
GETdirectly is gone — every number above went through Next's realrouteModule.handle, proven by the guard rather than asserted.Related work that landed alongside: #8220 (the GC rooting defect that made requests intermittently lose their response — not an ALS bug, but it was the reason the production path could not be trusted end to end), #8209, #8210, #8215, #8241, #8274.