diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index 16b7dd57fa..11cb72b26b 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -20,6 +20,7 @@ import type { StaticGenerationAsyncStorage } from '../client/components/static-g import '../server/require-hook' import '../server/node-polyfill-fetch' import '../server/node-polyfill-crypto' +import '../server/node-environment' import chalk from 'next/dist/compiled/chalk' import getGzipSize from 'next/dist/compiled/gzip-size' 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 { patchFetch } from '../server/lib/patch-fetch' import { nodeFs } from '../server/lib/node-fs-methods' -import '../server/node-environment' import * as ciEnvironment from '../telemetry/ci-info' export type ROUTER_TYPE = 'pages' | 'app' diff --git a/packages/next/src/server/lib/patch-fetch.ts b/packages/next/src/server/lib/patch-fetch.ts index bb39bf9596..4b9040e2ae 100644 --- a/packages/next/src/server/lib/patch-fetch.ts +++ b/packages/next/src/server/lib/patch-fetch.ts @@ -73,10 +73,14 @@ export function patchFetch({ serverHooks: typeof ServerHooks staticGenerationAsyncStorage: StaticGenerationAsyncStorage }) { + if (!(globalThis as any)._nextOriginalFetch) { + ;(globalThis as any)._nextOriginalFetch = globalThis.fetch + } + if ((globalThis.fetch as any).__nextPatched) return const { DynamicServerError } = serverHooks - const originFetch = globalThis.fetch + const originFetch: typeof fetch = (globalThis as any)._nextOriginalFetch globalThis.fetch = async ( input: RequestInfo | URL, @@ -527,8 +531,8 @@ export function patchFetch({ } ) } - ;(fetch as any).__nextGetStaticStore = () => { + ;(globalThis.fetch as any).__nextGetStaticStore = () => { return staticGenerationAsyncStorage } - ;(fetch as any).__nextPatched = true + ;(globalThis.fetch as any).__nextPatched = true } diff --git a/packages/next/src/server/node-polyfill-fetch.ts b/packages/next/src/server/node-polyfill-fetch.ts index 4e2c7006b9..1ae59a43e2 100644 --- a/packages/next/src/server/node-polyfill-fetch.ts +++ b/packages/next/src/server/node-polyfill-fetch.ts @@ -1,7 +1,7 @@ // TODO: Remove use of `any` type. // Polyfill fetch() in the Node.js environment -if (!(global as any).fetch) { +if (typeof fetch === 'undefined' && typeof globalThis.fetch === 'undefined') { function getFetchImpl() { 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 - ;(global as any).fetch = (...args: any[]) => { + globalThis.fetch = (...args: any[]) => { const fetchImpl = getFetchImpl() // Undici does not support the `keepAlive` option, diff --git a/test/e2e/app-dir/app-static/app-static.test.ts b/test/e2e/app-dir/app-static/app-static.test.ts index 7bfb74d7d1..8c3431c2f8 100644 --- a/test/e2e/app-dir/app-static/app-static.test.ts +++ b/test/e2e/app-dir/app-static/app-static.test.ts @@ -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([ { pathname: '/unstable-cache-node' }, { 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.rsc', '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/post/route.js', 'route-handler/revalidate-360-isr/route.js', diff --git a/test/e2e/app-dir/app-static/app/react-fetch-deduping-edge/page.js b/test/e2e/app-dir/app-static/app/react-fetch-deduping-edge/page.js new file mode 100644 index 0000000000..a3b5baae04 --- /dev/null +++ b/test/e2e/app-dir/app-static/app/react-fetch-deduping-edge/page.js @@ -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 ( + <> +

/react-fetch-deduping

+

{data1}

+

{data2}

+ + ) +} diff --git a/test/e2e/app-dir/app-static/app/react-fetch-deduping-node/page.js b/test/e2e/app-dir/app-static/app/react-fetch-deduping-node/page.js new file mode 100644 index 0000000000..3b7a3df3ca --- /dev/null +++ b/test/e2e/app-dir/app-static/app/react-fetch-deduping-node/page.js @@ -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 ( + <> +

/react-fetch-deduping

+

{data1}

+

{data2}

+ + ) +}