Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add prefetch header for data prefetching #34498

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/next/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1513,6 +1513,7 @@ export default abstract class Server {
},
{
isManualRevalidate,
isPrefetch: !!req.headers['x-nextjs-prefetch'],
}
)

Expand Down
7 changes: 3 additions & 4 deletions packages/next/server/response-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,7 @@ export default class ResponseCache {
public get(
key: string | null,
responseGenerator: ResponseGenerator,
context: {
isManualRevalidate?: boolean
}
context: { isManualRevalidate?: boolean; isPrefetch?: boolean }
): Promise<ResponseCacheEntry | null> {
const pendingResponse = key ? this.pendingResponses.get(key) : null
if (pendingResponse) {
Expand Down Expand Up @@ -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
Expand Down
18 changes: 13 additions & 5 deletions packages/next/shared/lib/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> {
return fetch(url, {
// Cookies are required to be present for Next.js' SSG "Preview Mode".
Expand All @@ -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) {
Expand All @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1784,6 +1791,7 @@ export default class Router implements BaseRouter {
false,
false, // text
this.sdc,
true,
true
)
: false
Expand Down Expand Up @@ -1848,7 +1856,7 @@ export default class Router implements BaseRouter {

_getFlightData(dataHref: string): Promise<object> {
// 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 }
}
Expand Down
65 changes: 65 additions & 0 deletions test/production/prerender-prefetch/index.test.ts
Original file line number Diff line number Diff line change
@@ -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])
}
})
})
26 changes: 26 additions & 0 deletions test/production/prerender-prefetch/pages/blog/[slug].js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export default function Page(props) {
return (
<>
<p id="page">blog/[slug]</p>
<p id="props">{JSON.stringify(props)}</p>
</>
)
}

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,
}
}
28 changes: 28 additions & 0 deletions test/production/prerender-prefetch/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Link from 'next/link'

export default function Page(props) {
return (
<>
<p id="page">index</p>
<p id="props">{JSON.stringify(props)}</p>
<Link href="/blog/first">
<a>/blog/first</a>
</Link>
<br />
<Link href="/blog/second">
<a>/blog/second</a>
</Link>
<br />
</>
)
}

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