Skip to content

[pull] canary from vercel:canary#1006

Merged
pull[bot] merged 2 commits intocode:canaryfrom
vercel:canary
Apr 28, 2026
Merged

[pull] canary from vercel:canary#1006
pull[bot] merged 2 commits intocode:canaryfrom
vercel:canary

Conversation

@pull
Copy link
Copy Markdown

@pull pull Bot commented Apr 28, 2026

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

unstubbable and others added 2 commits April 28, 2026 18:40
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)
The process of signing commits with GH API was stripping newlines. This
configures that step to not strip newlines so lint doesn't fail. x-ref:
https://github.com/vercel/next.js/actions/runs/25007903789/job/73240317094

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
@pull pull Bot locked and limited conversation to collaborators Apr 28, 2026
@pull pull Bot added the ⤵️ pull label Apr 28, 2026
@pull pull Bot merged commit 0ea2845 into code:canary Apr 28, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants