Sitelet https://github.com/nuxt/nuxt/commit/e6c8ebbd6cce9dda1e86fd592a9d8ce9ea4cd32e
Skip to content

Commit e6c8ebb

Browse files
KealanAUdanielroe
authored andcommitted
fix(nuxt): revalidate cached route payloads instead of using force-cache (#35672)
1 parent b540854 commit e6c8ebb

3 files changed

Lines changed: 35 additions & 5 deletions

File tree

‎packages/nuxt/src/app/composables/payload.ts‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ export async function loadPayload (url: string, opts: LoadPayloadOptions = {}):
2121
if (import.meta.server || !payloadExtraction) { return null }
2222
if (await shouldLoadPayload(url)) {
2323
const payloadURL = await _getPayloadURL(url, opts)
24-
return await _importPayload(payloadURL) || null
24+
// cached (`isr`/`swr`/`cache`) payloads are mutable within a deploy, so `?buildId`
25+
// cannot invalidate them - defer to normal HTTP cache semantics instead
26+
const cache: RequestCache = getRouteRules({ path: url }).payload ? 'default' : 'force-cache'
27+
return await _importPayload(payloadURL, cache) || null
2528
}
2629
return null
2730
}
@@ -86,11 +89,11 @@ async function _getPayloadURL (url: string, opts: LoadPayloadOptions = {}) {
8689
return joinURL(baseOrCdnURL, u.pathname, filename + (hash ? `?${hash}` : ''))
8790
}
8891

89-
async function _importPayload (payloadURL: string) {
92+
async function _importPayload (payloadURL: string, cache: RequestCache) {
9093
if (import.meta.server || !payloadExtraction) { return null }
9194
try {
9295
if (renderJsonPayloads) {
93-
const res = await fetch(payloadURL, import.meta.dev ? {} : { cache: 'force-cache' })
96+
const res = await fetch(payloadURL, import.meta.dev ? {} : { cache })
9497
if (!res.ok) {
9598
if (import.meta.dev) {
9699
console.warn(`[nuxt] Cannot load payload ${payloadURL}: ${res.status} ${res.statusText}`)
@@ -181,7 +184,10 @@ export async function getNuxtClientPayload () {
181184

182185
const inlineData = await parsePayload(el.textContent || '')
183186

184-
const externalData = el.dataset.src ? await _importPayload(el.dataset.src) : undefined
187+
// `prerenderedAt` is only set for build-time prerendered HTML - without it, the page
188+
// was rendered at runtime (`isr`/`swr`/`cache`) and the external payload must match
189+
// the HTML we were just served, so revalidate instead of trusting the browser cache
190+
const externalData = el.dataset.src ? await _importPayload(el.dataset.src, inlineData.prerenderedAt ? 'force-cache' : 'no-cache') : undefined
185191

186192
payloadCache = {
187193
...inlineData,

‎test/nuxt/composables.test.ts‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { useRouteAnnouncer } from '#app/composables/route-announcer'
2323
import { encodeRoutePath, encodeURL, resolveRouteObject } from '#app/composables/router'
2424
import { useRuntimeHook } from '#app/composables/runtime-hook'
2525

26-
import { shouldLoadPayload } from '#app/composables/payload'
26+
import { loadPayload, shouldLoadPayload } from '#app/composables/payload'
2727
import { NuxtPage } from '#components'
2828
import { isTestingAppManifest } from '../matrix'
2929

@@ -34,6 +34,12 @@ registerEndpoint('/api/test', defineEventHandler(event => ({
3434
headers: Object.fromEntries(event.headers.entries()),
3535
})))
3636

37+
// the test environment builds with `ssr: false`, which disables payload extraction
38+
vi.mock('#build/nuxt.config.mjs', async importOriginal => ({
39+
...await importOriginal<Record<string, unknown>>(),
40+
payloadExtraction: true,
41+
}))
42+
3743
describe('app config', () => {
3844
it('can be updated', () => {
3945
const appConfig = useAppConfig()
@@ -440,6 +446,9 @@ describe.skipIf(!isTestingAppManifest)('app manifests', () => {
440446
},
441447
},
442448
"wildcard": {
449+
"/isr": {
450+
"isr": 60,
451+
},
443452
"/pre": {
444453
"prerender": true,
445454
},
@@ -513,6 +522,20 @@ describe('compiled route rules', () => {
513522
const shouldLoadRedirect = await shouldLoadPayload('/pre/test')
514523
expect(shouldLoadRedirect).toBe(false)
515524
})
525+
526+
it('should only use `force-cache` for immutable prerendered payloads', async () => {
527+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(() => Promise.resolve(new Response('[{"data":1},{}]')))
528+
try {
529+
await loadPayload('/pre/thing')
530+
expect(fetchSpy.mock.calls[0]![1]).toMatchObject({ cache: 'force-cache' })
531+
532+
// cached (isr/swr/cache) payloads can change within a deploy, so the browser cache must be revalidated
533+
await loadPayload('/isr/thing')
534+
expect(fetchSpy.mock.calls[1]![1]).toMatchObject({ cache: 'default' })
535+
} finally {
536+
fetchSpy.mockRestore()
537+
}
538+
})
516539
})
517540

518541
describe('useRuntimeHook', () => {

‎vitest.config.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const commonSettings: NuxtConfig = {
1212
pages: true,
1313
routeRules: {
1414
'/specific-prerendered': { prerender: true },
15+
'/isr/**': { isr: 60 },
1516
'/pre/test': { redirect: '/' },
1617
'/pre/spa/**': { prerender: true, ssr: false },
1718
'/pre/**': { prerender: true },

0 commit comments

Comments
 (0)