Skip to content

Commit b242927

Browse files
authoredMar 7, 2025
fix: dynamic not-prerendered routes revalidate tracking (#2771)
* fix: dynamic not-prerendered routes revalidate tracking * fix: correct typeof check * test: adjust cache-status assertions for stale responses serverd by durable * test: ensure we don't prerender API responses for og image in test fixture
1 parent 3301077 commit b242927

File tree

7 files changed

+84
-32
lines changed

7 files changed

+84
-32
lines changed
 

‎src/run/handlers/cache.cts

+49-27
Original file line numberDiff line numberDiff line change
@@ -183,37 +183,56 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
183183

184184
private async injectEntryToPrerenderManifest(
185185
key: string,
186-
revalidate: NetlifyCachedPageValue['revalidate'],
186+
{ revalidate, cacheControl }: Pick<NetlifyCachedPageValue, 'revalidate' | 'cacheControl'>,
187187
) {
188-
if (this.options.serverDistDir && (typeof revalidate === 'number' || revalidate === false)) {
188+
if (
189+
this.options.serverDistDir &&
190+
(typeof revalidate === 'number' ||
191+
revalidate === false ||
192+
typeof cacheControl !== 'undefined')
193+
) {
189194
try {
190195
const { loadManifest } = await import('next/dist/server/load-manifest.js')
191196
const prerenderManifest = loadManifest(
192197
join(this.options.serverDistDir, '..', 'prerender-manifest.json'),
193198
) as PrerenderManifest
194199

195-
try {
196-
const { normalizePagePath } = await import(
197-
'next/dist/shared/lib/page-path/normalize-page-path.js'
200+
if (typeof cacheControl !== 'undefined') {
201+
// instead of `revalidate` property, we might get `cacheControls` ( https://github.com/vercel/next.js/pull/76207 )
202+
// then we need to keep track of revalidate values via SharedCacheControls
203+
const { SharedCacheControls } = await import(
204+
// @ts-expect-error supporting multiple next version, this module is not resolvable with currently used dev dependency
205+
// eslint-disable-next-line import/no-unresolved, n/no-missing-import
206+
'next/dist/server/lib/incremental-cache/shared-cache-controls.js'
198207
)
199-
200-
prerenderManifest.routes[key] = {
201-
experimentalPPR: undefined,
202-
dataRoute: posixJoin('/_next/data', `${normalizePagePath(key)}.json`),
203-
srcRoute: null, // FIXME: provide actual source route, however, when dynamically appending it doesn't really matter
204-
initialRevalidateSeconds: revalidate,
205-
// Pages routes do not have a prefetch data route.
206-
prefetchDataRoute: undefined,
208+
const sharedCacheControls = new SharedCacheControls(prerenderManifest)
209+
sharedCacheControls.set(key, cacheControl)
210+
} else if (typeof revalidate === 'number' || revalidate === false) {
211+
// if we don't get cacheControls, but we still get revalidate, it should mean we are before
212+
// https://github.com/vercel/next.js/pull/76207
213+
try {
214+
const { normalizePagePath } = await import(
215+
'next/dist/shared/lib/page-path/normalize-page-path.js'
216+
)
217+
218+
prerenderManifest.routes[key] = {
219+
experimentalPPR: undefined,
220+
dataRoute: posixJoin('/_next/data', `${normalizePagePath(key)}.json`),
221+
srcRoute: null, // FIXME: provide actual source route, however, when dynamically appending it doesn't really matter
222+
initialRevalidateSeconds: revalidate,
223+
// Pages routes do not have a prefetch data route.
224+
prefetchDataRoute: undefined,
225+
}
226+
} catch {
227+
// depending on Next.js version - prerender manifest might not be mutable
228+
// https://github.com/vercel/next.js/pull/64313
229+
// if it's not mutable we will try to use SharedRevalidateTimings ( https://github.com/vercel/next.js/pull/64370) instead
230+
const { SharedRevalidateTimings } = await import(
231+
'next/dist/server/lib/incremental-cache/shared-revalidate-timings.js'
232+
)
233+
const sharedRevalidateTimings = new SharedRevalidateTimings(prerenderManifest)
234+
sharedRevalidateTimings.set(key, revalidate)
207235
}
208-
} catch {
209-
// depending on Next.js version - prerender manifest might not be mutable
210-
// https://github.com/vercel/next.js/pull/64313
211-
// if it's not mutable we will try to use SharedRevalidateTimings ( https://github.com/vercel/next.js/pull/64370) instead
212-
const { SharedRevalidateTimings } = await import(
213-
'next/dist/server/lib/incremental-cache/shared-revalidate-timings.js'
214-
)
215-
const sharedRevalidateTimings = new SharedRevalidateTimings(prerenderManifest)
216-
sharedRevalidateTimings.set(key, revalidate)
217236
}
218237
} catch {}
219238
}
@@ -315,7 +334,7 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
315334

316335
span.addEvent(blob.value?.kind, { lastModified: blob.lastModified, revalidate, ttl })
317336

318-
await this.injectEntryToPrerenderManifest(key, revalidate)
337+
await this.injectEntryToPrerenderManifest(key, blob.value)
319338

320339
return {
321340
lastModified: blob.lastModified,
@@ -327,7 +346,7 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
327346

328347
span.addEvent(blob.value?.kind, { lastModified: blob.lastModified, revalidate, ttl })
329348

330-
await this.injectEntryToPrerenderManifest(key, revalidate)
349+
await this.injectEntryToPrerenderManifest(key, blob.value)
331350

332351
return {
333352
lastModified: blob.lastModified,
@@ -355,22 +374,25 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
355374
if (isCachedRouteValue(data)) {
356375
return {
357376
...data,
358-
revalidate: context.revalidate,
377+
revalidate: context.revalidate ?? context.cacheControl?.revalidate,
378+
cacheControl: context.cacheControl,
359379
body: data.body.toString('base64'),
360380
}
361381
}
362382

363383
if (isCachedPageValue(data)) {
364384
return {
365385
...data,
366-
revalidate: context.revalidate,
386+
revalidate: context.revalidate ?? context.cacheControl?.revalidate,
387+
cacheControl: context.cacheControl,
367388
}
368389
}
369390

370391
if (data?.kind === 'APP_PAGE') {
371392
return {
372393
...data,
373-
revalidate: context.revalidate,
394+
revalidate: context.revalidate ?? context.cacheControl?.revalidate,
395+
cacheControl: context.cacheControl,
374396
rscData: data.rscData?.toString('base64'),
375397
}
376398
}

‎src/shared/cache-types.cts

+23-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ import type {
1212

1313
export type { CacheHandlerContext } from 'next/dist/server/lib/incremental-cache/index.js'
1414

15+
type CacheControl = {
16+
revalidate: Parameters<CacheHandler['set']>[2]['revalidate']
17+
expire: number | undefined
18+
}
19+
1520
/**
1621
* Shape of the cache value that is returned from CacheHandler.get or passed to CacheHandler.set
1722
*/
@@ -28,6 +33,7 @@ export type NetlifyCachedRouteValue = Omit<CachedRouteValueForMultipleVersions,
2833
// Next.js doesn't produce cache-control tag we use to generate cdn cache control
2934
// so store needed values as part of cached response data
3035
revalidate?: Parameters<CacheHandler['set']>[2]['revalidate']
36+
cacheControl?: CacheControl
3137
}
3238

3339
/**
@@ -50,6 +56,7 @@ export type NetlifyCachedAppPageValue = Omit<
5056
// Next.js stores rscData as buffer, while we store it as base64 encoded string
5157
rscData: string | undefined
5258
revalidate?: Parameters<CacheHandler['set']>[2]['revalidate']
59+
cacheControl?: CacheControl
5360
}
5461

5562
/**
@@ -64,6 +71,7 @@ type IncrementalCachedPageValueForMultipleVersions = Omit<IncrementalCachedPageV
6471
*/
6572
export type NetlifyCachedPageValue = IncrementalCachedPageValueForMultipleVersions & {
6673
revalidate?: Parameters<CacheHandler['set']>[2]['revalidate']
74+
cacheControl?: CacheControl
6775
}
6876

6977
export type CachedFetchValueForMultipleVersions = Omit<CachedFetchValue, 'kind'> & {
@@ -131,4 +139,18 @@ type MapCacheHandlerClassMethod<T> = T extends (...args: infer Args) => infer Re
131139

132140
type MapCacheHandlerClass<T> = { [K in keyof T]: MapCacheHandlerClassMethod<T[K]> }
133141

134-
export type CacheHandlerForMultipleVersions = MapCacheHandlerClass<CacheHandler>
142+
type BaseCacheHandlerForMultipleVersions = MapCacheHandlerClass<CacheHandler>
143+
144+
type CacheHandlerSetContext = Parameters<CacheHandler['set']>[2]
145+
146+
type CacheHandlerSetContextForMultipleVersions = CacheHandlerSetContext & {
147+
cacheControl?: CacheControl
148+
}
149+
150+
export type CacheHandlerForMultipleVersions = BaseCacheHandlerForMultipleVersions & {
151+
set: (
152+
key: Parameters<BaseCacheHandlerForMultipleVersions['set']>[0],
153+
value: Parameters<BaseCacheHandlerForMultipleVersions['set']>[1],
154+
context: CacheHandlerSetContextForMultipleVersions,
155+
) => ReturnType<BaseCacheHandlerForMultipleVersions['set']>
156+
}

‎tests/e2e/page-router.test.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -413,12 +413,12 @@ test.describe('Simple Page Router (no basePath, no i18n)', () => {
413413
expect(date1.localeCompare(beforeFirstFetch)).toBeGreaterThan(0)
414414

415415
// allow page to get stale
416-
await page.waitForTimeout(60_000)
416+
await page.waitForTimeout(61_000)
417417

418418
const response2 = await page.goto(new URL(pathname, pageRouter.url).href)
419419
expect(response2?.status()).toBe(200)
420420
expect(response2?.headers()['cache-status']).toMatch(
421-
/"Netlify (Edge|Durable)"; hit; fwd=stale/m,
421+
/("Netlify Edge"; hit; fwd=stale|"Netlify Durable"; hit; ttl=-[0-9]+)/m,
422422
)
423423
expect(response2?.headers()['netlify-cdn-cache-control']).toMatch(
424424
/s-maxage=60, stale-while-revalidate=[0-9]+, durable/,
@@ -436,8 +436,8 @@ test.describe('Simple Page Router (no basePath, no i18n)', () => {
436436
const response3 = await page.goto(new URL(pathname, pageRouter.url).href)
437437
expect(response3?.status()).toBe(200)
438438
expect(response3?.headers()['cache-status']).toMatch(
439-
// hit, without being followed by ';fwd=stale'
440-
/"Netlify (Edge|Durable)"; hit(?!; fwd=stale)/m,
439+
// hit, without being followed by ';fwd=stale' for edge or negative TTL for durable, optionally with fwd=stale
440+
/("Netlify Edge"; hit(?!; fwd=stale)|"Netlify Durable"; hit(?!; ttl=-[0-9]+))/m,
441441
)
442442
expect(response3?.headers()['netlify-cdn-cache-control']).toMatch(
443443
/s-maxage=60, stale-while-revalidate=[0-9]+, durable/,

‎tests/fixtures/wasm-src/src/app/og-node/route.js

+2
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ export async function GET() {
66
height: 630,
77
})
88
}
9+
10+
export const dynamic = 'force-dynamic'

‎tests/fixtures/wasm-src/src/app/og/route.js

+2
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ export async function GET() {
88
}
99

1010
export const runtime = 'edge'
11+
12+
export const dynamic = 'force-dynamic'

‎tests/fixtures/wasm/app/og-node/route.js

+2
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ export async function GET() {
66
height: 630,
77
})
88
}
9+
10+
export const dynamic = 'force-dynamic'

‎tests/fixtures/wasm/app/og/route.js

+2
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ export async function GET() {
88
}
99

1010
export const runtime = 'edge'
11+
12+
export const dynamic = 'force-dynamic'

0 commit comments

Comments
 (0)
Please sign in to comment.