Fix react fetch deduping without next cache (#50187)

This ensures we don't reference the wrong fetch global bypassing react's
patched fetch so that it can properly fallback to react's fetch deduping
when next cache is disabled for a specific fetch.

x-ref: [slack
thread](https://vercel.slack.com/archives/C042LHPJ1NX/p1684518670927189?thread_ts=1682974724.765039&cid=C042LHPJ1NX)
fixes: https://github.com/vercel/next.js/issues/50031
This commit is contained in:
JJ Kasper 2023-05-22 16:13:02 -07:00 committed by GitHub
parent f56722c971
commit f4bb4aa73c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 98 additions and 6 deletions

View file

@ -20,6 +20,7 @@ import type { StaticGenerationAsyncStorage } from '../client/components/static-g
import '../server/require-hook' import '../server/require-hook'
import '../server/node-polyfill-fetch' import '../server/node-polyfill-fetch'
import '../server/node-polyfill-crypto' import '../server/node-polyfill-crypto'
import '../server/node-environment'
import chalk from 'next/dist/compiled/chalk' import chalk from 'next/dist/compiled/chalk'
import getGzipSize from 'next/dist/compiled/gzip-size' import getGzipSize from 'next/dist/compiled/gzip-size'
import textTable from 'next/dist/compiled/text-table' import textTable from 'next/dist/compiled/text-table'
@ -62,7 +63,6 @@ import { StaticGenerationAsyncStorageWrapper } from '../server/async-storage/sta
import { IncrementalCache } from '../server/lib/incremental-cache' import { IncrementalCache } from '../server/lib/incremental-cache'
import { patchFetch } from '../server/lib/patch-fetch' import { patchFetch } from '../server/lib/patch-fetch'
import { nodeFs } from '../server/lib/node-fs-methods' import { nodeFs } from '../server/lib/node-fs-methods'
import '../server/node-environment'
import * as ciEnvironment from '../telemetry/ci-info' import * as ciEnvironment from '../telemetry/ci-info'
export type ROUTER_TYPE = 'pages' | 'app' export type ROUTER_TYPE = 'pages' | 'app'

View file

@ -73,10 +73,14 @@ export function patchFetch({
serverHooks: typeof ServerHooks serverHooks: typeof ServerHooks
staticGenerationAsyncStorage: StaticGenerationAsyncStorage staticGenerationAsyncStorage: StaticGenerationAsyncStorage
}) { }) {
if (!(globalThis as any)._nextOriginalFetch) {
;(globalThis as any)._nextOriginalFetch = globalThis.fetch
}
if ((globalThis.fetch as any).__nextPatched) return if ((globalThis.fetch as any).__nextPatched) return
const { DynamicServerError } = serverHooks const { DynamicServerError } = serverHooks
const originFetch = globalThis.fetch const originFetch: typeof fetch = (globalThis as any)._nextOriginalFetch
globalThis.fetch = async ( globalThis.fetch = async (
input: RequestInfo | URL, input: RequestInfo | URL,
@ -527,8 +531,8 @@ export function patchFetch({
} }
) )
} }
;(fetch as any).__nextGetStaticStore = () => { ;(globalThis.fetch as any).__nextGetStaticStore = () => {
return staticGenerationAsyncStorage return staticGenerationAsyncStorage
} }
;(fetch as any).__nextPatched = true ;(globalThis.fetch as any).__nextPatched = true
} }

View file

@ -1,7 +1,7 @@
// TODO: Remove use of `any` type. // TODO: Remove use of `any` type.
// Polyfill fetch() in the Node.js environment // Polyfill fetch() in the Node.js environment
if (!(global as any).fetch) { if (typeof fetch === 'undefined' && typeof globalThis.fetch === 'undefined') {
function getFetchImpl() { function getFetchImpl() {
return require('next/dist/compiled/undici') return require('next/dist/compiled/undici')
} }
@ -17,7 +17,7 @@ if (!(global as any).fetch) {
} }
// Due to limitation of global configuration, we have to do this resolution at runtime // Due to limitation of global configuration, we have to do this resolution at runtime
;(global as any).fetch = (...args: any[]) => { globalThis.fetch = (...args: any[]) => {
const fetchImpl = getFetchImpl() const fetchImpl = getFetchImpl()
// Undici does not support the `keepAlive` option, // Undici does not support the `keepAlive` option,

View file

@ -35,6 +35,36 @@ createNextDescribe(
} }
}) })
it.each([
{
path: '/react-fetch-deduping-node',
},
{
path: '/react-fetch-deduping-edge',
},
])(
'should correctly de-dupe fetch without next cache $path',
async ({ path }) => {
for (let i = 0; i < 5; i++) {
const res = await next.fetch(path, {
redirect: 'manual',
})
expect(res.status).toBe(200)
const html = await res.text()
const $ = cheerio.load(html)
const data1 = $('#data-1').text()
const data2 = $('#data-2').text()
expect(data1).toBeTruthy()
expect(data1).toBe(data2)
await waitFor(250)
}
}
)
it.each([ it.each([
{ pathname: '/unstable-cache-node' }, { pathname: '/unstable-cache-node' },
{ pathname: '/unstable-cache-edge' }, { pathname: '/unstable-cache-edge' },
@ -506,6 +536,8 @@ createNextDescribe(
'partial-gen-params-no-additional-slug/fr/second.html', 'partial-gen-params-no-additional-slug/fr/second.html',
'partial-gen-params-no-additional-slug/fr/second.rsc', 'partial-gen-params-no-additional-slug/fr/second.rsc',
'partial-gen-params/[lang]/[slug]/page.js', 'partial-gen-params/[lang]/[slug]/page.js',
'react-fetch-deduping-edge/page.js',
'react-fetch-deduping-node/page.js',
'route-handler-edge/revalidate-360/route.js', 'route-handler-edge/revalidate-360/route.js',
'route-handler/post/route.js', 'route-handler/post/route.js',
'route-handler/revalidate-360-isr/route.js', 'route-handler/revalidate-360-isr/route.js',

View file

@ -0,0 +1,29 @@
export const runtime = 'edge'
export default async function Page() {
const data1 = await fetch(
'https://next-data-api-endpoint.vercel.app/api/random?1',
{
next: {
revalidate: 0,
},
}
).then((res) => res.text())
const data2 = await fetch(
'https://next-data-api-endpoint.vercel.app/api/random?1',
{
next: {
revalidate: 0,
},
}
).then((res) => res.text())
return (
<>
<p>/react-fetch-deduping</p>
<p id="data-1">{data1}</p>
<p id="data-2">{data2}</p>
</>
)
}

View file

@ -0,0 +1,27 @@
export default async function Page() {
const data1 = await fetch(
'https://next-data-api-endpoint.vercel.app/api/random?1',
{
next: {
revalidate: 0,
},
}
).then((res) => res.text())
const data2 = await fetch(
'https://next-data-api-endpoint.vercel.app/api/random?1',
{
next: {
revalidate: 0,
},
}
).then((res) => res.text())
return (
<>
<p>/react-fetch-deduping</p>
<p id="data-1">{data1}</p>
<p id="data-2">{data2}</p>
</>
)
}