[GH-ISSUE #959] Don't block route handler response on background SWR regeneration (Node runtime parity) #207

Closed
opened 2026-05-06 12:38:10 +02:00 by BreizhHardware · 0 comments

Originally created by @github-actions[bot] on GitHub (Apr 29, 2026).
Original GitHub issue: https://github.com/cloudflare/vinext/issues/959

Next.js Change

In a Node-runtime app route handler, 'use cache' and fetch-cache stale-while-revalidate were blocking the client response on the full background regeneration. A second request after the cache had gone stale did not return the stale value promptly — it waited the entire regen duration and then returned the freshly regenerated value. 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.

Commit: 521ae96
PR: #93189
Fixes: #93146

What Changed

In packages/next/src/build/templates/app-route.ts, the route-handler response template:

  • Declared let pendingWaitUntil = context.renderOpts.pendingWaitUntil, cleared it when handing off to ctx.waitUntil, but then passed context.renderOpts.pendingWaitUntil (the unmutated property, not the cleared local) into sendResponse.
  • The local variable was never read, so the hand-off never displaced the pipe-readable.ts await, and the same promise was awaited twice: ctx.waitUntil registered it redundantly and pipe-readable.ts still held res.end open for the full revalidation.

The fix 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, restoring the original intent of #74164.

The bug was originally introduced when route handler response handling was copied verbatim into the route template in #80189.

Impact on vinext

vinext implements its own App Router route handler runtime and ISR/SWR plumbing. The Next.js architecture relies on a clear split:

  • If a platform waitUntil is available (Cloudflare Workers, Vercel) → background revalidations run out of band via ctx.waitUntil and the response stream completes immediately.
  • Otherwise → pipe-readable.ts keeps res.end deferred until pending revalidates settle, so minimal-mode deployments stay alive long enough for writes to persist.

On Cloudflare Workers, vinext should always be in the "platform waitUntil available" branch — ctx.waitUntil is the standard Workers API. The risk to vinext is whether its route handler implementation correctly hands off pending revalidates to ctx.waitUntil and does not also await them before completing the response. If it does both (or neither), the symptom is identical to the Next.js bug: SWR appears to block the client.

Suggested Action

  • Audit the App Router route handler runtime in vinext (production server entry / cloudflare/worker-entry.ts and any Pages Router route equivalents) to confirm pendingWaitUntil / pendingRevalidates are forwarded to ctx.waitUntil and not also awaited before the response is flushed.
  • The same audit should cover 'use cache' regen and fetch-cache regen, since both feed into the same pending-revalidate queue.
  • Add a test mirroring the new e2e fixture in test/e2e/app-dir/use-cache-swr/app/delayed-route that asserts total response time on the second (stale) request is fast — not the full regen duration.
  • Update the existing route-handler stale-cache test (or equivalent) to assert total response time, not just time-to-first-byte. The Next.js test only checked TTFB and missed this regression because chunks were written before the close handler awaited waitUntilForEnd; only the terminating chunk was delayed.
Originally created by @github-actions[bot] on GitHub (Apr 29, 2026). Original GitHub issue: https://github.com/cloudflare/vinext/issues/959 ## Next.js Change In a Node-runtime app route handler, `'use cache'` and fetch-cache stale-while-revalidate were blocking the client response on the full background regeneration. A second request after the cache had gone stale did not return the stale value promptly — it waited the entire regen duration and then returned the freshly regenerated value. 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. **Commit:** [`521ae96`](https://github.com/vercel/next.js/commit/521ae9653e7c45ee1690323c86252b82d5740ffa) **PR:** [#93189](https://github.com/vercel/next.js/pull/93189) **Fixes:** [#93146](https://github.com/vercel/next.js/issues/93146) ## What Changed In `packages/next/src/build/templates/app-route.ts`, the route-handler response template: - Declared `let pendingWaitUntil = context.renderOpts.pendingWaitUntil`, cleared it when handing off to `ctx.waitUntil`, but then passed `context.renderOpts.pendingWaitUntil` (the **unmutated** property, not the cleared local) into `sendResponse`. - The local variable was never read, so the hand-off never displaced the `pipe-readable.ts` await, and the same promise was awaited twice: `ctx.waitUntil` registered it redundantly **and** `pipe-readable.ts` still held `res.end` open for the full revalidation. The fix 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, restoring the original intent of #74164. The bug was originally introduced when route handler response handling was copied verbatim into the route template in #80189. ## Impact on vinext vinext implements its own App Router route handler runtime and ISR/SWR plumbing. The Next.js architecture relies on a clear split: - If a platform `waitUntil` is available (Cloudflare Workers, Vercel) → background revalidations run out of band via `ctx.waitUntil` and the response stream completes immediately. - Otherwise → `pipe-readable.ts` keeps `res.end` deferred until pending revalidates settle, so minimal-mode deployments stay alive long enough for writes to persist. On Cloudflare Workers, vinext should always be in the "platform `waitUntil` available" branch — `ctx.waitUntil` is the standard Workers API. The risk to vinext is whether its route handler implementation correctly hands off pending revalidates to `ctx.waitUntil` and does **not** also `await` them before completing the response. If it does both (or neither), the symptom is identical to the Next.js bug: SWR appears to block the client. ## Suggested Action - Audit the App Router route handler runtime in vinext (production server entry / `cloudflare/worker-entry.ts` and any Pages Router route equivalents) to confirm `pendingWaitUntil` / `pendingRevalidates` are forwarded to `ctx.waitUntil` and **not** also awaited before the response is flushed. - The same audit should cover `'use cache'` regen and fetch-cache regen, since both feed into the same pending-revalidate queue. - Add a test mirroring the new e2e fixture in `test/e2e/app-dir/use-cache-swr/app/delayed-route` that asserts total response time on the second (stale) request is fast — not the full regen duration. - Update the existing route-handler stale-cache test (or equivalent) to assert **total** response time, not just time-to-first-byte. The Next.js test only checked TTFB and missed this regression because chunks were written before the close handler awaited `waitUntilForEnd`; only the terminating chunk was delayed.
BreizhHardware 2026-05-06 12:38:10 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#207
No description provided.