Skip to content

Commit 4d37f66

Browse files
authored
ensure fetch is patched before preloading entries (#72363)
If a user creates a custom fetch implementation in module scope, the logic to preload entries during `next start` will result in fetch not being patched before the userland implementation. As a result, it won't opt into any caching heuristics from our patched fetch. Fixes #71881 (in addition to the other PR in this stack)
1 parent 1c22d14 commit 4d37f66

File tree

4 files changed

+77
-0
lines changed

4 files changed

+77
-0
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,11 @@ export default class NextNodeServer extends BaseServer<
277277
for (const page of Object.keys(appPathsManifest || {})) {
278278
await loadComponents({ distDir: this.distDir, page, isAppPath: true })
279279
.then(async ({ ComponentMod }) => {
280+
// we need to ensure fetch is patched before we require the page,
281+
// otherwise if the fetch is patched by user code, we will be patching it
282+
// too late and there won't be any caching behaviors
283+
ComponentMod.patchFetch()
284+
280285
const webpackRequire = ComponentMod.__next_app__.require
281286
if (webpackRequire?.m) {
282287
for (const id of Object.keys(webpackRequire.m)) {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { waitFor, retry } from 'next-test-utils'
2+
import { nextTestSetup } from 'e2e-utils'
3+
4+
describe('app-fetch-deduping', () => {
5+
const { next } = nextTestSetup({ files: __dirname })
6+
7+
it('should still properly cache fetches when the user has a custom fetch implementation', async () => {
8+
const browser = await next.browser('/')
9+
10+
let currentValue: string | undefined
11+
await retry(async () => {
12+
const initialRandom = await browser.elementById('random').text()
13+
expect(initialRandom).toMatch(/^0\.\d+$/)
14+
15+
await browser.refresh()
16+
currentValue = await browser.elementById('random').text()
17+
expect(currentValue).toBe(initialRandom)
18+
})
19+
20+
// wait for the revalidation period
21+
await waitFor(3000)
22+
23+
await retry(async () => {
24+
await browser.refresh()
25+
const finalValue = await browser.elementById('random').text()
26+
expect(finalValue).not.toBe(currentValue)
27+
})
28+
})
29+
})
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function Layout({ children }) {
2+
return (
3+
<html lang="en">
4+
<body>{children}</body>
5+
</html>
6+
)
7+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { cookies } from 'next/headers'
2+
import { Suspense } from 'react'
3+
4+
const patchFetch = (originalFetch) => async (url, options) => {
5+
const res = await originalFetch(url, options)
6+
return res
7+
}
8+
9+
const customFetch = patchFetch(fetch)
10+
11+
export default async function Page() {
12+
const data = await customFetch(
13+
'https://next-data-api-endpoint.vercel.app/api/random?revalidate-3',
14+
{
15+
next: {
16+
revalidate: 3,
17+
},
18+
}
19+
).then((res) => res.text())
20+
21+
return (
22+
<>
23+
<div id="random">{data}</div>
24+
<Suspense fallback={<div>Loading...</div>}>
25+
<DynamicThing />
26+
</Suspense>
27+
</>
28+
)
29+
}
30+
31+
async function DynamicThing() {
32+
const cookieStore = await cookies()
33+
const cookie = cookieStore.get('name')
34+
35+
return <div>{cookie}</div>
36+
}

0 commit comments

Comments
 (0)