next.js
fea75474 - Route Handlers: Fix devRequestTimingInternalsEnd and Turbopack server HMR (#92271)

Commit
47 days ago
Route Handlers: Fix devRequestTimingInternalsEnd and Turbopack server HMR (#92271) ### What? Fixes issues with `AppRouteRouteModule` that interact with `devRequestTiming` and Turbopack server HMR. 1. **Incorrect `devRequestTimingInternalsEnd` attribution**: The app-route template previously statically imported userland (`import * as userland from 'VAR_USERLAND'`), meaning module load time was included in the framework's startup overhead. The `devRequestTimingInternalsEnd` timestamp is now set immediately before `routeModule.handle()` is called, and userland is loaded lazily on the first request so the timing correctly captures the framework→application boundary. 2. **Stale route handlers after Turbopack server HMR**: After a Turbopack server HMR update, `devModuleCache` is updated with a fresh route module, but the entry chunk remains in Node.js `require.cache`. The lazy `_userlandFactory` only runs once and would cache the old module reference — so route handler changes were not reflected after HMR. This restores a synchronous per-request `getUserland` getter that calls `require('VAR_USERLAND')` on every request. Since `require()` is a synchronous `devModuleCache` lookup, it has negligible overhead and doesn't add async time that would be incorrectly attributed to `application-code`. 3. **Route handlers with top-level await**: `require()` returns a Promise for modules that use top-level await. The module now accepts a lazy factory for `userland` and handles this case via `ensureUserland()`, which is called before the first request is handled. 4. **`output: export` route validation**: With the lazy factory, validation errors (e.g. `dynamic = "force-dynamic"` on an exported route) were deferred to request time, returning a 500 instead of surfacing as a Redbox in dev. The constructor now eagerly calls the factory for `output: export` routes so `_initFromUserland()` throws at module-load time — preserving the Redbox error. 5. **`node:stream` in edge bundles** (CI fix): `render-result.ts` was missing a `NEXT_RUNTIME === 'edge'` compile-time guard around `require('node:stream')` / `__non_webpack_require__('node:stream')`, which prevented webpack from DCE-ing those branches in edge builds. Vercel's deploy adapter rejects edge functions that reference `node:stream`. The guard is now restored to match the canary pattern. ### Why? #91466 enabled server HMR for app route handlers and introduced `getUserland: () => import('VAR_USERLAND')` to pick up HMR updates on each request. However, the async `import()` adds latency incorrectly attributed to application-code time in `devRequestTiming`. This PR replaces `import()` with synchronous `require()` and also lazily loads userland on the first request for accurate `devRequestTimingInternalsEnd` placement. ### How? - **Template** (`app-route.ts`): Replaces the static `import * as userland` with a lazy `() => require('VAR_USERLAND')` factory. In Turbopack dev mode, also provides `getUserland: () => require('VAR_USERLAND')` — a synchronous getter called on every request to pick up server HMR updates. - **Module** (`module.ts`): - Accepts `userland` as either a direct module or a lazy factory. - For `output: export` routes, calls the factory eagerly in the constructor so `_initFromUserland()` validation errors throw at module-load time (Redbox) rather than at request time (500). - For async modules (top-level await), stores the pending Promise in `_pendingUserland` so `ensureUserland()` can await it before the first request. - Accepts optional synchronous `getUserland` for per-request live userland access (Turbopack dev HMR). - `handle()` calls `_getUserland?.()` at request start; if set, uses the result for handler resolution, `fetchCache`, `hasNonStaticMethods`, and `dynamic` — ensuring HMR changes to all exported values are immediately reflected. - Extracts `_initFromUserland()` helper to deduplicate initialization between lazy and eager paths. - **Route module base** (`route-module.ts`): Makes `isDev` public for template access. - **Test** (`app-custom-routes.test.ts` + fixture): Adds an integration test for route handlers that use top-level await, ensuring the handler is correctly resolved even when `require()` returns a Promise. - **`render-result.ts`**: Restores the `if (process.env.NEXT_RUNTIME === 'edge') { throw }` guard matching canary, so `node:stream` is DCE'd from edge builds. Fixes #91318 --------- Co-authored-by: Will Binns-Smith <wbinnssmith@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
Author
Parents
Loading