Skip to content

Commit bda731f

Browse files
authored
Client router should discard stale prefetch entries for static pages (#79362)
Backport of #79309 to 15.3.x
1 parent d9ec4a4 commit bda731f

File tree

6 files changed

+78
-56
lines changed

6 files changed

+78
-56
lines changed

packages/next/src/client/components/router-reducer/fetch-server-response.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,13 @@ export async function fetchServerResponse(
187187
const contentType = res.headers.get('content-type') || ''
188188
const interception = !!res.headers.get('vary')?.includes(NEXT_URL)
189189
const postponed = !!res.headers.get(NEXT_DID_POSTPONE_HEADER)
190-
const staleTimeHeader = res.headers.get(NEXT_ROUTER_STALE_TIME_HEADER)
190+
const staleTimeHeaderSeconds = res.headers.get(
191+
NEXT_ROUTER_STALE_TIME_HEADER
192+
)
191193
const staleTime =
192-
staleTimeHeader !== null ? parseInt(staleTimeHeader, 10) : -1
194+
staleTimeHeaderSeconds !== null
195+
? parseInt(staleTimeHeaderSeconds, 10) * 1000
196+
: -1
193197
let isFlightResponse = contentType.startsWith(RSC_CONTENT_TYPE_HEADER)
194198

195199
if (process.env.NODE_ENV === 'production') {

packages/next/src/export/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ async function exportAppImpl(
390390
experimental: {
391391
clientTraceMetadata: nextConfig.experimental.clientTraceMetadata,
392392
expireTime: nextConfig.expireTime,
393+
staleTimes: nextConfig.experimental.staleTimes,
393394
dynamicIO: nextConfig.experimental.dynamicIO ?? false,
394395
clientSegmentCache:
395396
nextConfig.experimental.clientSegmentCache === 'client-only'

packages/next/src/server/app-render/app-render.tsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2745,6 +2745,12 @@ async function prerenderToStream(
27452745
setMetadataHeader(name)
27462746
}
27472747

2748+
const selectStaleTime = (stale: number) =>
2749+
stale === INFINITE_CACHE &&
2750+
typeof renderOpts.experimental.staleTimes?.static === 'number'
2751+
? renderOpts.experimental.staleTimes.static
2752+
: stale
2753+
27482754
let prerenderStore: PrerenderStore | null = null
27492755

27502756
try {
@@ -3166,7 +3172,7 @@ async function prerenderToStream(
31663172
// TODO: Should this include the SSR pass?
31673173
collectedRevalidate: finalRenderPrerenderStore.revalidate,
31683174
collectedExpire: finalRenderPrerenderStore.expire,
3169-
collectedStale: finalRenderPrerenderStore.stale,
3175+
collectedStale: selectStaleTime(finalRenderPrerenderStore.stale),
31703176
collectedTags: finalRenderPrerenderStore.tags,
31713177
}
31723178
} else {
@@ -3228,7 +3234,7 @@ async function prerenderToStream(
32283234
// TODO: Should this include the SSR pass?
32293235
collectedRevalidate: finalRenderPrerenderStore.revalidate,
32303236
collectedExpire: finalRenderPrerenderStore.expire,
3231-
collectedStale: finalRenderPrerenderStore.stale,
3237+
collectedStale: selectStaleTime(finalRenderPrerenderStore.stale),
32323238
collectedTags: finalRenderPrerenderStore.tags,
32333239
}
32343240
}
@@ -3653,7 +3659,7 @@ async function prerenderToStream(
36533659
// TODO: Should this include the SSR pass?
36543660
collectedRevalidate: finalServerPrerenderStore.revalidate,
36553661
collectedExpire: finalServerPrerenderStore.expire,
3656-
collectedStale: finalServerPrerenderStore.stale,
3662+
collectedStale: selectStaleTime(finalServerPrerenderStore.stale),
36573663
collectedTags: finalServerPrerenderStore.tags,
36583664
}
36593665
}
@@ -3802,7 +3808,7 @@ async function prerenderToStream(
38023808
// TODO: Should this include the SSR pass?
38033809
collectedRevalidate: reactServerPrerenderStore.revalidate,
38043810
collectedExpire: reactServerPrerenderStore.expire,
3805-
collectedStale: reactServerPrerenderStore.stale,
3811+
collectedStale: selectStaleTime(reactServerPrerenderStore.stale),
38063812
collectedTags: reactServerPrerenderStore.tags,
38073813
}
38083814
} else if (fallbackRouteParams && fallbackRouteParams.size > 0) {
@@ -3822,7 +3828,7 @@ async function prerenderToStream(
38223828
// TODO: Should this include the SSR pass?
38233829
collectedRevalidate: reactServerPrerenderStore.revalidate,
38243830
collectedExpire: reactServerPrerenderStore.expire,
3825-
collectedStale: reactServerPrerenderStore.stale,
3831+
collectedStale: selectStaleTime(reactServerPrerenderStore.stale),
38263832
collectedTags: reactServerPrerenderStore.tags,
38273833
}
38283834
} else {
@@ -3882,7 +3888,7 @@ async function prerenderToStream(
38823888
// TODO: Should this include the SSR pass?
38833889
collectedRevalidate: reactServerPrerenderStore.revalidate,
38843890
collectedExpire: reactServerPrerenderStore.expire,
3885-
collectedStale: reactServerPrerenderStore.stale,
3891+
collectedStale: selectStaleTime(reactServerPrerenderStore.stale),
38863892
collectedTags: reactServerPrerenderStore.tags,
38873893
}
38883894
}
@@ -3975,7 +3981,7 @@ async function prerenderToStream(
39753981
// TODO: Should this include the SSR pass?
39763982
collectedRevalidate: prerenderLegacyStore.revalidate,
39773983
collectedExpire: prerenderLegacyStore.expire,
3978-
collectedStale: prerenderLegacyStore.stale,
3984+
collectedStale: selectStaleTime(prerenderLegacyStore.stale),
39793985
collectedTags: prerenderLegacyStore.tags,
39803986
}
39813987
}
@@ -4156,8 +4162,9 @@ async function prerenderToStream(
41564162
prerenderStore !== null ? prerenderStore.revalidate : INFINITE_CACHE,
41574163
collectedExpire:
41584164
prerenderStore !== null ? prerenderStore.expire : INFINITE_CACHE,
4159-
collectedStale:
4160-
prerenderStore !== null ? prerenderStore.stale : INFINITE_CACHE,
4165+
collectedStale: selectStaleTime(
4166+
prerenderStore !== null ? prerenderStore.stale : INFINITE_CACHE
4167+
),
41614168
collectedTags: prerenderStore !== null ? prerenderStore.tags : null,
41624169
}
41634170
} catch (finalErr: any) {

packages/next/src/server/app-render/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import type { LoadComponentsReturnType } from '../load-components'
22
import type { ServerRuntime, SizeLimit } from '../../types'
3-
import type { NextConfigComplete } from '../../server/config-shared'
3+
import type {
4+
ExperimentalConfig,
5+
NextConfigComplete,
6+
} from '../../server/config-shared'
47
import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight-manifest-plugin'
58
import type { NextFontManifest } from '../../build/webpack/plugins/next-font-manifest-plugin'
69
import type { ParsedUrlQuery } from 'querystring'
@@ -211,6 +214,7 @@ export interface RenderOptsPartial {
211214
*/
212215
isRoutePPREnabled?: boolean
213216
expireTime: number | undefined
217+
staleTimes: ExperimentalConfig['staleTimes'] | undefined
214218
clientTraceMetadata: string[] | undefined
215219
dynamicIO: boolean
216220
clientSegmentCache: boolean | 'client-only'

packages/next/src/server/base-server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,7 @@ export default abstract class Server<
601601
htmlLimitedBots: this.nextConfig.htmlLimitedBots,
602602
experimental: {
603603
expireTime: this.nextConfig.expireTime,
604+
staleTimes: this.nextConfig.experimental.staleTimes,
604605
clientTraceMetadata: this.nextConfig.experimental.clientTraceMetadata,
605606
dynamicIO: this.nextConfig.experimental.dynamicIO ?? false,
606607
clientSegmentCache:

test/e2e/app-dir/app-prefetch/prefetching.stale-times.test.ts

Lines changed: 49 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,5 @@
11
import { nextTestSetup } from 'e2e-utils'
2-
import { retry } from 'next-test-utils'
3-
4-
import { NEXT_RSC_UNION_QUERY } from 'next/dist/client/components/app-router-headers'
5-
6-
const browserConfigWithFixedTime = {
7-
beforePageLoad: (page) => {
8-
page.addInitScript(() => {
9-
const startTime = new Date()
10-
const fixedTime = new Date('2023-04-17T00:00:00Z')
11-
12-
// Override the Date constructor
13-
// @ts-ignore
14-
// eslint-disable-next-line no-native-reassign
15-
Date = class extends Date {
16-
constructor() {
17-
super()
18-
// @ts-ignore
19-
return new startTime.constructor(fixedTime)
20-
}
21-
22-
static now() {
23-
return fixedTime.getTime()
24-
}
25-
}
26-
})
27-
},
28-
}
2+
import { retry, waitFor } from 'next-test-utils'
293

304
describe('app dir - prefetching (custom staleTime)', () => {
315
const { next, isNextDev } = nextTestSetup({
@@ -34,8 +8,8 @@ describe('app dir - prefetching (custom staleTime)', () => {
348
nextConfig: {
359
experimental: {
3610
staleTimes: {
37-
static: 180,
38-
dynamic: 30,
11+
static: 30,
12+
dynamic: 20,
3913
},
4014
},
4115
},
@@ -47,25 +21,18 @@ describe('app dir - prefetching (custom staleTime)', () => {
4721
}
4822

4923
it('should not fetch again when a static page was prefetched when navigating to it twice', async () => {
50-
const browser = await next.browser('/404', browserConfigWithFixedTime)
24+
const browser = await next.browser('/404')
5125
let requests: string[] = []
5226

5327
browser.on('request', (req) => {
5428
requests.push(new URL(req.url()).pathname)
5529
})
5630
await browser.eval('location.href = "/"')
5731

58-
await browser.eval(
59-
`window.nd.router.prefetch("/static-page", {kind: "auto"})`
60-
)
61-
6232
await retry(async () => {
6333
expect(
64-
requests.filter(
65-
(request) =>
66-
request === '/static-page' || request.includes(NEXT_RSC_UNION_QUERY)
67-
).length
68-
).toBe(1)
34+
requests.filter((request) => request === '/static-page')
35+
).toHaveLength(1)
6936
})
7037

7138
await browser
@@ -86,11 +53,49 @@ describe('app dir - prefetching (custom staleTime)', () => {
8653

8754
await retry(async () => {
8855
expect(
89-
requests.filter(
90-
(request) =>
91-
request === '/static-page' || request.includes(NEXT_RSC_UNION_QUERY)
92-
).length
93-
).toBe(1)
56+
requests.filter((request) => request === '/static-page')
57+
).toHaveLength(1)
58+
})
59+
})
60+
61+
it('should fetch again when a static page was prefetched when navigating to it after the stale time has passed', async () => {
62+
const browser = await next.browser('/404')
63+
let requests: string[] = []
64+
65+
browser.on('request', (req) => {
66+
requests.push(new URL(req.url()).pathname)
67+
})
68+
await browser.eval('location.href = "/"')
69+
70+
await retry(async () => {
71+
expect(
72+
requests.filter((request) => request === '/static-page')
73+
).toHaveLength(1)
74+
})
75+
76+
await browser
77+
.elementByCss('#to-static-page')
78+
.click()
79+
.waitForElementByCss('#static-page')
80+
81+
const linkToStaticPage = await browser
82+
.elementByCss('#to-home')
83+
// Go back to home page
84+
.click()
85+
// Wait for homepage to load
86+
.waitForElementByCss('#to-static-page')
87+
88+
// Wait for the stale time to pass.
89+
await waitFor(30000)
90+
// Click on the link to the static page again
91+
await linkToStaticPage.click()
92+
// Wait for the static page to load again
93+
await browser.waitForElementByCss('#static-page')
94+
95+
await retry(async () => {
96+
expect(
97+
requests.filter((request) => request === '/static-page')
98+
).toHaveLength(2)
9499
})
95100
})
96101

0 commit comments

Comments
 (0)