mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
In a Node-runtime app route handler, `'use cache'` and fetch-cache stale-while-revalidate block the client response on the full background regeneration. A second request after the cache has gone stale does not return the stale value promptly. It waits the entire regen duration and then returns the freshly regenerated value, as described in issue #93146. The symptom is visible both on self-hosted Node and on Vercel Node. Edge route handlers were unaffected because they route through `edge-route-module-wrapper.ts`, which hands `pendingWaitUntil` directly to `evt.waitUntil` and never awaits it before the response completes. The root cause is independent of the `await ignoredStream.cancel()` change proposed in that issue, and also independent of #92636, which removed a separate blocking await inside `use-cache-wrapper.ts`. Both of those sit inside the `'use cache'` wrapper and affect how the stale entry is returned to the caller. The blocking that remains is in the transport layer and applies equally to any revalidation pushed into `pendingRevalidates` or `pendingRevalidateWrites`, not just `'use cache'`. #74164 migrated pending revalidate handling from the "keep the response stream open until the promise resolves" pattern introduced in #55978 and reinforced in #58744 to a conditional hand-off. If a platform `waitUntil` is available, revalidations run out of band via `ctx.waitUntil`. Otherwise `pipe-readable.ts` keeps `res.end` deferred until they settle, so minimal-mode deployments stay alive long enough for writes to persist. That migration landed correctly for app pages. `app-render.tsx` only assigns `options.waitUntil` in the `else` branch, so once `renderOpts.waitUntil` is present the `pipe-readable.ts` path receives nothing and does not block. The matching code for route handlers was added to `base-server.ts` in the same PR. It declared `let pendingWaitUntil = context.renderOpts.pendingWaitUntil`, cleared it when handing off to `ctx.waitUntil`, and then passed `context.renderOpts.pendingWaitUntil` into `sendResponse`. That is the unmutated property, not the cleared local. The local variable was never read. The hand-off therefore never displaced the pipe-readable await. The same promise was awaited twice. `ctx.waitUntil` registered it redundantly and `pipe-readable.ts` still held `res.end` open for the full revalidation. #80189 copied the block verbatim into `packages/next/src/build/templates/app-route.ts`, carrying the bug forward when response handling moved into the route template. Fixing it is a one-line change. Pass the local `pendingWaitUntil` (which is `undefined` once handed off) into `sendResponse`, so Node route handlers match both app pages and edge route handlers and the original intent of #74164 is restored. The existing `stale-cache-serving/route-handler` test in `app-static.test.ts` did not catch this. It only measured time-to-first-byte against the route start time, and first-byte was already fast pre-fix. Chunks are written before `pipe-readable.ts`'s close handler awaits `waitUntilForEnd`. Only the terminating chunk was delayed. The test is updated to also assert total response time against the route start. Pre-fix that new assertion fails at ~3000ms for the Node route handler variant while the page and edge variants continue to pass, which is exactly the scope of the regression. A new e2e test in `use-cache-swr` exercises the symmetric `'use cache'` path through a route handler fixture. fixes #93146 closes #93177 (incorrect fix) closes #93188 (incorrect fix)