Honor the route-level expire value with blocking revalidation#93211
Honor the route-level expire value with blocking revalidation#93211unstubbable wants to merge 1 commit intocanaryfrom
expire value with blocking revalidation#93211Conversation
40b8bdc to
83b9721
Compare
Failing test suitesCommit: 924f3b9 | About building and testing Next.js
Expand output● expire-time › should do a blocking revalidation when the cache entry has expired |
Stats from current PR🔴 1 regression, 1 improvement
📊 All Metrics📖 Metrics GlossaryDev Server Metrics:
Build Metrics:
Change Thresholds:
⚡ Dev Server
📦 Dev Server (Webpack) (Legacy)📦 Dev Server (Webpack)
⚡ Production Builds
📦 Production Builds (Webpack) (Legacy)📦 Production Builds (Webpack)
📦 Bundle SizesBundle Sizes⚡ TurbopackClient Main Bundles
Server Middleware
Build DetailsBuild Manifests
📦 WebpackClient Main Bundles
Polyfills
Pages
Server Edge SSR
Middleware
Build DetailsBuild Manifests
Build Cache
🔄 Shared (bundler-independent)Runtimes
📝 Changed Files (25 files)Files with changes:
View diffsapp-page-exp..ntime.dev.jsfailed to diffapp-page-exp..time.prod.jsDiff too large to display app-page-tur..ntime.dev.jsfailed to diffapp-page-tur..time.prod.jsDiff too large to display app-page-tur..ntime.dev.jsfailed to diffapp-page-tur..time.prod.jsDiff too large to display app-page.runtime.dev.jsfailed to diffapp-page.runtime.prod.jsDiff too large to display app-route-ex..ntime.dev.jsDiff too large to display app-route-ex..time.prod.jsDiff too large to display app-route-tu..ntime.dev.jsDiff too large to display app-route-tu..time.prod.jsDiff too large to display app-route-tu..ntime.dev.jsDiff too large to display app-route-tu..time.prod.jsDiff too large to display app-route.runtime.dev.jsDiff too large to display app-route.ru..time.prod.jsDiff too large to display pages-api-tu..ntime.dev.jsDiff too large to display pages-api-tu..time.prod.jsDiff too large to display pages-api.runtime.dev.jsDiff too large to display pages-api.ru..time.prod.jsDiff too large to display pages-turbo...ntime.dev.jsDiff too large to display pages-turbo...time.prod.jsDiff too large to display pages.runtime.dev.jsDiff too large to display pages.runtime.prod.jsDiff too large to display server.runtime.prod.jsDiff too large to display 📎 Tarball URLCommit: 924f3b9 |
A prerendered route's `expire` — set via `cacheLife({ expire })` inside
`'use cache'` or via the `expireTime` config fallback — lands in the
prerender manifest as `initialExpireSeconds` / `fallbackExpire`
(#76207), but the runtime never read it: `IncrementalCache.get` only
considered `revalidate`. So past expire, Next.js served stale with a
background refresh instead of the blocking regeneration the
`cacheLife` `expire` docs describe.
The fix is two coordinated changes. `IncrementalCache.get` now returns
`isStale = -1` when `lastModified + expire * 1000 < now`, and
`response-cache.handleGet` skips its early `resolve(previousEntry)` for
`isStale === -1` so the blocking revalidation inside `responseGenerator`
(which already picks `BLOCKING_STATIC_RENDER` on that signal) can return
its fresh output to the user. Previously the early resolve committed the
stale value to the response first, so even though `responseGenerator`
still ran a fresh render its output only warmed the cache for the next
request. As a side effect this also closes the same early-resolve hole
on the existing tag-expired `isStale = -1` path.
On Vercel, ISR cache decisions live at the Proxy and the Proxy currently
ignores `staleExpiration` (using a hard-coded one-year value instead).
It is also expected, once it starts honoring `staleExpiration`, to pick
up updated values from the `stale-while-revalidate` response header.
Until that lands this change is only observable on `next start` —
deploy-mode behavior is tracked independently of Next.js.
Two test suites cover the new behavior.
`test/production/app-dir/use-cache-expire` uses `cacheComponents` +
`cacheLife({ expire: 300 })` with a custom cache handler that shifts
`lastModified` via an `x-test-cache-age-offset-ms` header, exercising
the fully-static shell, the partially-static route shell for a known
param, and the partially-static fallback shell for unknown params.
`test/e2e/app-dir/expire-time` covers classic ISR (`revalidate = 1`,
`expireTime: 2`) with a real three-second wait and is `it.failing` on
deploy, so it will flip the moment the Proxy honors the expire value.
fixes #78269
83b9721 to
924f3b9
Compare
A prerendered route's
expire— set viacacheLife({ expire })inside'use cache'or via theexpireTimeconfig fallback — lands in the prerender manifest asinitialExpireSeconds/fallbackExpire(#76207), but the runtime never read it:IncrementalCache.getonly consideredrevalidate. So past expire, Next.js served stale with a background refresh instead of the blocking regeneration thecacheLifeexpiredocs describe.The fix is two coordinated changes.
IncrementalCache.getnow returnsisStale = -1whenlastModified + expire * 1000 < now, andresponse-cache.handleGetskips its earlyresolve(previousEntry)forisStale === -1so the blocking revalidation insideresponseGenerator(which already picksBLOCKING_STATIC_RENDERon that signal) can return its fresh output to the user. Previously the early resolve committed the stale value to the response first, so even thoughresponseGeneratorstill ran a fresh render its output only warmed the cache for the next request. As a side effect this also closes the same early-resolve hole on the existing tag-expiredisStale = -1path.On Vercel, ISR cache decisions live at the Proxy and the Proxy currently ignores
staleExpiration(using a hard-coded one-year value instead). It is also expected, once it starts honoringstaleExpiration, to pick up updated values from thestale-while-revalidateresponse header. Until that lands this change is only observable onnext start— deploy-mode behavior is tracked independently of Next.js.Two test suites cover the new behavior.
test/production/app-dir/use-cache-expireusescacheComponents+cacheLife({ expire: 300 })with a custom cache handler that shiftslastModifiedvia anx-test-cache-age-offset-msheader, exercising the fully-static shell, the partially-static route shell for a known param, and the partially-static fallback shell for unknown params.test/e2e/app-dir/expire-timecovers classic ISR (revalidate = 1,expireTime: 2) with a real three-second wait and isit.failingon deploy, so it will flip the moment the Proxy honors the expire value.fixes #78269