diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index cf049a4b9d5f8..b30dc12c650fe 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -1513,6 +1513,7 @@ export default abstract class Server { }, { isManualRevalidate, + isPrefetch: !!req.headers['x-nextjs-prefetch'], } ) diff --git a/packages/next/server/response-cache.ts b/packages/next/server/response-cache.ts index d7d4937125e1d..2f83352c3c986 100644 --- a/packages/next/server/response-cache.ts +++ b/packages/next/server/response-cache.ts @@ -97,9 +97,7 @@ export default class ResponseCache { public get( key: string | null, responseGenerator: ResponseGenerator, - context: { - isManualRevalidate?: boolean - } + context: { isManualRevalidate?: boolean; isPrefetch?: boolean } ): Promise { const pendingResponse = key ? this.pendingResponses.get(key) : null if (pendingResponse) { @@ -165,7 +163,8 @@ export default class ResponseCache { } : cachedResponse.value, }) - if (!cachedResponse.isStale) { + // for prefetch we do not trigger revalidation + if (!cachedResponse.isStale || context.isPrefetch) { // The cached value is still valid, so we don't need // to update it yet. return diff --git a/packages/next/shared/lib/router/router.ts b/packages/next/shared/lib/router/router.ts index 30214fa6a0734..5df3a5d57fcb3 100644 --- a/packages/next/shared/lib/router/router.ts +++ b/packages/next/shared/lib/router/router.ts @@ -518,7 +518,7 @@ const SSG_DATA_NOT_FOUND = Symbol('SSG_DATA_NOT_FOUND') function fetchRetry( url: string, attempts: number, - opts: { text?: boolean } + opts: { text?: boolean; isPrefetch?: boolean } ): Promise { return fetch(url, { // Cookies are required to be present for Next.js' SSG "Preview Mode". @@ -533,6 +533,11 @@ function fetchRetry( // > option instead of relying on the default. // https://github.com/github/fetch#caveats credentials: 'same-origin', + headers: opts.isPrefetch + ? { + 'x-nextjs-prefetch': '1', + } + : {}, }).then((res) => { if (!res.ok) { if (attempts > 1 && res.status >= 500) { @@ -557,7 +562,8 @@ function fetchNextData( isServerRender: boolean, text: boolean | undefined, inflightCache: NextDataCache, - persistCache: boolean + persistCache: boolean, + isPrefetch: boolean ) { const { href: cacheKey } = new URL(dataHref, window.location.href) @@ -567,7 +573,7 @@ function fetchNextData( return (inflightCache[cacheKey] = fetchRetry( dataHref, isServerRender ? 3 : 1, - { text } + { text, isPrefetch } ) .catch((err: Error) => { // We should only trigger a server-side transition if this was caused @@ -1562,7 +1568,8 @@ export default class Router implements BaseRouter { this.isSsr, false, __N_SSG ? this.sdc : this.sdr, - !!__N_SSG && !isPreview + !!__N_SSG && !isPreview, + false ) : this.getInitialProps( Component, @@ -1784,6 +1791,7 @@ export default class Router implements BaseRouter { false, false, // text this.sdc, + true, true ) : false @@ -1848,7 +1856,7 @@ export default class Router implements BaseRouter { _getFlightData(dataHref: string): Promise { // Do not cache RSC flight response since it's not a static resource - return fetchNextData(dataHref, true, true, this.sdc, false).then( + return fetchNextData(dataHref, true, true, this.sdc, false, false).then( (serialized) => { return { fresh: true, data: serialized } } diff --git a/test/production/prerender-prefetch/index.test.ts b/test/production/prerender-prefetch/index.test.ts new file mode 100644 index 0000000000000..57845f30ab49c --- /dev/null +++ b/test/production/prerender-prefetch/index.test.ts @@ -0,0 +1,65 @@ +import { NextInstance } from 'test/lib/next-modes/base' +import { createNext, FileRef } from 'e2e-utils' +import { check, fetchViaHTTP, waitFor } from 'next-test-utils' +import cheerio from 'cheerio' +import { join } from 'path' +import webdriver from 'next-webdriver' + +describe('Prerender prefetch', () => { + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: { + pages: new FileRef(join(__dirname, 'pages')), + }, + dependencies: {}, + }) + }) + afterAll(() => next.destroy()) + + it('should not revalidate during prefetching', async () => { + const reqs = {} + + // get initial values + for (const path of ['/blog/first', '/blog/second']) { + const res = await fetchViaHTTP(next.url, path) + expect(res.status).toBe(200) + + const $ = cheerio.load(await res.text()) + const props = JSON.parse($('#props').text()) + reqs[path] = props + } + + const browser = await webdriver(next.url, '/') + + // wait for prefetch to occur + await check(async () => { + const cache = await browser.eval('JSON.stringify(window.next.router.sdc)') + return cache.includes('/blog/first') && cache.includes('/blog/second') + ? 'success' + : cache + }, 'success') + + await waitFor(3000) + await browser.refresh() + + // reload after revalidate period and wait for prefetch again + await check(async () => { + const cache = await browser.eval('JSON.stringify(window.next.router.sdc)') + return cache.includes('/blog/first') && cache.includes('/blog/second') + ? 'success' + : cache + }, 'success') + + // ensure revalidate did not occur from prefetch + for (const path of ['/blog/first', '/blog/second']) { + const res = await fetchViaHTTP(next.url, path) + expect(res.status).toBe(200) + + const $ = cheerio.load(await res.text()) + const props = JSON.parse($('#props').text()) + expect(props).toEqual(reqs[path]) + } + }) +}) diff --git a/test/production/prerender-prefetch/pages/blog/[slug].js b/test/production/prerender-prefetch/pages/blog/[slug].js new file mode 100644 index 0000000000000..4c246aa6b28bd --- /dev/null +++ b/test/production/prerender-prefetch/pages/blog/[slug].js @@ -0,0 +1,26 @@ +export default function Page(props) { + return ( + <> +

blog/[slug]

+

{JSON.stringify(props)}

+ + ) +} + +export function getStaticProps({ params }) { + console.log('revalidating /blog', params.slug) + return { + props: { + params, + now: Date.now(), + }, + revalidate: 2, + } +} + +export function getStaticPaths() { + return { + paths: ['/blog/first', '/blog/second'], + fallback: false, + } +} diff --git a/test/production/prerender-prefetch/pages/index.js b/test/production/prerender-prefetch/pages/index.js new file mode 100644 index 0000000000000..e4bd4bb60dfc0 --- /dev/null +++ b/test/production/prerender-prefetch/pages/index.js @@ -0,0 +1,28 @@ +import Link from 'next/link' + +export default function Page(props) { + return ( + <> +

index

+

{JSON.stringify(props)}

+ + /blog/first + +
+ + /blog/second + +
+ + ) +} + +export function getStaticProps() { + console.log('revalidating /') + return { + props: { + now: Date.now(), + }, + revalidate: 1, + } +}