Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 26 additions & 32 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@ import {
} from '../resume-data-cache/resume-data-cache'
import type { MetadataErrorType } from '../../lib/metadata/resolve-metadata'
import isError from '../../lib/is-error'
import { isUseCacheTimeoutError } from '../use-cache/use-cache-errors'
import { createServerInsertedMetadata } from './metadata-insertion/create-server-inserted-metadata'
import { getPreviouslyRevalidatedTags } from '../server-utils'
import { executeRevalidates } from '../revalidation-utils'
Expand Down Expand Up @@ -632,7 +631,6 @@ async function generateDynamicFlightRenderResult(
ctx,
false,
ctx.clientReferenceManifest,
ctx.workStore.route,
requestStore
)
}
Expand Down Expand Up @@ -1390,7 +1388,6 @@ async function renderToHTMLOrFlightImpl(
res,
ctx,
metadata,
workStore,
loaderTree
)

Expand All @@ -1410,8 +1407,8 @@ async function renderToHTMLOrFlightImpl(

// If we encountered any unexpected errors during build we fail the
// prerendering phase and the build.
if (workStore.invalidUsageError) {
throw workStore.invalidUsageError
if (workStore.invalidDynamicUsageError) {
throw workStore.invalidDynamicUsageError
}
if (response.digestErrorsMap.size) {
const buildFailingError = response.digestErrorsMap.values().next().value
Expand Down Expand Up @@ -1564,7 +1561,6 @@ async function renderToHTMLOrFlightImpl(
req,
res,
ctx,
workStore,
notFoundLoaderTree,
formState,
postponedState
Expand All @@ -1591,14 +1587,13 @@ async function renderToHTMLOrFlightImpl(
req,
res,
ctx,
workStore,
loaderTree,
formState,
postponedState
)

if (workStore.invalidUsageError) {
throw workStore.invalidUsageError
if (workStore.invalidDynamicUsageError) {
throw workStore.invalidDynamicUsageError
}

// If we have pending revalidates, wait until they are all resolved.
Expand Down Expand Up @@ -1727,7 +1722,6 @@ async function renderToStream(
req: BaseNextRequest,
res: BaseNextResponse,
ctx: AppRenderContext,
workStore: WorkStore,
tree: LoaderTree,
formState: any,
postponedState: PostponedState | null
Expand Down Expand Up @@ -1873,7 +1867,6 @@ async function renderToStream(
ctx,
res.statusCode === 404,
clientReferenceManifest,
workStore.route,
requestStore
)

Expand Down Expand Up @@ -2220,10 +2213,9 @@ async function spawnDynamicValidationInDev(
ctx: AppRenderContext,
isNotFound: boolean,
clientReferenceManifest: NonNullable<RenderOpts['clientReferenceManifest']>,
route: string,
requestStore: RequestStore
): Promise<void> {
const { componentMod: ComponentMod, implicitTags } = ctx
const { componentMod: ComponentMod, implicitTags, workStore } = ctx
const rootParams = getRootParams(
ComponentMod.tree,
ctx.getDynamicParamFromSegment
Expand Down Expand Up @@ -2317,7 +2309,7 @@ async function spawnDynamicValidationInDev(
process.env.NEXT_DEBUG_BUILD ||
process.env.__NEXT_VERBOSE_LOGGING
) {
printDebugThrownValueForProspectiveRender(err, route)
printDebugThrownValueForProspectiveRender(err, workStore.route)
}
},
signal: initialServerRenderController.signal,
Expand All @@ -2335,7 +2327,7 @@ async function spawnDynamicValidationInDev(
) {
// We don't normally log these errors because we are going to retry anyway but
// it can be useful for debugging Next.js itself to get visibility here when needed
printDebugThrownValueForProspectiveRender(err, route)
printDebugThrownValueForProspectiveRender(err, workStore.route)
}
}

Expand Down Expand Up @@ -2375,7 +2367,7 @@ async function spawnDynamicValidationInDev(
) {
// We don't normally log these errors because we are going to retry anyway but
// it can be useful for debugging Next.js itself to get visibility here when needed
printDebugThrownValueForProspectiveRender(err, route)
printDebugThrownValueForProspectiveRender(err, workStore.route)
}
},
}
Expand All @@ -2387,7 +2379,7 @@ async function spawnDynamicValidationInDev(
// We're going to retry to so we normally would suppress this error but
// when verbose logging is on we print it
if (process.env.__NEXT_VERBOSE_LOGGING) {
printDebugThrownValueForProspectiveRender(err, route)
printDebugThrownValueForProspectiveRender(err, workStore.route)
}
}
})
Expand Down Expand Up @@ -2467,10 +2459,6 @@ async function spawnDynamicValidationInDev(
clientReferenceManifest.clientModules,
{
onError: (err) => {
if (isUseCacheTimeoutError(err)) {
return err.digest
}

if (
finalServerController.signal.aborted &&
isPrerenderInterruptedError(err)
Expand Down Expand Up @@ -2511,12 +2499,6 @@ async function spawnDynamicValidationInDev(
{
signal: finalClientController.signal,
onError: (err, errorInfo) => {
if (isUseCacheTimeoutError(err)) {
dynamicValidation.dynamicErrors.push(err)

return
}

if (
isPrerenderInterruptedError(err) ||
finalClientController.signal.aborted
Expand All @@ -2531,7 +2513,7 @@ async function spawnDynamicValidationInDev(
const componentStack = errorInfo.componentStack
if (typeof componentStack === 'string') {
trackAllowedDynamicAccess(
route,
workStore.route,
componentStack,
dynamicValidation
)
Expand Down Expand Up @@ -2572,7 +2554,7 @@ async function spawnDynamicValidationInDev(
// track any dynamic access that occurs above the suspense boundary because
// we'll do so in the route shell.
throwIfDisallowedDynamic(
route,
workStore,
preludeIsEmpty,
dynamicValidation,
serverDynamicTracking,
Expand Down Expand Up @@ -2611,7 +2593,6 @@ async function prerenderToStream(
res: BaseNextResponse,
ctx: AppRenderContext,
metadata: AppPageRenderResultMetadata,
workStore: WorkStore,
tree: LoaderTree
): Promise<PrerenderToStreamResult> {
// When prerendering formState is always null. We still include it
Expand All @@ -2626,6 +2607,7 @@ async function prerenderToStream(
nonce,
pagePath,
renderOpts,
workStore,
} = ctx

const rootParams = getRootParams(tree, getDynamicParamFromSegment)
Expand Down Expand Up @@ -2840,6 +2822,12 @@ async function prerenderToStream(
initialServerRenderController.abort()
initialServerPrerenderController.abort()

// We don't need to continue the prerender process if we already
// detected invalid dynamic usage in the initial prerender phase.
if (workStore.invalidDynamicUsageError) {
throw workStore.invalidDynamicUsageError
}

let initialServerResult
try {
initialServerResult = await createReactServerPrerenderResult(
Expand Down Expand Up @@ -3106,7 +3094,7 @@ async function prerenderToStream(
// we'll do so in the route shell.
if (!ctx.renderOpts.doNotThrowOnEmptyStaticShell) {
throwIfDisallowedDynamic(
workStore.route,
workStore,
preludeIsEmpty,
dynamicValidation,
serverDynamicTracking,
Expand Down Expand Up @@ -3428,6 +3416,12 @@ async function prerenderToStream(
initialServerRenderController.abort()
initialServerPrerenderController.abort()

// We don't need to continue the prerender process if we already
// detected invalid dynamic usage in the initial prerender phase.
if (workStore.invalidDynamicUsageError) {
throw workStore.invalidDynamicUsageError
}

// We've now filled caches and triggered any inadvertant sync bailouts
// due to lazy module initialization. We can restart our render to capture results

Expand Down Expand Up @@ -3591,7 +3585,7 @@ async function prerenderToStream(
if (!ctx.renderOpts.doNotThrowOnEmptyStaticShell) {
// We don't have a shell because the root errored when we aborted.
throwIfDisallowedDynamic(
workStore.route,
workStore,
preludeIsEmpty,
dynamicValidation,
serverDynamicTracking,
Expand Down
11 changes: 8 additions & 3 deletions packages/next/src/server/app-render/dynamic-rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,12 +651,17 @@ function createErrorWithComponentStack(
}

export function throwIfDisallowedDynamic(
route: string,
workStore: WorkStore,
hasEmptyShell: boolean,
dynamicValidation: DynamicValidationState,
serverDynamic: DynamicTrackingState,
clientDynamic: DynamicTrackingState
): void {
if (workStore.invalidDynamicUsageError) {
console.error(workStore.invalidDynamicUsageError)
throw new StaticGenBailoutError()
}

if (hasEmptyShell) {
if (dynamicValidation.hasSuspenseAboveBody) {
// This route has opted into allowing fully dynamic rendering
Expand Down Expand Up @@ -698,7 +703,7 @@ export function throwIfDisallowedDynamic(
// to indicate your are ok with fully dynamic rendering.
if (dynamicValidation.hasDynamicViewport) {
console.error(
`Route "${route}" has a \`generateViewport\` that depends on Request data (\`cookies()\`, etc...) or uncached external data (\`fetch(...)\`, etc...) without explicitly allowing fully dynamic rendering. See more info here: https://nextjs.org/docs/messages/next-prerender-dynamic-viewport`
`Route "${workStore.route}" has a \`generateViewport\` that depends on Request data (\`cookies()\`, etc...) or uncached external data (\`fetch(...)\`, etc...) without explicitly allowing fully dynamic rendering. See more info here: https://nextjs.org/docs/messages/next-prerender-dynamic-viewport`
)
throw new StaticGenBailoutError()
}
Expand All @@ -708,7 +713,7 @@ export function throwIfDisallowedDynamic(
dynamicValidation.hasDynamicMetadata
) {
console.error(
`Route "${route}" has a \`generateMetadata\` that depends on Request data (\`cookies()\`, etc...) or uncached external data (\`fetch(...)\`, etc...) when the rest of the route does not. See more info here: https://nextjs.org/docs/messages/next-prerender-dynamic-metadata`
`Route "${workStore.route}" has a \`generateMetadata\` that depends on Request data (\`cookies()\`, etc...) or uncached external data (\`fetch(...)\`, etc...) when the rest of the route does not. See more info here: https://nextjs.org/docs/messages/next-prerender-dynamic-metadata`
)
throw new StaticGenBailoutError()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,13 @@ export interface WorkStore {
dynamicUsageStack?: string

/**
* Invalid usage errors might be caught in userland. We attach them to the
* work store to ensure we can still fail the build or dev render.
* Invalid dynamic usage errors might be caught in userland. We attach them to
* the work store to ensure we can still fail the build, or show en error in
* dev mode.
*/
// TODO: Collect an array of errors, and throw as AggregateError when
// `serializeError` and the Dev Overlay support it.
invalidUsageError?: Error
invalidDynamicUsageError?: Error

nextFetchId?: number
pathWasRevalidated?: boolean
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/server/request/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function throwForSearchParamsAccessInUseCache(
)

Error.captureStackTrace(error, constructorOpt)
workStore.invalidUsageError ??= error
workStore.invalidDynamicUsageError ??= error

throw error
}
Expand Down
21 changes: 5 additions & 16 deletions packages/next/src/server/use-cache/use-cache-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,8 @@ async function collectResult(
let idx = 0
const bufferStream = new ReadableStream({
pull(controller) {
if (workStore.invalidUsageError) {
controller.error(workStore.invalidUsageError)
if (workStore.invalidDynamicUsageError) {
controller.error(workStore.invalidDynamicUsageError)
} else if (idx < buffer.length) {
controller.enqueue(buffer[idx++])
} else if (errors.length > 0) {
Expand Down Expand Up @@ -365,7 +365,6 @@ async function generateCacheEntryImpl(
const resultPromise = createLazyResult(() => fn.apply(null, args))

let errors: Array<unknown> = []
let timeoutErrorHandled = false

// In the "Cache" environment, we only need to make sure that the error
// digests are handled correctly. Error formatting and reporting is not
Expand All @@ -385,13 +384,6 @@ async function generateCacheEntryImpl(
console.error(error)
}

if (error === timeoutError) {
timeoutErrorHandled = true
// The timeout error already aborted the whole stream. We don't need
// to also push this error into the `errors` array.
return timeoutError.digest
}

errors.push(error)
}

Expand All @@ -404,6 +396,7 @@ async function generateCacheEntryImpl(
// Otherwise we assume you stalled on hanging input and de-opt. This needs
// to be lower than just the general timeout of 60 seconds.
const timer = setTimeout(() => {
workStore.invalidDynamicUsageError = timeoutError
timeoutAbortController.abort(timeoutError)
}, 50000)

Expand All @@ -422,7 +415,7 @@ async function generateCacheEntryImpl(
signal: abortSignal,
temporaryReferences,
onError(error) {
if (renderSignal.aborted && renderSignal.reason === error) {
if (abortSignal.aborted && abortSignal.reason === error) {
return undefined
}

Expand All @@ -433,11 +426,7 @@ async function generateCacheEntryImpl(

clearTimeout(timer)

if (timeoutAbortController.signal.aborted && !timeoutErrorHandled) {
// When halting is enabled, the prerender will not call `onError` when
// it's aborted with the timeout abort signal, and hanging promises will
// also not be rejected. In this case, we're creating an erroring stream
// here, to ensure that the error is propagated to the server environment.
if (timeoutAbortController.signal.aborted) {
stream = new ReadableStream({
start(controller) {
controller.error(timeoutError)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use cache'

export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params

return <p>slug: {slug}</p>
}

// If generateStaticParams would be used here to define at least one set of
// complete params, we would not yield a timeout error.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
async function getSearchParam({
searchParams,
}: {
searchParams: Promise<{ n: string }>
}): Promise<string> {
'use cache'

return (await searchParams).n
}

export default async function Page({
searchParams,
}: {
searchParams: Promise<{ n: string }>
}) {
let searchParam: string | undefined

try {
searchParam = await getSearchParam({ searchParams })
} catch {
// Ignore not having access to searchParams. This is still an invalid
// dynamic access though that we need to detect.
}

return (
<p>{searchParam ? `search param: ${searchParam}` : 'no search param'}</p>
)
}
Loading
Loading