From 56d4c965541ae821f3d06da740d7ab811483afb2 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Fri, 5 Jan 2024 16:29:24 +0100 Subject: [PATCH 01/20] Display original failed fetch trace --- .../container/RuntimeError/CallStackFrame.tsx | 2 +- .../RuntimeError/GroupedStackFrames.tsx | 1 - .../internal/helpers/stack-frame.ts | 1 - packages/next/src/server/lib/patch-fetch.ts | 39 +++++++++++++++++-- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/packages/next/src/client/components/react-dev-overlay/internal/container/RuntimeError/CallStackFrame.tsx b/packages/next/src/client/components/react-dev-overlay/internal/container/RuntimeError/CallStackFrame.tsx index 3d4e042edbf1dd..aa5f9b31cba089 100644 --- a/packages/next/src/client/components/react-dev-overlay/internal/container/RuntimeError/CallStackFrame.tsx +++ b/packages/next/src/client/components/react-dev-overlay/internal/container/RuntimeError/CallStackFrame.tsx @@ -36,7 +36,7 @@ export const CallStackFrame: React.FC<{ onClick={open} title={hasSource ? 'Click to open in your editor' : undefined} > - {getFrameSource(f)} + {getFrameSource(f)} { + // If it's failed with internal fetch call, and there's only node:internal traces + if ( + (err instanceof Error && err.name === 'TypeError', + err.message === 'fetch failed') + ) { + const traces: string[] = err.stack?.split('\n') + // trace without the error message + const originStackTrace = traces.slice(1) + if (originStackTrace.every((line) => line.includes('node:internal'))) { + err.stack = traces[0] + '\n' + modifiedTrace + throw err + } + } + throw err + }) + } + return overriddenFetch +} + // we patch fetch to collect cache information used for // determining if a page is static or not export function patchFetch({ @@ -191,10 +217,14 @@ export function patchFetch({ const { DynamicServerError } = serverHooks const originFetch: typeof fetch = (globalThis as any)._nextOriginalFetch - globalThis.fetch = async ( + async function patchedFetch( input: RequestInfo | URL, init: RequestInit | undefined - ) => { + ) { + const tracedOriginalFetch = (...args: Parameters) => + fetchErrorHandlingWrapper(originFetch, tracingError)(...args) + const tracingError = new Error('NEXT_FETCH_TRACING_ERROR') + let url: URL | undefined try { url = new URL(input instanceof Request ? input.url : input) @@ -246,7 +276,7 @@ export function patchFetch({ isInternal || staticGenerationStore.isDraftMode ) { - return originFetch(input, init) + return tracedOriginalFetch(input, init) } let revalidate: number | undefined | false = undefined @@ -496,7 +526,7 @@ export function patchFetch({ next: { ...init?.next, fetchType: 'origin', fetchIdx }, } - return originFetch(input, clonedInit).then(async (res) => { + return tracedOriginalFetch(input, clonedInit).then(async (res) => { if (!isStale) { trackFetchMetric(staticGenerationStore, { start: fetchStart, @@ -682,6 +712,7 @@ export function patchFetch({ } ) } + globalThis.fetch = patchedFetch.bind(globalThis) ;(globalThis.fetch as any).__nextGetStaticStore = () => { return staticGenerationAsyncStorage } From ff3dd1938abedcf9d58cfceb35138868a328108d Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Fri, 5 Jan 2024 17:06:58 +0100 Subject: [PATCH 02/20] bypass webpack:// --- packages/react-dev-overlay/src/middleware.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/react-dev-overlay/src/middleware.ts b/packages/react-dev-overlay/src/middleware.ts index e52cdf9afffe6f..9d48213015d24a 100644 --- a/packages/react-dev-overlay/src/middleware.ts +++ b/packages/react-dev-overlay/src/middleware.ts @@ -320,8 +320,9 @@ function getOverlayMiddleware(options: OverlayMiddlewareOptions) { if ( !( - (frame.file?.startsWith('webpack-internal:///') || - frame.file?.startsWith('file://')) && + (frame.file?.startsWith('webpack-internal://') || + frame.file?.startsWith('file://') || + frame.file?.startsWith('webpack://')) && Boolean(parseInt(frame.lineNumber?.toString() ?? '', 10)) ) ) { @@ -331,11 +332,11 @@ function getOverlayMiddleware(options: OverlayMiddlewareOptions) { } const moduleId: string = frame.file.replace( - /^(webpack-internal:\/\/\/|file:\/\/)/, + /webpack-internal:(\/)+|file:\/\//, '' ) const modulePath = frame.file.replace( - /^(webpack-internal:\/\/\/|file:\/\/)(\(.*\)\/)?/, + /webpack-internal:(\/)+|file:\/\/(\(.*\)\/)?/, '' ) From e0893ac10c870c01d521b0b270d7f1611c711fb8 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Fri, 5 Jan 2024 17:44:21 +0100 Subject: [PATCH 03/20] update test --- .../acceptance-app/rsc-runtime-errors.test.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/test/development/acceptance-app/rsc-runtime-errors.test.ts b/test/development/acceptance-app/rsc-runtime-errors.test.ts index ce249de931714a..0d69a3f5c21608 100644 --- a/test/development/acceptance-app/rsc-runtime-errors.test.ts +++ b/test/development/acceptance-app/rsc-runtime-errors.test.ts @@ -4,6 +4,7 @@ import { FileRef, createNextDescribe } from 'e2e-utils' import { check, getRedboxDescription, + getRedboxSource, hasRedbox, shouldRunTurboDevTest, } from 'next-test-utils' @@ -88,14 +89,36 @@ createNextDescribe( ) const browser = await next.browser('/server') - await check( async () => ((await hasRedbox(browser, true)) ? 'success' : 'fail'), /success/ ) + const errorDescription = await getRedboxDescription(browser) expect(errorDescription).toContain(`Error: alert is not defined`) }) + + it('should show the userland code error trace when fetch failed error occurred', async () => { + await next.patchFile( + 'app/server/page.js', + outdent` + export default async function Page() { + await fetch('http://locahost:3000/xxxx') + return 'page' + } + ` + ) + const browser = await next.browser('/server') + await check( + async () => ((await hasRedbox(browser, true)) ? 'success' : 'fail'), + /success/ + ) + + const source = await getRedboxSource(browser) + // Can show the original source code + expect(source).toContain('app/server/page.js') + expect(source).toContain(`> 2 | await fetch('http://locahost:3000/xxxx')`) + }) } ) From b3b4ed3bad3e756b88ebc7fbbda7ff052b50d0f8 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Fri, 5 Jan 2024 18:07:34 +0100 Subject: [PATCH 04/20] polish --- .../container/RuntimeError/CallStackFrame.tsx | 2 +- packages/next/src/server/lib/patch-fetch.ts | 23 ++++++++++++------- packages/react-dev-overlay/src/middleware.ts | 4 ++-- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/next/src/client/components/react-dev-overlay/internal/container/RuntimeError/CallStackFrame.tsx b/packages/next/src/client/components/react-dev-overlay/internal/container/RuntimeError/CallStackFrame.tsx index aa5f9b31cba089..3d4e042edbf1dd 100644 --- a/packages/next/src/client/components/react-dev-overlay/internal/container/RuntimeError/CallStackFrame.tsx +++ b/packages/next/src/client/components/react-dev-overlay/internal/container/RuntimeError/CallStackFrame.tsx @@ -36,7 +36,7 @@ export const CallStackFrame: React.FC<{ onClick={open} title={hasSource ? 'Click to open in your editor' : undefined} > - {getFrameSource(f)} + {getFrameSource(f)} `Error: fetch failed` + userland trace + */ +function traceErroredFetcher( fetchFn: typeof fetch, tracingError: Error ): typeof fetch { + // Remove the error message and the trace line inside `patchFetch()` const modifiedTrace = tracingError.stack?.split('\n').slice(2).join('\n') - const overriddenFetch: typeof fetch = function (...args) { + const tracedFetch: typeof fetch = function (...args) { return fetchFn(...args).catch((err) => { // If it's failed with internal fetch call, and there's only node:internal traces if ( - (err instanceof Error && err.name === 'TypeError', - err.message === 'fetch failed') + err instanceof Error && + err.name === 'TypeError' && + err.message === 'fetch failed' ) { - const traces: string[] = err.stack?.split('\n') + const traces: string[] = err.stack?.split('\n') || [] // trace without the error message const originStackTrace = traces.slice(1) if (originStackTrace.every((line) => line.includes('node:internal'))) { + // Combine the origin error message with the modified trace containing userland trace err.stack = traces[0] + '\n' + modifiedTrace throw err } @@ -199,7 +206,7 @@ function fetchErrorHandlingWrapper( throw err }) } - return overriddenFetch + return tracedFetch } // we patch fetch to collect cache information used for @@ -221,9 +228,9 @@ export function patchFetch({ input: RequestInfo | URL, init: RequestInit | undefined ) { + const tracingError = new Error() const tracedOriginalFetch = (...args: Parameters) => - fetchErrorHandlingWrapper(originFetch, tracingError)(...args) - const tracingError = new Error('NEXT_FETCH_TRACING_ERROR') + traceErroredFetcher(originFetch, tracingError)(...args) let url: URL | undefined try { diff --git a/packages/react-dev-overlay/src/middleware.ts b/packages/react-dev-overlay/src/middleware.ts index 9d48213015d24a..81eae3b6c0dead 100644 --- a/packages/react-dev-overlay/src/middleware.ts +++ b/packages/react-dev-overlay/src/middleware.ts @@ -320,9 +320,9 @@ function getOverlayMiddleware(options: OverlayMiddlewareOptions) { if ( !( - (frame.file?.startsWith('webpack-internal://') || + (frame.file?.startsWith('webpack-internal:///') || frame.file?.startsWith('file://') || - frame.file?.startsWith('webpack://')) && + frame.file?.startsWith('webpack:///')) && Boolean(parseInt(frame.lineNumber?.toString() ?? '', 10)) ) ) { From 9f50f410adb78b5903d84e9b132272438d813df1 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Fri, 5 Jan 2024 18:46:57 +0100 Subject: [PATCH 05/20] disable for turbopack --- test/turbopack-tests-manifest.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/turbopack-tests-manifest.json b/test/turbopack-tests-manifest.json index c8792f8c82bb3e..df11eaf9c2f279 100644 --- a/test/turbopack-tests-manifest.json +++ b/test/turbopack-tests-manifest.json @@ -1183,7 +1183,8 @@ "test/development/acceptance-app/rsc-runtime-errors.test.ts": { "passed": [ "Error overlay - RSC runtime errors should show runtime errors if invalid client API from node_modules is executed", - "Error overlay - RSC runtime errors should show runtime errors if invalid server API from node_modules is executed" + "Error overlay - RSC runtime errors should show runtime errors if invalid server API from node_modules is executed", + "Error overlay - RSC runtime errors should show the userland code error trace when fetch failed error occurred" ], "failed": [], "pending": [], From c67ebecf5ca741b56fa85b16a05425e223c6365f Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Fri, 5 Jan 2024 18:55:46 +0100 Subject: [PATCH 06/20] fix manifest --- test/turbopack-tests-manifest.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/turbopack-tests-manifest.json b/test/turbopack-tests-manifest.json index df11eaf9c2f279..591b133af0f54c 100644 --- a/test/turbopack-tests-manifest.json +++ b/test/turbopack-tests-manifest.json @@ -1183,10 +1183,11 @@ "test/development/acceptance-app/rsc-runtime-errors.test.ts": { "passed": [ "Error overlay - RSC runtime errors should show runtime errors if invalid client API from node_modules is executed", - "Error overlay - RSC runtime errors should show runtime errors if invalid server API from node_modules is executed", + "Error overlay - RSC runtime errors should show runtime errors if invalid server API from node_modules is executed" + ], + "failed": [ "Error overlay - RSC runtime errors should show the userland code error trace when fetch failed error occurred" ], - "failed": [], "pending": [], "flakey": [], "runtimeError": false From cd8a78317ca9730cffa642cc15b6f3b5652689c0 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Mon, 8 Jan 2024 16:02:27 +0100 Subject: [PATCH 07/20] Update comment --- packages/next/src/server/lib/patch-fetch.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/next/src/server/lib/patch-fetch.ts b/packages/next/src/server/lib/patch-fetch.ts index 378438678183f9..11eaa932446bae 100644 --- a/packages/next/src/server/lib/patch-fetch.ts +++ b/packages/next/src/server/lib/patch-fetch.ts @@ -228,6 +228,19 @@ export function patchFetch({ input: RequestInfo | URL, init: RequestInit | undefined ) { + /** + * The tracing error is used to get the stack trace of the fetch call, when the fetch is executed in the + * different tick, where the stack trace is not available to trace back to original invoked place. + * + * e.g. You might see failed fetch stack trace like this: + * > fetch + * > process.processTicksAndRejections + * + * This tracing error will preserve the original stack trace, so that we can trace back to the original, + * we'll use it to replace the stack trace of the error thrown by the fetch call, once we detect there's only + * unhelpful internal call trace showed up. + * + */ const tracingError = new Error() const tracedOriginalFetch = (...args: Parameters) => traceErroredFetcher(originFetch, tracingError)(...args) From 27e5ebe3a870807bf39b20868c7ed028404837c0 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Mon, 8 Jan 2024 18:18:46 +0100 Subject: [PATCH 08/20] fix fetch name --- packages/next/src/server/lib/patch-fetch.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/next/src/server/lib/patch-fetch.ts b/packages/next/src/server/lib/patch-fetch.ts index 11eaa932446bae..3c776a02d8f6d8 100644 --- a/packages/next/src/server/lib/patch-fetch.ts +++ b/packages/next/src/server/lib/patch-fetch.ts @@ -224,7 +224,7 @@ export function patchFetch({ const { DynamicServerError } = serverHooks const originFetch: typeof fetch = (globalThis as any)._nextOriginalFetch - async function patchedFetch( + globalThis.fetch = async function ( input: RequestInfo | URL, init: RequestInit | undefined ) { @@ -732,7 +732,6 @@ export function patchFetch({ } ) } - globalThis.fetch = patchedFetch.bind(globalThis) ;(globalThis.fetch as any).__nextGetStaticStore = () => { return staticGenerationAsyncStorage } From 39f571a71b0081e6f826d5988529b63bd4d7d7c3 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Mon, 8 Jan 2024 18:53:41 +0100 Subject: [PATCH 09/20] patch test --- packages/next/src/server/lib/patch-fetch.ts | 27 ++++++++++++--------- test/e2e/app-dir/hello-world/app/page.tsx | 9 +++++-- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/next/src/server/lib/patch-fetch.ts b/packages/next/src/server/lib/patch-fetch.ts index 3c776a02d8f6d8..0920523032b321 100644 --- a/packages/next/src/server/lib/patch-fetch.ts +++ b/packages/next/src/server/lib/patch-fetch.ts @@ -186,8 +186,8 @@ function traceErroredFetcher( ): typeof fetch { // Remove the error message and the trace line inside `patchFetch()` const modifiedTrace = tracingError.stack?.split('\n').slice(2).join('\n') - const tracedFetch: typeof fetch = function (...args) { - return fetchFn(...args).catch((err) => { + const tracedFetch: typeof fetch = async function (...args) { + return await fetchFn(...args).catch((err) => { // If it's failed with internal fetch call, and there's only node:internal traces if ( err instanceof Error && @@ -224,6 +224,7 @@ export function patchFetch({ const { DynamicServerError } = serverHooks const originFetch: typeof fetch = (globalThis as any)._nextOriginalFetch + console.log('nextjs patch fetch') globalThis.fetch = async function ( input: RequestInfo | URL, init: RequestInit | undefined @@ -243,7 +244,8 @@ export function patchFetch({ */ const tracingError = new Error() const tracedOriginalFetch = (...args: Parameters) => - traceErroredFetcher(originFetch, tracingError)(...args) + originFetch(...args) + // traceErroredFetcher(originFetch, tracingError)(...args) let url: URL | undefined try { @@ -296,7 +298,9 @@ export function patchFetch({ isInternal || staticGenerationStore.isDraftMode ) { - return tracedOriginalFetch(input, init) + console.log('original fetch') + return await originFetch(input, init) + // return tracedOriginalFetch(input, init) } let revalidate: number | undefined | false = undefined @@ -546,7 +550,7 @@ export function patchFetch({ next: { ...init?.next, fetchType: 'origin', fetchIdx }, } - return tracedOriginalFetch(input, clonedInit).then(async (res) => { + return await originFetch(input, clonedInit).then(async (res) => { if (!isStale) { trackFetchMetric(staticGenerationStore, { start: fetchStart, @@ -602,13 +606,13 @@ export function patchFetch({ }) } - let handleUnlock = () => Promise.resolve() + // let handleUnlock = () => Promise.resolve() let cacheReasonOverride if (cacheKey && staticGenerationStore.incrementalCache) { - handleUnlock = await staticGenerationStore.incrementalCache.lock( - cacheKey - ) + // handleUnlock = await staticGenerationStore.incrementalCache.lock( + // cacheKey + // ) const entry = staticGenerationStore.isOnDemandRevalidate ? null @@ -622,13 +626,14 @@ export function patchFetch({ }) if (entry) { - await handleUnlock() + // await handleUnlock() } else { // in dev, incremental cache response will be null in case the browser adds `cache-control: no-cache` in the request headers cacheReasonOverride = 'cache-control: no-cache (hard refresh)' } if (entry?.value && entry.value.kind === 'FETCH') { + console.log('cache promise') // when stale and is revalidating we wait for fresh data // so the revalidated entry has the updated data if (!(staticGenerationStore.isRevalidate && entry.isStale)) { @@ -728,7 +733,7 @@ export function patchFetch({ if (hasNextConfig) delete init.next } - return doOriginalFetch(false, cacheReasonOverride).finally(handleUnlock) + return await doOriginalFetch(false, cacheReasonOverride) //.finally(handleUnlock) } ) } diff --git a/test/e2e/app-dir/hello-world/app/page.tsx b/test/e2e/app-dir/hello-world/app/page.tsx index ff7159d9149fee..3c90a360dbcdb4 100644 --- a/test/e2e/app-dir/hello-world/app/page.tsx +++ b/test/e2e/app-dir/hello-world/app/page.tsx @@ -1,3 +1,8 @@ -export default function Page() { - return

hello world

+export default async function Page() { + try { + await fetch('http://locahost:3000/xxxx') + } catch (e) { + throw e + } + return 'page' } From 1ba4e83a9243163949d7f6e413bdc8855c9eca5b Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Mon, 8 Jan 2024 19:33:36 +0100 Subject: [PATCH 10/20] try-catch --- examples/hello-world/app/page.tsx | 3 ++- packages/next/src/server/lib/patch-fetch.ts | 25 +++++++++++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/examples/hello-world/app/page.tsx b/examples/hello-world/app/page.tsx index 6baa6ade86b9a9..8a3e8303a4d776 100644 --- a/examples/hello-world/app/page.tsx +++ b/examples/hello-world/app/page.tsx @@ -1,3 +1,4 @@ -export default function Page() { +export default async function Page() { + await fetch('http://localhost:3000/xxxx') return

Hello, Next.js!

} diff --git a/packages/next/src/server/lib/patch-fetch.ts b/packages/next/src/server/lib/patch-fetch.ts index 0920523032b321..d768b3946a5f15 100644 --- a/packages/next/src/server/lib/patch-fetch.ts +++ b/packages/next/src/server/lib/patch-fetch.ts @@ -243,8 +243,14 @@ export function patchFetch({ * */ const tracingError = new Error() - const tracedOriginalFetch = (...args: Parameters) => - originFetch(...args) + const tryCatchFetch = async (...args: Parameters) => { + try { + return await originFetch(...args) + } catch (e) { + throw e + } + } + // traceErroredFetcher(originFetch, tracingError)(...args) let url: URL | undefined @@ -298,8 +304,7 @@ export function patchFetch({ isInternal || staticGenerationStore.isDraftMode ) { - console.log('original fetch') - return await originFetch(input, init) + return await tryCatchFetch(input, init) // return tracedOriginalFetch(input, init) } @@ -606,13 +611,13 @@ export function patchFetch({ }) } - // let handleUnlock = () => Promise.resolve() + let handleUnlock = () => Promise.resolve() let cacheReasonOverride if (cacheKey && staticGenerationStore.incrementalCache) { - // handleUnlock = await staticGenerationStore.incrementalCache.lock( - // cacheKey - // ) + handleUnlock = await staticGenerationStore.incrementalCache.lock( + cacheKey + ) const entry = staticGenerationStore.isOnDemandRevalidate ? null @@ -733,7 +738,9 @@ export function patchFetch({ if (hasNextConfig) delete init.next } - return await doOriginalFetch(false, cacheReasonOverride) //.finally(handleUnlock) + return await doOriginalFetch(false, cacheReasonOverride).finally( + handleUnlock + ) } ) } From 1a69d03967dbd58e0ba420cdfac014de0bcd59c5 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 9 Jan 2024 13:46:52 +0100 Subject: [PATCH 11/20] wrap all --- packages/next/src/server/lib/patch-fetch.ts | 814 ++++++++++---------- 1 file changed, 408 insertions(+), 406 deletions(-) diff --git a/packages/next/src/server/lib/patch-fetch.ts b/packages/next/src/server/lib/patch-fetch.ts index d768b3946a5f15..42dc871b9bfa45 100644 --- a/packages/next/src/server/lib/patch-fetch.ts +++ b/packages/next/src/server/lib/patch-fetch.ts @@ -224,7 +224,6 @@ export function patchFetch({ const { DynamicServerError } = serverHooks const originFetch: typeof fetch = (globalThis as any)._nextOriginalFetch - console.log('nextjs patch fetch') globalThis.fetch = async function ( input: RequestInfo | URL, init: RequestInit | undefined @@ -243,13 +242,7 @@ export function patchFetch({ * */ const tracingError = new Error() - const tryCatchFetch = async (...args: Parameters) => { - try { - return await originFetch(...args) - } catch (e) { - throw e - } - } + const tryCatchFetch = async (...args: Parameters) => {} // traceErroredFetcher(originFetch, tracingError)(...args) @@ -283,441 +276,414 @@ export function patchFetch({ }, }, async () => { - const staticGenerationStore: StaticGenerationStore = - staticGenerationAsyncStorage.getStore() || - (fetch as any).__nextGetStaticStore?.() - const isRequestInput = - input && - typeof input === 'object' && - typeof (input as Request).method === 'string' - - const getRequestMeta = (field: string) => { - let value = isRequestInput ? (input as any)[field] : null - return value || (init as any)?.[field] - } + try { + // return await originFetch(...args) + + const staticGenerationStore: StaticGenerationStore = + staticGenerationAsyncStorage.getStore() || + (fetch as any).__nextGetStaticStore?.() + const isRequestInput = + input && + typeof input === 'object' && + typeof (input as Request).method === 'string' + + const getRequestMeta = (field: string) => { + let value = isRequestInput ? (input as any)[field] : null + return value || (init as any)?.[field] + } - // If the staticGenerationStore is not available, we can't do any - // special treatment of fetch, therefore fallback to the original - // fetch implementation. - if ( - !staticGenerationStore || - isInternal || - staticGenerationStore.isDraftMode - ) { - return await tryCatchFetch(input, init) - // return tracedOriginalFetch(input, init) - } + // If the staticGenerationStore is not available, we can't do any + // special treatment of fetch, therefore fallback to the original + // fetch implementation. + if ( + !staticGenerationStore || + isInternal || + staticGenerationStore.isDraftMode + ) { + return await originFetch(input, init) + // return tracedOriginalFetch(input, init) + } - let revalidate: number | undefined | false = undefined - const getNextField = (field: 'revalidate' | 'tags') => { - return typeof init?.next?.[field] !== 'undefined' - ? init?.next?.[field] - : isRequestInput - ? (input as any).next?.[field] - : undefined - } - // RequestInit doesn't keep extra fields e.g. next so it's - // only available if init is used separate - let curRevalidate = getNextField('revalidate') - const tags: string[] = validateTags( - getNextField('tags') || [], - `fetch ${input.toString()}` - ) - - if (Array.isArray(tags)) { - if (!staticGenerationStore.tags) { - staticGenerationStore.tags = [] + let revalidate: number | undefined | false = undefined + const getNextField = (field: 'revalidate' | 'tags') => { + return typeof init?.next?.[field] !== 'undefined' + ? init?.next?.[field] + : isRequestInput + ? (input as any).next?.[field] + : undefined } - for (const tag of tags) { - if (!staticGenerationStore.tags.includes(tag)) { - staticGenerationStore.tags.push(tag) + // RequestInit doesn't keep extra fields e.g. next so it's + // only available if init is used separate + let curRevalidate = getNextField('revalidate') + const tags: string[] = validateTags( + getNextField('tags') || [], + `fetch ${input.toString()}` + ) + + if (Array.isArray(tags)) { + if (!staticGenerationStore.tags) { + staticGenerationStore.tags = [] + } + for (const tag of tags) { + if (!staticGenerationStore.tags.includes(tag)) { + staticGenerationStore.tags.push(tag) + } } } - } - const implicitTags = addImplicitTags(staticGenerationStore) - - const isOnlyCache = staticGenerationStore.fetchCache === 'only-cache' - const isForceCache = staticGenerationStore.fetchCache === 'force-cache' - const isDefaultCache = - staticGenerationStore.fetchCache === 'default-cache' - const isDefaultNoStore = - staticGenerationStore.fetchCache === 'default-no-store' - const isOnlyNoStore = - staticGenerationStore.fetchCache === 'only-no-store' - const isForceNoStore = - staticGenerationStore.fetchCache === 'force-no-store' - - let _cache = getRequestMeta('cache') - let cacheReason = '' - - if ( - typeof _cache === 'string' && - typeof curRevalidate !== 'undefined' - ) { - // when providing fetch with a Request input, it'll automatically set a cache value of 'default' - // we only want to warn if the user is explicitly setting a cache value - if (!(isRequestInput && _cache === 'default')) { - Log.warn( - `fetch for ${fetchUrl} on ${staticGenerationStore.urlPathname} specified "cache: ${_cache}" and "revalidate: ${curRevalidate}", only one should be specified.` - ) + const implicitTags = addImplicitTags(staticGenerationStore) + + const isOnlyCache = staticGenerationStore.fetchCache === 'only-cache' + const isForceCache = + staticGenerationStore.fetchCache === 'force-cache' + const isDefaultCache = + staticGenerationStore.fetchCache === 'default-cache' + const isDefaultNoStore = + staticGenerationStore.fetchCache === 'default-no-store' + const isOnlyNoStore = + staticGenerationStore.fetchCache === 'only-no-store' + const isForceNoStore = + staticGenerationStore.fetchCache === 'force-no-store' + + let _cache = getRequestMeta('cache') + let cacheReason = '' + + if ( + typeof _cache === 'string' && + typeof curRevalidate !== 'undefined' + ) { + // when providing fetch with a Request input, it'll automatically set a cache value of 'default' + // we only want to warn if the user is explicitly setting a cache value + if (!(isRequestInput && _cache === 'default')) { + Log.warn( + `fetch for ${fetchUrl} on ${staticGenerationStore.urlPathname} specified "cache: ${_cache}" and "revalidate: ${curRevalidate}", only one should be specified.` + ) + } + _cache = undefined } - _cache = undefined - } - if (_cache === 'force-cache') { - curRevalidate = false - } else if ( - _cache === 'no-cache' || - _cache === 'no-store' || - isForceNoStore || - isOnlyNoStore - ) { - curRevalidate = 0 - } + if (_cache === 'force-cache') { + curRevalidate = false + } else if ( + _cache === 'no-cache' || + _cache === 'no-store' || + isForceNoStore || + isOnlyNoStore + ) { + curRevalidate = 0 + } - if (_cache === 'no-cache' || _cache === 'no-store') { - cacheReason = `cache: ${_cache}` - } + if (_cache === 'no-cache' || _cache === 'no-store') { + cacheReason = `cache: ${_cache}` + } - revalidate = validateRevalidate( - curRevalidate, - staticGenerationStore.urlPathname - ) - - const _headers = getRequestMeta('headers') - const initHeaders: Headers = - typeof _headers?.get === 'function' - ? _headers - : new Headers(_headers || {}) - - const hasUnCacheableHeader = - initHeaders.get('authorization') || initHeaders.get('cookie') - - const isUnCacheableMethod = !['get', 'head'].includes( - getRequestMeta('method')?.toLowerCase() || 'get' - ) - - // if there are authorized headers or a POST method and - // dynamic data usage was present above the tree we bail - // e.g. if cookies() is used before an authed/POST fetch - const autoNoCache = - (hasUnCacheableHeader || isUnCacheableMethod) && - staticGenerationStore.revalidate === 0 - - if (isForceNoStore) { - cacheReason = 'fetchCache = force-no-store' - } + revalidate = validateRevalidate( + curRevalidate, + staticGenerationStore.urlPathname + ) - if (isOnlyNoStore) { - if ( - _cache === 'force-cache' || - (typeof revalidate !== 'undefined' && - (revalidate === false || revalidate > 0)) - ) { - throw new Error( - `cache: 'force-cache' used on fetch for ${fetchUrl} with 'export const fetchCache = 'only-no-store'` - ) - } - cacheReason = 'fetchCache = only-no-store' - } + const _headers = getRequestMeta('headers') + const initHeaders: Headers = + typeof _headers?.get === 'function' + ? _headers + : new Headers(_headers || {}) - if (isOnlyCache && _cache === 'no-store') { - throw new Error( - `cache: 'no-store' used on fetch for ${fetchUrl} with 'export const fetchCache = 'only-cache'` + const hasUnCacheableHeader = + initHeaders.get('authorization') || initHeaders.get('cookie') + + const isUnCacheableMethod = !['get', 'head'].includes( + getRequestMeta('method')?.toLowerCase() || 'get' ) - } - if ( - isForceCache && - (typeof curRevalidate === 'undefined' || curRevalidate === 0) - ) { - cacheReason = 'fetchCache = force-cache' - revalidate = false - } + // if there are authorized headers or a POST method and + // dynamic data usage was present above the tree we bail + // e.g. if cookies() is used before an authed/POST fetch + const autoNoCache = + (hasUnCacheableHeader || isUnCacheableMethod) && + staticGenerationStore.revalidate === 0 - if (typeof revalidate === 'undefined') { - if (isDefaultCache) { - revalidate = false - cacheReason = 'fetchCache = default-cache' - } else if (autoNoCache) { - revalidate = 0 - cacheReason = 'auto no cache' - } else if (isDefaultNoStore) { - revalidate = 0 - cacheReason = 'fetchCache = default-no-store' - } else { - cacheReason = 'auto cache' - revalidate = - typeof staticGenerationStore.revalidate === 'boolean' || - typeof staticGenerationStore.revalidate === 'undefined' - ? false - : staticGenerationStore.revalidate + if (isForceNoStore) { + cacheReason = 'fetchCache = force-no-store' } - } else if (!cacheReason) { - cacheReason = `revalidate: ${revalidate}` - } - if ( - // when force static is configured we don't bail from - // `revalidate: 0` values - !(staticGenerationStore.forceStatic && revalidate === 0) && - // we don't consider autoNoCache to switch to dynamic during - // revalidate although if it occurs during build we do - !autoNoCache && - // If the revalidate value isn't currently set or the value is less - // than the current revalidate value, we should update the revalidate - // value. - (typeof staticGenerationStore.revalidate === 'undefined' || - (typeof revalidate === 'number' && - (staticGenerationStore.revalidate === false || - (typeof staticGenerationStore.revalidate === 'number' && - revalidate < staticGenerationStore.revalidate)))) - ) { - // If we were setting the revalidate value to 0, we should try to - // postpone instead first. - if (revalidate === 0) { - staticGenerationStore.postpone?.('revalidate: 0') + if (isOnlyNoStore) { + if ( + _cache === 'force-cache' || + (typeof revalidate !== 'undefined' && + (revalidate === false || revalidate > 0)) + ) { + throw new Error( + `cache: 'force-cache' used on fetch for ${fetchUrl} with 'export const fetchCache = 'only-no-store'` + ) + } + cacheReason = 'fetchCache = only-no-store' } - staticGenerationStore.revalidate = revalidate - } + if (isOnlyCache && _cache === 'no-store') { + throw new Error( + `cache: 'no-store' used on fetch for ${fetchUrl} with 'export const fetchCache = 'only-cache'` + ) + } - const isCacheableRevalidate = - (typeof revalidate === 'number' && revalidate > 0) || - revalidate === false - - let cacheKey: string | undefined - if (staticGenerationStore.incrementalCache && isCacheableRevalidate) { - try { - cacheKey = - await staticGenerationStore.incrementalCache.fetchCacheKey( - fetchUrl, - isRequestInput ? (input as RequestInit) : init - ) - } catch (err) { - console.error(`Failed to generate cache key for`, input) + if ( + isForceCache && + (typeof curRevalidate === 'undefined' || curRevalidate === 0) + ) { + cacheReason = 'fetchCache = force-cache' + revalidate = false } - } - const fetchIdx = staticGenerationStore.nextFetchId ?? 1 - staticGenerationStore.nextFetchId = fetchIdx + 1 - - const normalizedRevalidate = - typeof revalidate !== 'number' ? CACHE_ONE_YEAR : revalidate - - const doOriginalFetch = async ( - isStale?: boolean, - cacheReasonOverride?: string - ) => { - const requestInputFields = [ - 'cache', - 'credentials', - 'headers', - 'integrity', - 'keepalive', - 'method', - 'mode', - 'redirect', - 'referrer', - 'referrerPolicy', - 'window', - 'duplex', - - // don't pass through signal when revalidating - ...(isStale ? [] : ['signal']), - ] - - if (isRequestInput) { - const reqInput: Request = input as any - const reqOptions: RequestInit = { - body: (reqInput as any)._ogBody || reqInput.body, + if (typeof revalidate === 'undefined') { + if (isDefaultCache) { + revalidate = false + cacheReason = 'fetchCache = default-cache' + } else if (autoNoCache) { + revalidate = 0 + cacheReason = 'auto no cache' + } else if (isDefaultNoStore) { + revalidate = 0 + cacheReason = 'fetchCache = default-no-store' + } else { + cacheReason = 'auto cache' + revalidate = + typeof staticGenerationStore.revalidate === 'boolean' || + typeof staticGenerationStore.revalidate === 'undefined' + ? false + : staticGenerationStore.revalidate } + } else if (!cacheReason) { + cacheReason = `revalidate: ${revalidate}` + } - for (const field of requestInputFields) { - // @ts-expect-error custom fields - reqOptions[field] = reqInput[field] - } - input = new Request(reqInput.url, reqOptions) - } else if (init) { - const initialInit = init - init = { - body: (init as any)._ogBody || init.body, - } - for (const field of requestInputFields) { - // @ts-expect-error custom fields - init[field] = initialInit[field] + if ( + // when force static is configured we don't bail from + // `revalidate: 0` values + !(staticGenerationStore.forceStatic && revalidate === 0) && + // we don't consider autoNoCache to switch to dynamic during + // revalidate although if it occurs during build we do + !autoNoCache && + // If the revalidate value isn't currently set or the value is less + // than the current revalidate value, we should update the revalidate + // value. + (typeof staticGenerationStore.revalidate === 'undefined' || + (typeof revalidate === 'number' && + (staticGenerationStore.revalidate === false || + (typeof staticGenerationStore.revalidate === 'number' && + revalidate < staticGenerationStore.revalidate)))) + ) { + // If we were setting the revalidate value to 0, we should try to + // postpone instead first. + if (revalidate === 0) { + staticGenerationStore.postpone?.('revalidate: 0') } - } - // add metadata to init without editing the original - const clonedInit = { - ...init, - next: { ...init?.next, fetchType: 'origin', fetchIdx }, + staticGenerationStore.revalidate = revalidate } - return await originFetch(input, clonedInit).then(async (res) => { - if (!isStale) { - trackFetchMetric(staticGenerationStore, { - start: fetchStart, - url: fetchUrl, - cacheReason: cacheReasonOverride || cacheReason, - cacheStatus: - revalidate === 0 || cacheReasonOverride ? 'skip' : 'miss', - status: res.status, - method: clonedInit.method || 'GET', - }) + const isCacheableRevalidate = + (typeof revalidate === 'number' && revalidate > 0) || + revalidate === false + + let cacheKey: string | undefined + if (staticGenerationStore.incrementalCache && isCacheableRevalidate) { + try { + cacheKey = + await staticGenerationStore.incrementalCache.fetchCacheKey( + fetchUrl, + isRequestInput ? (input as RequestInit) : init + ) + } catch (err) { + console.error(`Failed to generate cache key for`, input) } - if ( - res.status === 200 && - staticGenerationStore.incrementalCache && - cacheKey && - isCacheableRevalidate - ) { - const bodyBuffer = Buffer.from(await res.arrayBuffer()) + } - try { - await staticGenerationStore.incrementalCache.set( - cacheKey, - { - kind: 'FETCH', - data: { - headers: Object.fromEntries(res.headers.entries()), - body: bodyBuffer.toString('base64'), - status: res.status, - url: res.url, - }, - revalidate: normalizedRevalidate, - }, - { - fetchCache: true, - revalidate, - fetchUrl, - fetchIdx, - tags, - } - ) - } catch (err) { - console.warn(`Failed to set fetch cache`, input, err) + const fetchIdx = staticGenerationStore.nextFetchId ?? 1 + staticGenerationStore.nextFetchId = fetchIdx + 1 + + const normalizedRevalidate = + typeof revalidate !== 'number' ? CACHE_ONE_YEAR : revalidate + + const doOriginalFetch = async ( + isStale?: boolean, + cacheReasonOverride?: string + ) => { + const requestInputFields = [ + 'cache', + 'credentials', + 'headers', + 'integrity', + 'keepalive', + 'method', + 'mode', + 'redirect', + 'referrer', + 'referrerPolicy', + 'window', + 'duplex', + + // don't pass through signal when revalidating + ...(isStale ? [] : ['signal']), + ] + + if (isRequestInput) { + const reqInput: Request = input as any + const reqOptions: RequestInit = { + body: (reqInput as any)._ogBody || reqInput.body, } - const response = new Response(bodyBuffer, { - headers: new Headers(res.headers), - status: res.status, - }) - Object.defineProperty(response, 'url', { value: res.url }) - return response + for (const field of requestInputFields) { + // @ts-expect-error custom fields + reqOptions[field] = reqInput[field] + } + input = new Request(reqInput.url, reqOptions) + } else if (init) { + const initialInit = init + init = { + body: (init as any)._ogBody || init.body, + } + for (const field of requestInputFields) { + // @ts-expect-error custom fields + init[field] = initialInit[field] + } } - return res - }) - } - let handleUnlock = () => Promise.resolve() - let cacheReasonOverride + // add metadata to init without editing the original + const clonedInit = { + ...init, + next: { ...init?.next, fetchType: 'origin', fetchIdx }, + } - if (cacheKey && staticGenerationStore.incrementalCache) { - handleUnlock = await staticGenerationStore.incrementalCache.lock( - cacheKey - ) + return await originFetch(input, clonedInit).then(async (res) => { + if (!isStale) { + trackFetchMetric(staticGenerationStore, { + start: fetchStart, + url: fetchUrl, + cacheReason: cacheReasonOverride || cacheReason, + cacheStatus: + revalidate === 0 || cacheReasonOverride ? 'skip' : 'miss', + status: res.status, + method: clonedInit.method || 'GET', + }) + } + if ( + res.status === 200 && + staticGenerationStore.incrementalCache && + cacheKey && + isCacheableRevalidate + ) { + const bodyBuffer = Buffer.from(await res.arrayBuffer()) + + try { + await staticGenerationStore.incrementalCache.set( + cacheKey, + { + kind: 'FETCH', + data: { + headers: Object.fromEntries(res.headers.entries()), + body: bodyBuffer.toString('base64'), + status: res.status, + url: res.url, + }, + revalidate: normalizedRevalidate, + }, + { + fetchCache: true, + revalidate, + fetchUrl, + fetchIdx, + tags, + } + ) + } catch (err) { + console.warn(`Failed to set fetch cache`, input, err) + } - const entry = staticGenerationStore.isOnDemandRevalidate - ? null - : await staticGenerationStore.incrementalCache.get(cacheKey, { - kindHint: 'fetch', - revalidate, - fetchUrl, - fetchIdx, - tags, - softTags: implicitTags, - }) - - if (entry) { - // await handleUnlock() - } else { - // in dev, incremental cache response will be null in case the browser adds `cache-control: no-cache` in the request headers - cacheReasonOverride = 'cache-control: no-cache (hard refresh)' + const response = new Response(bodyBuffer, { + headers: new Headers(res.headers), + status: res.status, + }) + Object.defineProperty(response, 'url', { value: res.url }) + return response + } + return res + }) } - if (entry?.value && entry.value.kind === 'FETCH') { - console.log('cache promise') - // when stale and is revalidating we wait for fresh data - // so the revalidated entry has the updated data - if (!(staticGenerationStore.isRevalidate && entry.isStale)) { - if (entry.isStale) { - staticGenerationStore.pendingRevalidates ??= {} - if (!staticGenerationStore.pendingRevalidates[cacheKey]) { - staticGenerationStore.pendingRevalidates[cacheKey] = - doOriginalFetch(true).catch(console.error) + let handleUnlock = () => Promise.resolve() + let cacheReasonOverride + + if (cacheKey && staticGenerationStore.incrementalCache) { + handleUnlock = await staticGenerationStore.incrementalCache.lock( + cacheKey + ) + + const entry = staticGenerationStore.isOnDemandRevalidate + ? null + : await staticGenerationStore.incrementalCache.get(cacheKey, { + kindHint: 'fetch', + revalidate, + fetchUrl, + fetchIdx, + tags, + softTags: implicitTags, + }) + + if (entry) { + // await handleUnlock() + } else { + // in dev, incremental cache response will be null in case the browser adds `cache-control: no-cache` in the request headers + cacheReasonOverride = 'cache-control: no-cache (hard refresh)' + } + + if (entry?.value && entry.value.kind === 'FETCH') { + console.log('cache promise') + // when stale and is revalidating we wait for fresh data + // so the revalidated entry has the updated data + if (!(staticGenerationStore.isRevalidate && entry.isStale)) { + if (entry.isStale) { + staticGenerationStore.pendingRevalidates ??= {} + if (!staticGenerationStore.pendingRevalidates[cacheKey]) { + staticGenerationStore.pendingRevalidates[cacheKey] = + doOriginalFetch(true).catch(console.error) + } } + const resData = entry.value.data + + trackFetchMetric(staticGenerationStore, { + start: fetchStart, + url: fetchUrl, + cacheReason, + cacheStatus: 'hit', + status: resData.status || 200, + method: init?.method || 'GET', + }) + + const response = new Response( + Buffer.from(resData.body, 'base64'), + { + headers: resData.headers, + status: resData.status, + } + ) + Object.defineProperty(response, 'url', { + value: entry.value.data.url, + }) + return response } - const resData = entry.value.data - - trackFetchMetric(staticGenerationStore, { - start: fetchStart, - url: fetchUrl, - cacheReason, - cacheStatus: 'hit', - status: resData.status || 200, - method: init?.method || 'GET', - }) - - const response = new Response( - Buffer.from(resData.body, 'base64'), - { - headers: resData.headers, - status: resData.status, - } - ) - Object.defineProperty(response, 'url', { - value: entry.value.data.url, - }) - return response } } - } - if ( - staticGenerationStore.isStaticGeneration && - init && - typeof init === 'object' - ) { - const { cache } = init - - // Delete `cache` property as Cloudflare Workers will throw an error - if (isEdgeRuntime) delete init.cache - - if (!staticGenerationStore.forceStatic && cache === 'no-store') { - const dynamicUsageReason = `no-store fetch ${input}${ - staticGenerationStore.urlPathname - ? ` ${staticGenerationStore.urlPathname}` - : '' - }` - - // If enabled, we should bail out of static generation. - staticGenerationStore.postpone?.(dynamicUsageReason) - - // PPR is not enabled, or React postpone is not available, we - // should set the revalidate to 0. - staticGenerationStore.revalidate = 0 - - const err = new DynamicServerError(dynamicUsageReason) - staticGenerationStore.dynamicUsageErr = err - staticGenerationStore.dynamicUsageDescription = dynamicUsageReason - } - - const hasNextConfig = 'next' in init - const { next = {} } = init if ( - typeof next.revalidate === 'number' && - (typeof staticGenerationStore.revalidate === 'undefined' || - (typeof staticGenerationStore.revalidate === 'number' && - next.revalidate < staticGenerationStore.revalidate)) + staticGenerationStore.isStaticGeneration && + init && + typeof init === 'object' ) { - if ( - !staticGenerationStore.forceDynamic && - !staticGenerationStore.forceStatic && - next.revalidate === 0 - ) { - const dynamicUsageReason = `revalidate: 0 fetch ${input}${ + const { cache } = init + + // Delete `cache` property as Cloudflare Workers will throw an error + if (isEdgeRuntime) delete init.cache + + if (!staticGenerationStore.forceStatic && cache === 'no-store') { + const dynamicUsageReason = `no-store fetch ${input}${ staticGenerationStore.urlPathname ? ` ${staticGenerationStore.urlPathname}` : '' @@ -726,21 +692,57 @@ export function patchFetch({ // If enabled, we should bail out of static generation. staticGenerationStore.postpone?.(dynamicUsageReason) + // PPR is not enabled, or React postpone is not available, we + // should set the revalidate to 0. + staticGenerationStore.revalidate = 0 + const err = new DynamicServerError(dynamicUsageReason) staticGenerationStore.dynamicUsageErr = err staticGenerationStore.dynamicUsageDescription = dynamicUsageReason } - if (!staticGenerationStore.forceStatic || next.revalidate !== 0) { - staticGenerationStore.revalidate = next.revalidate + const hasNextConfig = 'next' in init + const { next = {} } = init + if ( + typeof next.revalidate === 'number' && + (typeof staticGenerationStore.revalidate === 'undefined' || + (typeof staticGenerationStore.revalidate === 'number' && + next.revalidate < staticGenerationStore.revalidate)) + ) { + if ( + !staticGenerationStore.forceDynamic && + !staticGenerationStore.forceStatic && + next.revalidate === 0 + ) { + const dynamicUsageReason = `revalidate: 0 fetch ${input}${ + staticGenerationStore.urlPathname + ? ` ${staticGenerationStore.urlPathname}` + : '' + }` + + // If enabled, we should bail out of static generation. + staticGenerationStore.postpone?.(dynamicUsageReason) + + const err = new DynamicServerError(dynamicUsageReason) + staticGenerationStore.dynamicUsageErr = err + staticGenerationStore.dynamicUsageDescription = + dynamicUsageReason + } + + if (!staticGenerationStore.forceStatic || next.revalidate !== 0) { + staticGenerationStore.revalidate = next.revalidate + } } + if (hasNextConfig) delete init.next } - if (hasNextConfig) delete init.next - } - return await doOriginalFetch(false, cacheReasonOverride).finally( - handleUnlock - ) + return await doOriginalFetch(false, cacheReasonOverride).finally( + handleUnlock + ) + } catch (e) { + // @ts-ignore + throw new TypeError(e?.message, { cause: e?.cause }) + } } ) } From 4a10d13290c22865fdbbbe864b320237160582c2 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 9 Jan 2024 21:33:15 +0100 Subject: [PATCH 12/20] fix tracer --- packages/next/src/server/lib/patch-fetch.ts | 856 +++++++++---------- packages/next/src/server/lib/trace/tracer.ts | 12 +- 2 files changed, 404 insertions(+), 464 deletions(-) diff --git a/packages/next/src/server/lib/patch-fetch.ts b/packages/next/src/server/lib/patch-fetch.ts index 42dc871b9bfa45..214ca469a92860 100644 --- a/packages/next/src/server/lib/patch-fetch.ts +++ b/packages/next/src/server/lib/patch-fetch.ts @@ -176,39 +176,6 @@ interface PatchableModule { staticGenerationAsyncStorage: StaticGenerationAsyncStorage } -/* - * When the fetch throws an error, the error stack trace will be: - * `Error: fetch failed` + node-internal trace -> `Error: fetch failed` + userland trace - */ -function traceErroredFetcher( - fetchFn: typeof fetch, - tracingError: Error -): typeof fetch { - // Remove the error message and the trace line inside `patchFetch()` - const modifiedTrace = tracingError.stack?.split('\n').slice(2).join('\n') - const tracedFetch: typeof fetch = async function (...args) { - return await fetchFn(...args).catch((err) => { - // If it's failed with internal fetch call, and there's only node:internal traces - if ( - err instanceof Error && - err.name === 'TypeError' && - err.message === 'fetch failed' - ) { - const traces: string[] = err.stack?.split('\n') || [] - // trace without the error message - const originStackTrace = traces.slice(1) - if (originStackTrace.every((line) => line.includes('node:internal'))) { - // Combine the origin error message with the modified trace containing userland trace - err.stack = traces[0] + '\n' + modifiedTrace - throw err - } - } - throw err - }) - } - return tracedFetch -} - // we patch fetch to collect cache information used for // determining if a page is static or not export function patchFetch({ @@ -228,24 +195,6 @@ export function patchFetch({ input: RequestInfo | URL, init: RequestInit | undefined ) { - /** - * The tracing error is used to get the stack trace of the fetch call, when the fetch is executed in the - * different tick, where the stack trace is not available to trace back to original invoked place. - * - * e.g. You might see failed fetch stack trace like this: - * > fetch - * > process.processTicksAndRejections - * - * This tracing error will preserve the original stack trace, so that we can trace back to the original, - * we'll use it to replace the stack trace of the error thrown by the fetch call, once we detect there's only - * unhelpful internal call trace showed up. - * - */ - const tracingError = new Error() - const tryCatchFetch = async (...args: Parameters) => {} - - // traceErroredFetcher(originFetch, tracingError)(...args) - let url: URL | undefined try { url = new URL(input instanceof Request ? input.url : input) @@ -276,414 +225,441 @@ export function patchFetch({ }, }, async () => { - try { - // return await originFetch(...args) - - const staticGenerationStore: StaticGenerationStore = - staticGenerationAsyncStorage.getStore() || - (fetch as any).__nextGetStaticStore?.() - const isRequestInput = - input && - typeof input === 'object' && - typeof (input as Request).method === 'string' - - const getRequestMeta = (field: string) => { - let value = isRequestInput ? (input as any)[field] : null - return value || (init as any)?.[field] - } - - // If the staticGenerationStore is not available, we can't do any - // special treatment of fetch, therefore fallback to the original - // fetch implementation. - if ( - !staticGenerationStore || - isInternal || - staticGenerationStore.isDraftMode - ) { - return await originFetch(input, init) - // return tracedOriginalFetch(input, init) - } + const staticGenerationStore: StaticGenerationStore = + staticGenerationAsyncStorage.getStore() || + (fetch as any).__nextGetStaticStore?.() + const isRequestInput = + input && + typeof input === 'object' && + typeof (input as Request).method === 'string' + + const getRequestMeta = (field: string) => { + let value = isRequestInput ? (input as any)[field] : null + return value || (init as any)?.[field] + } - let revalidate: number | undefined | false = undefined - const getNextField = (field: 'revalidate' | 'tags') => { - return typeof init?.next?.[field] !== 'undefined' - ? init?.next?.[field] - : isRequestInput - ? (input as any).next?.[field] - : undefined - } - // RequestInit doesn't keep extra fields e.g. next so it's - // only available if init is used separate - let curRevalidate = getNextField('revalidate') - const tags: string[] = validateTags( - getNextField('tags') || [], - `fetch ${input.toString()}` - ) + // If the staticGenerationStore is not available, we can't do any + // special treatment of fetch, therefore fallback to the original + // fetch implementation. + if ( + !staticGenerationStore || + isInternal || + staticGenerationStore.isDraftMode + ) { + return await originFetch(input, init) + // return tracedOriginalFetch(input, init) + } - if (Array.isArray(tags)) { - if (!staticGenerationStore.tags) { - staticGenerationStore.tags = [] - } - for (const tag of tags) { - if (!staticGenerationStore.tags.includes(tag)) { - staticGenerationStore.tags.push(tag) - } - } + let revalidate: number | undefined | false = undefined + const getNextField = (field: 'revalidate' | 'tags') => { + return typeof init?.next?.[field] !== 'undefined' + ? init?.next?.[field] + : isRequestInput + ? (input as any).next?.[field] + : undefined + } + // RequestInit doesn't keep extra fields e.g. next so it's + // only available if init is used separate + let curRevalidate = getNextField('revalidate') + const tags: string[] = validateTags( + getNextField('tags') || [], + `fetch ${input.toString()}` + ) + + if (Array.isArray(tags)) { + if (!staticGenerationStore.tags) { + staticGenerationStore.tags = [] } - const implicitTags = addImplicitTags(staticGenerationStore) - - const isOnlyCache = staticGenerationStore.fetchCache === 'only-cache' - const isForceCache = - staticGenerationStore.fetchCache === 'force-cache' - const isDefaultCache = - staticGenerationStore.fetchCache === 'default-cache' - const isDefaultNoStore = - staticGenerationStore.fetchCache === 'default-no-store' - const isOnlyNoStore = - staticGenerationStore.fetchCache === 'only-no-store' - const isForceNoStore = - staticGenerationStore.fetchCache === 'force-no-store' - - let _cache = getRequestMeta('cache') - let cacheReason = '' - - if ( - typeof _cache === 'string' && - typeof curRevalidate !== 'undefined' - ) { - // when providing fetch with a Request input, it'll automatically set a cache value of 'default' - // we only want to warn if the user is explicitly setting a cache value - if (!(isRequestInput && _cache === 'default')) { - Log.warn( - `fetch for ${fetchUrl} on ${staticGenerationStore.urlPathname} specified "cache: ${_cache}" and "revalidate: ${curRevalidate}", only one should be specified.` - ) + for (const tag of tags) { + if (!staticGenerationStore.tags.includes(tag)) { + staticGenerationStore.tags.push(tag) } - _cache = undefined } - - if (_cache === 'force-cache') { - curRevalidate = false - } else if ( - _cache === 'no-cache' || - _cache === 'no-store' || - isForceNoStore || - isOnlyNoStore - ) { - curRevalidate = 0 + } + const implicitTags = addImplicitTags(staticGenerationStore) + + const isOnlyCache = staticGenerationStore.fetchCache === 'only-cache' + const isForceCache = staticGenerationStore.fetchCache === 'force-cache' + const isDefaultCache = + staticGenerationStore.fetchCache === 'default-cache' + const isDefaultNoStore = + staticGenerationStore.fetchCache === 'default-no-store' + const isOnlyNoStore = + staticGenerationStore.fetchCache === 'only-no-store' + const isForceNoStore = + staticGenerationStore.fetchCache === 'force-no-store' + + let _cache = getRequestMeta('cache') + let cacheReason = '' + + if ( + typeof _cache === 'string' && + typeof curRevalidate !== 'undefined' + ) { + // when providing fetch with a Request input, it'll automatically set a cache value of 'default' + // we only want to warn if the user is explicitly setting a cache value + if (!(isRequestInput && _cache === 'default')) { + Log.warn( + `fetch for ${fetchUrl} on ${staticGenerationStore.urlPathname} specified "cache: ${_cache}" and "revalidate: ${curRevalidate}", only one should be specified.` + ) } + _cache = undefined + } - if (_cache === 'no-cache' || _cache === 'no-store') { - cacheReason = `cache: ${_cache}` - } + if (_cache === 'force-cache') { + curRevalidate = false + } else if ( + _cache === 'no-cache' || + _cache === 'no-store' || + isForceNoStore || + isOnlyNoStore + ) { + curRevalidate = 0 + } - revalidate = validateRevalidate( - curRevalidate, - staticGenerationStore.urlPathname - ) + if (_cache === 'no-cache' || _cache === 'no-store') { + cacheReason = `cache: ${_cache}` + } - const _headers = getRequestMeta('headers') - const initHeaders: Headers = - typeof _headers?.get === 'function' - ? _headers - : new Headers(_headers || {}) + revalidate = validateRevalidate( + curRevalidate, + staticGenerationStore.urlPathname + ) + + const _headers = getRequestMeta('headers') + const initHeaders: Headers = + typeof _headers?.get === 'function' + ? _headers + : new Headers(_headers || {}) + + const hasUnCacheableHeader = + initHeaders.get('authorization') || initHeaders.get('cookie') + + const isUnCacheableMethod = !['get', 'head'].includes( + getRequestMeta('method')?.toLowerCase() || 'get' + ) + + // if there are authorized headers or a POST method and + // dynamic data usage was present above the tree we bail + // e.g. if cookies() is used before an authed/POST fetch + const autoNoCache = + (hasUnCacheableHeader || isUnCacheableMethod) && + staticGenerationStore.revalidate === 0 + + if (isForceNoStore) { + cacheReason = 'fetchCache = force-no-store' + } - const hasUnCacheableHeader = - initHeaders.get('authorization') || initHeaders.get('cookie') + if (isOnlyNoStore) { + if ( + _cache === 'force-cache' || + (typeof revalidate !== 'undefined' && + (revalidate === false || revalidate > 0)) + ) { + throw new Error( + `cache: 'force-cache' used on fetch for ${fetchUrl} with 'export const fetchCache = 'only-no-store'` + ) + } + cacheReason = 'fetchCache = only-no-store' + } - const isUnCacheableMethod = !['get', 'head'].includes( - getRequestMeta('method')?.toLowerCase() || 'get' + if (isOnlyCache && _cache === 'no-store') { + throw new Error( + `cache: 'no-store' used on fetch for ${fetchUrl} with 'export const fetchCache = 'only-cache'` ) + } - // if there are authorized headers or a POST method and - // dynamic data usage was present above the tree we bail - // e.g. if cookies() is used before an authed/POST fetch - const autoNoCache = - (hasUnCacheableHeader || isUnCacheableMethod) && - staticGenerationStore.revalidate === 0 + if ( + isForceCache && + (typeof curRevalidate === 'undefined' || curRevalidate === 0) + ) { + cacheReason = 'fetchCache = force-cache' + revalidate = false + } - if (isForceNoStore) { - cacheReason = 'fetchCache = force-no-store' + if (typeof revalidate === 'undefined') { + if (isDefaultCache) { + revalidate = false + cacheReason = 'fetchCache = default-cache' + } else if (autoNoCache) { + revalidate = 0 + cacheReason = 'auto no cache' + } else if (isDefaultNoStore) { + revalidate = 0 + cacheReason = 'fetchCache = default-no-store' + } else { + cacheReason = 'auto cache' + revalidate = + typeof staticGenerationStore.revalidate === 'boolean' || + typeof staticGenerationStore.revalidate === 'undefined' + ? false + : staticGenerationStore.revalidate } + } else if (!cacheReason) { + cacheReason = `revalidate: ${revalidate}` + } - if (isOnlyNoStore) { - if ( - _cache === 'force-cache' || - (typeof revalidate !== 'undefined' && - (revalidate === false || revalidate > 0)) - ) { - throw new Error( - `cache: 'force-cache' used on fetch for ${fetchUrl} with 'export const fetchCache = 'only-no-store'` - ) - } - cacheReason = 'fetchCache = only-no-store' + if ( + // when force static is configured we don't bail from + // `revalidate: 0` values + !(staticGenerationStore.forceStatic && revalidate === 0) && + // we don't consider autoNoCache to switch to dynamic during + // revalidate although if it occurs during build we do + !autoNoCache && + // If the revalidate value isn't currently set or the value is less + // than the current revalidate value, we should update the revalidate + // value. + (typeof staticGenerationStore.revalidate === 'undefined' || + (typeof revalidate === 'number' && + (staticGenerationStore.revalidate === false || + (typeof staticGenerationStore.revalidate === 'number' && + revalidate < staticGenerationStore.revalidate)))) + ) { + // If we were setting the revalidate value to 0, we should try to + // postpone instead first. + if (revalidate === 0) { + staticGenerationStore.postpone?.('revalidate: 0') } - if (isOnlyCache && _cache === 'no-store') { - throw new Error( - `cache: 'no-store' used on fetch for ${fetchUrl} with 'export const fetchCache = 'only-cache'` - ) - } + staticGenerationStore.revalidate = revalidate + } - if ( - isForceCache && - (typeof curRevalidate === 'undefined' || curRevalidate === 0) - ) { - cacheReason = 'fetchCache = force-cache' - revalidate = false + const isCacheableRevalidate = + (typeof revalidate === 'number' && revalidate > 0) || + revalidate === false + + let cacheKey: string | undefined + if (staticGenerationStore.incrementalCache && isCacheableRevalidate) { + try { + cacheKey = + await staticGenerationStore.incrementalCache.fetchCacheKey( + fetchUrl, + isRequestInput ? (input as RequestInit) : init + ) + } catch (err) { + console.error(`Failed to generate cache key for`, input) } + } - if (typeof revalidate === 'undefined') { - if (isDefaultCache) { - revalidate = false - cacheReason = 'fetchCache = default-cache' - } else if (autoNoCache) { - revalidate = 0 - cacheReason = 'auto no cache' - } else if (isDefaultNoStore) { - revalidate = 0 - cacheReason = 'fetchCache = default-no-store' - } else { - cacheReason = 'auto cache' - revalidate = - typeof staticGenerationStore.revalidate === 'boolean' || - typeof staticGenerationStore.revalidate === 'undefined' - ? false - : staticGenerationStore.revalidate + const fetchIdx = staticGenerationStore.nextFetchId ?? 1 + staticGenerationStore.nextFetchId = fetchIdx + 1 + + const normalizedRevalidate = + typeof revalidate !== 'number' ? CACHE_ONE_YEAR : revalidate + + const doOriginalFetch = async ( + isStale?: boolean, + cacheReasonOverride?: string + ) => { + const requestInputFields = [ + 'cache', + 'credentials', + 'headers', + 'integrity', + 'keepalive', + 'method', + 'mode', + 'redirect', + 'referrer', + 'referrerPolicy', + 'window', + 'duplex', + + // don't pass through signal when revalidating + ...(isStale ? [] : ['signal']), + ] + + if (isRequestInput) { + const reqInput: Request = input as any + const reqOptions: RequestInit = { + body: (reqInput as any)._ogBody || reqInput.body, } - } else if (!cacheReason) { - cacheReason = `revalidate: ${revalidate}` - } - if ( - // when force static is configured we don't bail from - // `revalidate: 0` values - !(staticGenerationStore.forceStatic && revalidate === 0) && - // we don't consider autoNoCache to switch to dynamic during - // revalidate although if it occurs during build we do - !autoNoCache && - // If the revalidate value isn't currently set or the value is less - // than the current revalidate value, we should update the revalidate - // value. - (typeof staticGenerationStore.revalidate === 'undefined' || - (typeof revalidate === 'number' && - (staticGenerationStore.revalidate === false || - (typeof staticGenerationStore.revalidate === 'number' && - revalidate < staticGenerationStore.revalidate)))) - ) { - // If we were setting the revalidate value to 0, we should try to - // postpone instead first. - if (revalidate === 0) { - staticGenerationStore.postpone?.('revalidate: 0') + for (const field of requestInputFields) { + // @ts-expect-error custom fields + reqOptions[field] = reqInput[field] + } + input = new Request(reqInput.url, reqOptions) + } else if (init) { + const initialInit = init + init = { + body: (init as any)._ogBody || init.body, + } + for (const field of requestInputFields) { + // @ts-expect-error custom fields + init[field] = initialInit[field] } + } - staticGenerationStore.revalidate = revalidate + // add metadata to init without editing the original + const clonedInit = { + ...init, + next: { ...init?.next, fetchType: 'origin', fetchIdx }, } - const isCacheableRevalidate = - (typeof revalidate === 'number' && revalidate > 0) || - revalidate === false - - let cacheKey: string | undefined - if (staticGenerationStore.incrementalCache && isCacheableRevalidate) { - try { - cacheKey = - await staticGenerationStore.incrementalCache.fetchCacheKey( - fetchUrl, - isRequestInput ? (input as RequestInit) : init - ) - } catch (err) { - console.error(`Failed to generate cache key for`, input) + return await originFetch(input, clonedInit).then(async (res) => { + if (!isStale) { + trackFetchMetric(staticGenerationStore, { + start: fetchStart, + url: fetchUrl, + cacheReason: cacheReasonOverride || cacheReason, + cacheStatus: + revalidate === 0 || cacheReasonOverride ? 'skip' : 'miss', + status: res.status, + method: clonedInit.method || 'GET', + }) } - } + if ( + res.status === 200 && + staticGenerationStore.incrementalCache && + cacheKey && + isCacheableRevalidate + ) { + const bodyBuffer = Buffer.from(await res.arrayBuffer()) - const fetchIdx = staticGenerationStore.nextFetchId ?? 1 - staticGenerationStore.nextFetchId = fetchIdx + 1 - - const normalizedRevalidate = - typeof revalidate !== 'number' ? CACHE_ONE_YEAR : revalidate - - const doOriginalFetch = async ( - isStale?: boolean, - cacheReasonOverride?: string - ) => { - const requestInputFields = [ - 'cache', - 'credentials', - 'headers', - 'integrity', - 'keepalive', - 'method', - 'mode', - 'redirect', - 'referrer', - 'referrerPolicy', - 'window', - 'duplex', - - // don't pass through signal when revalidating - ...(isStale ? [] : ['signal']), - ] - - if (isRequestInput) { - const reqInput: Request = input as any - const reqOptions: RequestInit = { - body: (reqInput as any)._ogBody || reqInput.body, + try { + await staticGenerationStore.incrementalCache.set( + cacheKey, + { + kind: 'FETCH', + data: { + headers: Object.fromEntries(res.headers.entries()), + body: bodyBuffer.toString('base64'), + status: res.status, + url: res.url, + }, + revalidate: normalizedRevalidate, + }, + { + fetchCache: true, + revalidate, + fetchUrl, + fetchIdx, + tags, + } + ) + } catch (err) { + console.warn(`Failed to set fetch cache`, input, err) } - for (const field of requestInputFields) { - // @ts-expect-error custom fields - reqOptions[field] = reqInput[field] - } - input = new Request(reqInput.url, reqOptions) - } else if (init) { - const initialInit = init - init = { - body: (init as any)._ogBody || init.body, - } - for (const field of requestInputFields) { - // @ts-expect-error custom fields - init[field] = initialInit[field] - } + const response = new Response(bodyBuffer, { + headers: new Headers(res.headers), + status: res.status, + }) + Object.defineProperty(response, 'url', { value: res.url }) + return response } + return res + }) + } - // add metadata to init without editing the original - const clonedInit = { - ...init, - next: { ...init?.next, fetchType: 'origin', fetchIdx }, - } + let handleUnlock = () => Promise.resolve() + let cacheReasonOverride - return await originFetch(input, clonedInit).then(async (res) => { - if (!isStale) { - trackFetchMetric(staticGenerationStore, { - start: fetchStart, - url: fetchUrl, - cacheReason: cacheReasonOverride || cacheReason, - cacheStatus: - revalidate === 0 || cacheReasonOverride ? 'skip' : 'miss', - status: res.status, - method: clonedInit.method || 'GET', - }) - } - if ( - res.status === 200 && - staticGenerationStore.incrementalCache && - cacheKey && - isCacheableRevalidate - ) { - const bodyBuffer = Buffer.from(await res.arrayBuffer()) - - try { - await staticGenerationStore.incrementalCache.set( - cacheKey, - { - kind: 'FETCH', - data: { - headers: Object.fromEntries(res.headers.entries()), - body: bodyBuffer.toString('base64'), - status: res.status, - url: res.url, - }, - revalidate: normalizedRevalidate, - }, - { - fetchCache: true, - revalidate, - fetchUrl, - fetchIdx, - tags, - } - ) - } catch (err) { - console.warn(`Failed to set fetch cache`, input, err) - } + if (cacheKey && staticGenerationStore.incrementalCache) { + handleUnlock = await staticGenerationStore.incrementalCache.lock( + cacheKey + ) - const response = new Response(bodyBuffer, { - headers: new Headers(res.headers), - status: res.status, - }) - Object.defineProperty(response, 'url', { value: res.url }) - return response - } - return res - }) + const entry = staticGenerationStore.isOnDemandRevalidate + ? null + : await staticGenerationStore.incrementalCache.get(cacheKey, { + kindHint: 'fetch', + revalidate, + fetchUrl, + fetchIdx, + tags, + softTags: implicitTags, + }) + + if (entry) { + // await handleUnlock() + } else { + // in dev, incremental cache response will be null in case the browser adds `cache-control: no-cache` in the request headers + cacheReasonOverride = 'cache-control: no-cache (hard refresh)' } - let handleUnlock = () => Promise.resolve() - let cacheReasonOverride - - if (cacheKey && staticGenerationStore.incrementalCache) { - handleUnlock = await staticGenerationStore.incrementalCache.lock( - cacheKey - ) - - const entry = staticGenerationStore.isOnDemandRevalidate - ? null - : await staticGenerationStore.incrementalCache.get(cacheKey, { - kindHint: 'fetch', - revalidate, - fetchUrl, - fetchIdx, - tags, - softTags: implicitTags, - }) - - if (entry) { - // await handleUnlock() - } else { - // in dev, incremental cache response will be null in case the browser adds `cache-control: no-cache` in the request headers - cacheReasonOverride = 'cache-control: no-cache (hard refresh)' - } - - if (entry?.value && entry.value.kind === 'FETCH') { - console.log('cache promise') - // when stale and is revalidating we wait for fresh data - // so the revalidated entry has the updated data - if (!(staticGenerationStore.isRevalidate && entry.isStale)) { - if (entry.isStale) { - staticGenerationStore.pendingRevalidates ??= {} - if (!staticGenerationStore.pendingRevalidates[cacheKey]) { - staticGenerationStore.pendingRevalidates[cacheKey] = - doOriginalFetch(true).catch(console.error) - } + if (entry?.value && entry.value.kind === 'FETCH') { + console.log('cache promise') + // when stale and is revalidating we wait for fresh data + // so the revalidated entry has the updated data + if (!(staticGenerationStore.isRevalidate && entry.isStale)) { + if (entry.isStale) { + staticGenerationStore.pendingRevalidates ??= {} + if (!staticGenerationStore.pendingRevalidates[cacheKey]) { + staticGenerationStore.pendingRevalidates[cacheKey] = + doOriginalFetch(true).catch(console.error) } - const resData = entry.value.data - - trackFetchMetric(staticGenerationStore, { - start: fetchStart, - url: fetchUrl, - cacheReason, - cacheStatus: 'hit', - status: resData.status || 200, - method: init?.method || 'GET', - }) - - const response = new Response( - Buffer.from(resData.body, 'base64'), - { - headers: resData.headers, - status: resData.status, - } - ) - Object.defineProperty(response, 'url', { - value: entry.value.data.url, - }) - return response } + const resData = entry.value.data + + trackFetchMetric(staticGenerationStore, { + start: fetchStart, + url: fetchUrl, + cacheReason, + cacheStatus: 'hit', + status: resData.status || 200, + method: init?.method || 'GET', + }) + + const response = new Response( + Buffer.from(resData.body, 'base64'), + { + headers: resData.headers, + status: resData.status, + } + ) + Object.defineProperty(response, 'url', { + value: entry.value.data.url, + }) + return response } } + } + if ( + staticGenerationStore.isStaticGeneration && + init && + typeof init === 'object' + ) { + const { cache } = init + + // Delete `cache` property as Cloudflare Workers will throw an error + if (isEdgeRuntime) delete init.cache + + if (!staticGenerationStore.forceStatic && cache === 'no-store') { + const dynamicUsageReason = `no-store fetch ${input}${ + staticGenerationStore.urlPathname + ? ` ${staticGenerationStore.urlPathname}` + : '' + }` + + // If enabled, we should bail out of static generation. + staticGenerationStore.postpone?.(dynamicUsageReason) + + // PPR is not enabled, or React postpone is not available, we + // should set the revalidate to 0. + staticGenerationStore.revalidate = 0 + + const err = new DynamicServerError(dynamicUsageReason) + staticGenerationStore.dynamicUsageErr = err + staticGenerationStore.dynamicUsageDescription = dynamicUsageReason + } + + const hasNextConfig = 'next' in init + const { next = {} } = init if ( - staticGenerationStore.isStaticGeneration && - init && - typeof init === 'object' + typeof next.revalidate === 'number' && + (typeof staticGenerationStore.revalidate === 'undefined' || + (typeof staticGenerationStore.revalidate === 'number' && + next.revalidate < staticGenerationStore.revalidate)) ) { - const { cache } = init - - // Delete `cache` property as Cloudflare Workers will throw an error - if (isEdgeRuntime) delete init.cache - - if (!staticGenerationStore.forceStatic && cache === 'no-store') { - const dynamicUsageReason = `no-store fetch ${input}${ + if ( + !staticGenerationStore.forceDynamic && + !staticGenerationStore.forceStatic && + next.revalidate === 0 + ) { + const dynamicUsageReason = `revalidate: 0 fetch ${input}${ staticGenerationStore.urlPathname ? ` ${staticGenerationStore.urlPathname}` : '' @@ -692,57 +668,21 @@ export function patchFetch({ // If enabled, we should bail out of static generation. staticGenerationStore.postpone?.(dynamicUsageReason) - // PPR is not enabled, or React postpone is not available, we - // should set the revalidate to 0. - staticGenerationStore.revalidate = 0 - const err = new DynamicServerError(dynamicUsageReason) staticGenerationStore.dynamicUsageErr = err staticGenerationStore.dynamicUsageDescription = dynamicUsageReason } - const hasNextConfig = 'next' in init - const { next = {} } = init - if ( - typeof next.revalidate === 'number' && - (typeof staticGenerationStore.revalidate === 'undefined' || - (typeof staticGenerationStore.revalidate === 'number' && - next.revalidate < staticGenerationStore.revalidate)) - ) { - if ( - !staticGenerationStore.forceDynamic && - !staticGenerationStore.forceStatic && - next.revalidate === 0 - ) { - const dynamicUsageReason = `revalidate: 0 fetch ${input}${ - staticGenerationStore.urlPathname - ? ` ${staticGenerationStore.urlPathname}` - : '' - }` - - // If enabled, we should bail out of static generation. - staticGenerationStore.postpone?.(dynamicUsageReason) - - const err = new DynamicServerError(dynamicUsageReason) - staticGenerationStore.dynamicUsageErr = err - staticGenerationStore.dynamicUsageDescription = - dynamicUsageReason - } - - if (!staticGenerationStore.forceStatic || next.revalidate !== 0) { - staticGenerationStore.revalidate = next.revalidate - } + if (!staticGenerationStore.forceStatic || next.revalidate !== 0) { + staticGenerationStore.revalidate = next.revalidate } - if (hasNextConfig) delete init.next } - - return await doOriginalFetch(false, cacheReasonOverride).finally( - handleUnlock - ) - } catch (e) { - // @ts-ignore - throw new TypeError(e?.message, { cause: e?.cause }) + if (hasNextConfig) delete init.next } + + return await doOriginalFetch(false, cacheReasonOverride).finally( + handleUnlock + ) } ) } diff --git a/packages/next/src/server/lib/trace/tracer.ts b/packages/next/src/server/lib/trace/tracer.ts index 8abb21aba9ce4b..258717017d0836 100644 --- a/packages/next/src/server/lib/trace/tracer.ts +++ b/packages/next/src/server/lib/trace/tracer.ts @@ -284,13 +284,13 @@ class NextTracerImpl implements NextTracer { } const result = fn(span) - if (isPromise(result)) { - result - .then( - () => span.end(), - (err) => closeSpanWithError(span, err) - ) + return (result as Promise) + .catch((err) => { + closeSpanWithError(span, err) + throw err + }) + .then(() => span.end()) .finally(onCleanup) } else { span.end() From b00592025d64cb9f76125a9d39aa0dc1a4e28d2c Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 9 Jan 2024 21:53:07 +0100 Subject: [PATCH 13/20] fix --- examples/hello-world/app/page.tsx | 3 +-- packages/next/src/server/lib/patch-fetch.ts | 9 +++------ packages/next/src/server/lib/trace/tracer.ts | 20 ++++++++++++-------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/examples/hello-world/app/page.tsx b/examples/hello-world/app/page.tsx index 8a3e8303a4d776..6baa6ade86b9a9 100644 --- a/examples/hello-world/app/page.tsx +++ b/examples/hello-world/app/page.tsx @@ -1,4 +1,3 @@ -export default async function Page() { - await fetch('http://localhost:3000/xxxx') +export default function Page() { return

Hello, Next.js!

} diff --git a/packages/next/src/server/lib/patch-fetch.ts b/packages/next/src/server/lib/patch-fetch.ts index 214ca469a92860..7175c728cfbb3a 100644 --- a/packages/next/src/server/lib/patch-fetch.ts +++ b/packages/next/src/server/lib/patch-fetch.ts @@ -246,8 +246,7 @@ export function patchFetch({ isInternal || staticGenerationStore.isDraftMode ) { - return await originFetch(input, init) - // return tracedOriginalFetch(input, init) + return originFetch(input, init) } let revalidate: number | undefined | false = undefined @@ -497,7 +496,7 @@ export function patchFetch({ next: { ...init?.next, fetchType: 'origin', fetchIdx }, } - return await originFetch(input, clonedInit).then(async (res) => { + return originFetch(input, clonedInit).then(async (res) => { if (!isStale) { trackFetchMetric(staticGenerationStore, { start: fetchStart, @@ -680,9 +679,7 @@ export function patchFetch({ if (hasNextConfig) delete init.next } - return await doOriginalFetch(false, cacheReasonOverride).finally( - handleUnlock - ) + return doOriginalFetch(false, cacheReasonOverride).finally(handleUnlock) } ) } diff --git a/packages/next/src/server/lib/trace/tracer.ts b/packages/next/src/server/lib/trace/tracer.ts index 258717017d0836..d3385a82fc20eb 100644 --- a/packages/next/src/server/lib/trace/tracer.ts +++ b/packages/next/src/server/lib/trace/tracer.ts @@ -283,15 +283,19 @@ class NextTracerImpl implements NextTracer { return fn(span, (err?: Error) => closeSpanWithError(span, err)) } - const result = fn(span) + let result: T | Promise = fn(span) if (isPromise(result)) { - return (result as Promise) - .catch((err) => { - closeSpanWithError(span, err) - throw err - }) - .then(() => span.end()) - .finally(onCleanup) + result = result.catch((err) => { + closeSpanWithError(span, err) + onCleanup() + throw err + }) + + return result.then((res) => { + span.end() + onCleanup() + return res + }) } else { span.end() onCleanup() From adae15aabe0560d36fa5a59bf59c7f7e1719688e Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 9 Jan 2024 21:56:25 +0100 Subject: [PATCH 14/20] revert test changes --- packages/next/src/server/lib/trace/tracer.ts | 25 +++++++++++-------- .../acceptance-app/rsc-runtime-errors.test.ts | 2 +- test/e2e/app-dir/hello-world/app/page.tsx | 9 ++----- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/packages/next/src/server/lib/trace/tracer.ts b/packages/next/src/server/lib/trace/tracer.ts index d3385a82fc20eb..483a9e46f163f9 100644 --- a/packages/next/src/server/lib/trace/tracer.ts +++ b/packages/next/src/server/lib/trace/tracer.ts @@ -285,17 +285,20 @@ class NextTracerImpl implements NextTracer { let result: T | Promise = fn(span) if (isPromise(result)) { - result = result.catch((err) => { - closeSpanWithError(span, err) - onCleanup() - throw err - }) - - return result.then((res) => { - span.end() - onCleanup() - return res - }) + // If there's error make sure it throws + return result + + .then((res) => { + span.end() + // onCleanup() + return res + }) + .catch((err) => { + closeSpanWithError(span, err) + // onCleanup() + throw err + }) + .finally(onCleanup) } else { span.end() onCleanup() diff --git a/test/development/acceptance-app/rsc-runtime-errors.test.ts b/test/development/acceptance-app/rsc-runtime-errors.test.ts index 0d69a3f5c21608..7c2728f223db0f 100644 --- a/test/development/acceptance-app/rsc-runtime-errors.test.ts +++ b/test/development/acceptance-app/rsc-runtime-errors.test.ts @@ -118,7 +118,7 @@ createNextDescribe( const source = await getRedboxSource(browser) // Can show the original source code expect(source).toContain('app/server/page.js') - expect(source).toContain(`> 2 | await fetch('http://locahost:3000/xxxx')`) + expect(source).toContain(`await fetch('http://locahost:3000/xxxx')`) }) } ) diff --git a/test/e2e/app-dir/hello-world/app/page.tsx b/test/e2e/app-dir/hello-world/app/page.tsx index 3c90a360dbcdb4..6baa6ade86b9a9 100644 --- a/test/e2e/app-dir/hello-world/app/page.tsx +++ b/test/e2e/app-dir/hello-world/app/page.tsx @@ -1,8 +1,3 @@ -export default async function Page() { - try { - await fetch('http://locahost:3000/xxxx') - } catch (e) { - throw e - } - return 'page' +export default function Page() { + return

Hello, Next.js!

} From e524a64253df66a5766264b449c4506e59b5e64a Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 9 Jan 2024 22:03:29 +0100 Subject: [PATCH 15/20] revert test changes --- packages/next/src/server/lib/patch-fetch.ts | 7 +++---- packages/next/src/server/lib/trace/tracer.ts | 5 +---- test/e2e/app-dir/hello-world/app/page.tsx | 2 +- test/turbopack-tests-manifest.json | 4 +--- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/next/src/server/lib/patch-fetch.ts b/packages/next/src/server/lib/patch-fetch.ts index 7175c728cfbb3a..1cf98e9ba33af8 100644 --- a/packages/next/src/server/lib/patch-fetch.ts +++ b/packages/next/src/server/lib/patch-fetch.ts @@ -191,10 +191,10 @@ export function patchFetch({ const { DynamicServerError } = serverHooks const originFetch: typeof fetch = (globalThis as any)._nextOriginalFetch - globalThis.fetch = async function ( + globalThis.fetch = async ( input: RequestInfo | URL, init: RequestInit | undefined - ) { + ) => { let url: URL | undefined try { url = new URL(input instanceof Request ? input.url : input) @@ -572,14 +572,13 @@ export function patchFetch({ }) if (entry) { - // await handleUnlock() + await handleUnlock() } else { // in dev, incremental cache response will be null in case the browser adds `cache-control: no-cache` in the request headers cacheReasonOverride = 'cache-control: no-cache (hard refresh)' } if (entry?.value && entry.value.kind === 'FETCH') { - console.log('cache promise') // when stale and is revalidating we wait for fresh data // so the revalidated entry has the updated data if (!(staticGenerationStore.isRevalidate && entry.isStale)) { diff --git a/packages/next/src/server/lib/trace/tracer.ts b/packages/next/src/server/lib/trace/tracer.ts index 483a9e46f163f9..09bbe1d8bbf1c8 100644 --- a/packages/next/src/server/lib/trace/tracer.ts +++ b/packages/next/src/server/lib/trace/tracer.ts @@ -283,19 +283,16 @@ class NextTracerImpl implements NextTracer { return fn(span, (err?: Error) => closeSpanWithError(span, err)) } - let result: T | Promise = fn(span) + const result = fn(span) if (isPromise(result)) { // If there's error make sure it throws return result - .then((res) => { span.end() - // onCleanup() return res }) .catch((err) => { closeSpanWithError(span, err) - // onCleanup() throw err }) .finally(onCleanup) diff --git a/test/e2e/app-dir/hello-world/app/page.tsx b/test/e2e/app-dir/hello-world/app/page.tsx index 6baa6ade86b9a9..ff7159d9149fee 100644 --- a/test/e2e/app-dir/hello-world/app/page.tsx +++ b/test/e2e/app-dir/hello-world/app/page.tsx @@ -1,3 +1,3 @@ export default function Page() { - return

Hello, Next.js!

+ return

hello world

} diff --git a/test/turbopack-tests-manifest.json b/test/turbopack-tests-manifest.json index 591b133af0f54c..c8792f8c82bb3e 100644 --- a/test/turbopack-tests-manifest.json +++ b/test/turbopack-tests-manifest.json @@ -1185,9 +1185,7 @@ "Error overlay - RSC runtime errors should show runtime errors if invalid client API from node_modules is executed", "Error overlay - RSC runtime errors should show runtime errors if invalid server API from node_modules is executed" ], - "failed": [ - "Error overlay - RSC runtime errors should show the userland code error trace when fetch failed error occurred" - ], + "failed": [], "pending": [], "flakey": [], "runtimeError": false From accea93c0bf151928f0d907e109654a7b5f1fecc Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 9 Jan 2024 22:20:24 +0100 Subject: [PATCH 16/20] return from tracer --- packages/next/src/server/lib/trace/tracer.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/next/src/server/lib/trace/tracer.ts b/packages/next/src/server/lib/trace/tracer.ts index 09bbe1d8bbf1c8..40b38df3e7ca2e 100644 --- a/packages/next/src/server/lib/trace/tracer.ts +++ b/packages/next/src/server/lib/trace/tracer.ts @@ -289,6 +289,8 @@ class NextTracerImpl implements NextTracer { return result .then((res) => { span.end() + // Need to pass down the promise result, + // it could be react stream response with error { error, stream } return res }) .catch((err) => { From ec8df5acd234b978cd774cb5673af6b4bf0f6b03 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 9 Jan 2024 22:34:07 +0100 Subject: [PATCH 17/20] fix test --- .../server-component-compiler-errors-in-pages.test.ts | 3 ++- test/turbopack-tests-manifest.json | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts b/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts index 126cbebde94869..d99b883a6c4454 100644 --- a/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts +++ b/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts @@ -1,6 +1,6 @@ /* eslint-env jest */ import { nextTestSetup } from 'e2e-utils' -import { check } from 'next-test-utils' +import { check, waitFor } from 'next-test-utils' import { sandbox } from 'development-sandbox' import { outdent } from 'outdent' @@ -101,6 +101,7 @@ describe('Error Overlay for server components compiler errors in pages', () => { expect(next.normalizeTestDirContent(await session.getRedboxSource())) .toMatchInlineSnapshot(` "./components/Comp.js + ./pages/index.js Error: x You're importing a component that needs server-only. That only works in a Server Component which is not supported in the pages/ directory. Read more: https://nextjs.org/docs/getting-started/ | react-essentials#server-components diff --git a/test/turbopack-tests-manifest.json b/test/turbopack-tests-manifest.json index c8792f8c82bb3e..591b133af0f54c 100644 --- a/test/turbopack-tests-manifest.json +++ b/test/turbopack-tests-manifest.json @@ -1185,7 +1185,9 @@ "Error overlay - RSC runtime errors should show runtime errors if invalid client API from node_modules is executed", "Error overlay - RSC runtime errors should show runtime errors if invalid server API from node_modules is executed" ], - "failed": [], + "failed": [ + "Error overlay - RSC runtime errors should show the userland code error trace when fetch failed error occurred" + ], "pending": [], "flakey": [], "runtimeError": false From 796649a9b6401e86b7afa8c31b4f5d1aea879167 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 9 Jan 2024 22:43:57 +0100 Subject: [PATCH 18/20] fix test --- .../server-component-compiler-errors-in-pages.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts b/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts index d99b883a6c4454..ab7be8cbfc24c3 100644 --- a/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts +++ b/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts @@ -1,6 +1,6 @@ /* eslint-env jest */ import { nextTestSetup } from 'e2e-utils' -import { check, waitFor } from 'next-test-utils' +import { check } from 'next-test-utils' import { sandbox } from 'development-sandbox' import { outdent } from 'outdent' @@ -101,7 +101,6 @@ describe('Error Overlay for server components compiler errors in pages', () => { expect(next.normalizeTestDirContent(await session.getRedboxSource())) .toMatchInlineSnapshot(` "./components/Comp.js - ./pages/index.js Error: x You're importing a component that needs server-only. That only works in a Server Component which is not supported in the pages/ directory. Read more: https://nextjs.org/docs/getting-started/ | react-essentials#server-components @@ -117,7 +116,6 @@ describe('Error Overlay for server components compiler errors in pages', () => { Import trace for requested module: ./components/Comp.js - ./pages/index.js" `) await cleanup() From e43f2ebd5803a57cc7455f2e96bf7eee97347401 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 9 Jan 2024 22:57:32 +0100 Subject: [PATCH 19/20] fix snapshot --- .../acceptance/server-component-compiler-errors-in-pages.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts b/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts index ab7be8cbfc24c3..a123d85f33694f 100644 --- a/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts +++ b/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts @@ -101,6 +101,7 @@ describe('Error Overlay for server components compiler errors in pages', () => { expect(next.normalizeTestDirContent(await session.getRedboxSource())) .toMatchInlineSnapshot(` "./components/Comp.js + ./pages/index.js Error: x You're importing a component that needs server-only. That only works in a Server Component which is not supported in the pages/ directory. Read more: https://nextjs.org/docs/getting-started/ | react-essentials#server-components From eceb4107177f9602704fa74737347e6b25b69cc5 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 9 Jan 2024 23:07:17 +0100 Subject: [PATCH 20/20] revert --- .../server-component-compiler-errors-in-pages.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts b/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts index a123d85f33694f..126cbebde94869 100644 --- a/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts +++ b/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts @@ -101,7 +101,6 @@ describe('Error Overlay for server components compiler errors in pages', () => { expect(next.normalizeTestDirContent(await session.getRedboxSource())) .toMatchInlineSnapshot(` "./components/Comp.js - ./pages/index.js Error: x You're importing a component that needs server-only. That only works in a Server Component which is not supported in the pages/ directory. Read more: https://nextjs.org/docs/getting-started/ | react-essentials#server-components @@ -117,6 +116,7 @@ describe('Error Overlay for server components compiler errors in pages', () => { Import trace for requested module: ./components/Comp.js + ./pages/index.js" `) await cleanup()