diff --git a/packages/next/src/compiled/react-server-dom-turbopack-experimental/cjs/react-server-dom-turbopack-server.edge.development.js b/packages/next/src/compiled/react-server-dom-turbopack-experimental/cjs/react-server-dom-turbopack-server.edge.development.js index f0200af4c6eb..48cbb14ffcf1 100644 --- a/packages/next/src/compiled/react-server-dom-turbopack-experimental/cjs/react-server-dom-turbopack-server.edge.development.js +++ b/packages/next/src/compiled/react-server-dom-turbopack-experimental/cjs/react-server-dom-turbopack-server.edge.development.js @@ -3609,10 +3609,8 @@ } function markOperationEndTime(request, task, timestamp) { (request.status === ABORTING && timestamp > request.abortTime) || - (timestamp > task.time - ? (emitTimingChunk(request, task.id, timestamp), - (task.time = timestamp)) - : emitTimingChunk(request, task.id, task.time)); + (emitTimingChunk(request, task.id, timestamp), + timestamp > task.time && (task.time = timestamp)); } function emitChunk(request, task, value) { var id = task.id; diff --git a/packages/next/src/compiled/react-server-dom-turbopack-experimental/cjs/react-server-dom-turbopack-server.node.development.js b/packages/next/src/compiled/react-server-dom-turbopack-experimental/cjs/react-server-dom-turbopack-server.node.development.js index 99d7ca071fe1..8d1a2e4d3b18 100644 --- a/packages/next/src/compiled/react-server-dom-turbopack-experimental/cjs/react-server-dom-turbopack-server.node.development.js +++ b/packages/next/src/compiled/react-server-dom-turbopack-experimental/cjs/react-server-dom-turbopack-server.node.development.js @@ -3995,10 +3995,8 @@ } function markOperationEndTime(request, task, timestamp) { (request.status === ABORTING && timestamp > request.abortTime) || - (timestamp > task.time - ? (emitTimingChunk(request, task.id, timestamp), - (task.time = timestamp)) - : emitTimingChunk(request, task.id, task.time)); + (emitTimingChunk(request, task.id, timestamp), + timestamp > task.time && (task.time = timestamp)); } function emitChunk(request, task, value) { var id = task.id; diff --git a/packages/next/src/compiled/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.edge.development.js b/packages/next/src/compiled/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.edge.development.js index 4675055a0b66..4c168f40ba59 100644 --- a/packages/next/src/compiled/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.edge.development.js +++ b/packages/next/src/compiled/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.edge.development.js @@ -3566,10 +3566,8 @@ } function markOperationEndTime(request, task, timestamp) { (request.status === ABORTING && timestamp > request.abortTime) || - (timestamp > task.time - ? (emitTimingChunk(request, task.id, timestamp), - (task.time = timestamp)) - : emitTimingChunk(request, task.id, task.time)); + (emitTimingChunk(request, task.id, timestamp), + timestamp > task.time && (task.time = timestamp)); } function emitChunk(request, task, value) { var id = task.id; diff --git a/packages/next/src/compiled/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.node.development.js b/packages/next/src/compiled/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.node.development.js index 560de270d9b5..80ba0f5926ab 100644 --- a/packages/next/src/compiled/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.node.development.js +++ b/packages/next/src/compiled/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.node.development.js @@ -3952,10 +3952,8 @@ } function markOperationEndTime(request, task, timestamp) { (request.status === ABORTING && timestamp > request.abortTime) || - (timestamp > task.time - ? (emitTimingChunk(request, task.id, timestamp), - (task.time = timestamp)) - : emitTimingChunk(request, task.id, task.time)); + (emitTimingChunk(request, task.id, timestamp), + timestamp > task.time && (task.time = timestamp)); } function emitChunk(request, task, value) { var id = task.id; diff --git a/packages/next/src/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-server.edge.development.js b/packages/next/src/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-server.edge.development.js index e56d7c66d5d2..774cefadfdf8 100644 --- a/packages/next/src/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-server.edge.development.js +++ b/packages/next/src/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-server.edge.development.js @@ -3609,10 +3609,8 @@ } function markOperationEndTime(request, task, timestamp) { (request.status === ABORTING && timestamp > request.abortTime) || - (timestamp > task.time - ? (emitTimingChunk(request, task.id, timestamp), - (task.time = timestamp)) - : emitTimingChunk(request, task.id, task.time)); + (emitTimingChunk(request, task.id, timestamp), + timestamp > task.time && (task.time = timestamp)); } function emitChunk(request, task, value) { var id = task.id; diff --git a/packages/next/src/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-server.node.development.js b/packages/next/src/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-server.node.development.js index e3a6a8994220..6b54e8f8cd69 100644 --- a/packages/next/src/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-server.node.development.js +++ b/packages/next/src/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-server.node.development.js @@ -3995,10 +3995,8 @@ } function markOperationEndTime(request, task, timestamp) { (request.status === ABORTING && timestamp > request.abortTime) || - (timestamp > task.time - ? (emitTimingChunk(request, task.id, timestamp), - (task.time = timestamp)) - : emitTimingChunk(request, task.id, task.time)); + (emitTimingChunk(request, task.id, timestamp), + timestamp > task.time && (task.time = timestamp)); } function emitChunk(request, task, value) { var id = task.id; diff --git a/packages/next/src/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.development.js b/packages/next/src/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.development.js index aff3f7390ed7..7fef449745c3 100644 --- a/packages/next/src/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.development.js +++ b/packages/next/src/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.development.js @@ -3566,10 +3566,8 @@ } function markOperationEndTime(request, task, timestamp) { (request.status === ABORTING && timestamp > request.abortTime) || - (timestamp > task.time - ? (emitTimingChunk(request, task.id, timestamp), - (task.time = timestamp)) - : emitTimingChunk(request, task.id, task.time)); + (emitTimingChunk(request, task.id, timestamp), + timestamp > task.time && (task.time = timestamp)); } function emitChunk(request, task, value) { var id = task.id; diff --git a/packages/next/src/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js b/packages/next/src/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js index f87d41f8c9d2..4c77d8f9a780 100644 --- a/packages/next/src/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js +++ b/packages/next/src/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js @@ -3952,10 +3952,8 @@ } function markOperationEndTime(request, task, timestamp) { (request.status === ABORTING && timestamp > request.abortTime) || - (timestamp > task.time - ? (emitTimingChunk(request, task.id, timestamp), - (task.time = timestamp)) - : emitTimingChunk(request, task.id, task.time)); + (emitTimingChunk(request, task.id, timestamp), + timestamp > task.time && (task.time = timestamp)); } function emitChunk(request, task, value) { var id = task.id; diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index e8ff2c8d44e9..fb2f12245596 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -290,6 +290,7 @@ import { ResponseCookies } from '../web/spec-extension/cookies' import { isInstantValidationError } from './instant-validation/instant-validation-error' import { createPromiseWithResolvers } from '../../shared/lib/promise-with-resolvers' import { RENDER_STAGES_BY_DATA_KIND } from '../dynamic-rendering-utils' +import type { StageEndTimes } from './instant-validation/instant-validation' export type GetDynamicParamFromSegment = ( // The LoaderTree to extract the dynamic param from @@ -5560,6 +5561,10 @@ async function spawnStaticShellValidationInDevImpl( accumulatedChunks, debugChunks, startTime, + { + [RenderStage.Static]: staticStageEndTime, + [RenderStage.Runtime]: runtimeStageEndTime, + }, rootParams, fallbackRouteParams, ctx, @@ -5920,6 +5925,7 @@ async function validateInstantConfigs( accumulatedChunks: AccumulatedStreamChunks, debugChunks: null | Array, startTime: number, + endTimes: StageEndTimes, rootParams: Params, fallbackRouteParams: OpaqueFallbackRouteParams | null, ctx: AppRenderContext, @@ -5956,11 +5962,7 @@ async function validateInstantConfigs( ? createNodeDebugChannel : createWebDebugChannel - const { - cache, - payload: initialRscPayload, - stageEndTimes, - } = await collectStagedSegmentData( + const { cache, payload: initialRscPayload } = await collectStagedSegmentData( ctx.componentMod, renderFlightStream, { @@ -6013,7 +6015,7 @@ async function validateInstantConfigs( extraChunksController.signal, boundaryState, clientReferenceManifest, - stageEndTimes, + endTimes, useRuntimeStageForPartialSegments ) @@ -6081,7 +6083,7 @@ async function validateInstantConfigs( [] = [] - /** Track when we advance stages so we can pass them as `endTime` later. */ - const stageEndTimes: StageEndTimes = { - [RenderStage.Static]: Infinity, - [RenderStage.Runtime]: Infinity, - } - const renderIntoCacheItem = async ( data: HeadData | SegmentData, cacheEntry: SegmentCacheItem @@ -370,16 +364,6 @@ export async function collectStagedSegmentData( ]) } - const advanceStage = ( - targetStage: Exclude - ) => { - const { currentStage } = controller - if (currentStage !== RenderStage.Dynamic) { - stageEndTimes[currentStage] = performance.now() + performance.timeOrigin - } - controller.advanceStage(targetStage) - } - await runInSequentialTasks( () => { { @@ -394,12 +378,12 @@ export async function collectStagedSegmentData( pendingTasks.push(renderIntoCacheItem(segmentData, segmentCacheItem)) } }, - () => advanceStage(RenderStage.Runtime), - () => advanceStage(RenderStage.Dynamic) + () => controller.advanceStage(RenderStage.Runtime), + () => controller.advanceStage(RenderStage.Dynamic) ) await Promise.all(pendingTasks) - return { cache, payload, stageEndTimes } + return { cache, payload } } /** @@ -894,6 +878,7 @@ export function discoverValidationDepths(loaderTree: LoaderTree): number[] { */ export type ValidationPayloadResult = { payload: InitialRSCPayload + endTime: number /** Whether errors from this payload could be ambiguous between runtime * API access (cookies, headers) and uncached IO (connection, fetch). * True when some segments used Static stage. False when all segments @@ -1343,8 +1328,13 @@ export async function createCombinedPayloadAtDepth( f: [[flightRouterState, seedData, head]], } + const combinedEndTime = hasRuntimeSegments + ? stageEndTimes[RenderStage.Runtime] + : stageEndTimes[RenderStage.Static] + return { payload, + endTime: combinedEndTime, hasAmbiguousErrors: hasStaticSegments, slotStacks, } diff --git a/packages/next/src/server/app-render/staged-rendering.ts b/packages/next/src/server/app-render/staged-rendering.ts index d3f24d3052c3..ff9c9dfe19ad 100644 --- a/packages/next/src/server/app-render/staged-rendering.ts +++ b/packages/next/src/server/app-render/staged-rendering.ts @@ -249,18 +249,17 @@ export class StagedRenderingController { return this.syncInterruptReason } + getStageEndTime(stage: Exclude) { + // Stage N ends when stage N+1 begins. + return this.triggers[getNextStage(stage)].triggeredAt ?? Infinity + } + getStaticStageEndTime() { - // The Static stage ends when the stage after it began. - return ( - this.triggers[getNextStage(RenderStage.Static)].triggeredAt ?? Infinity - ) + return this.getStageEndTime(RenderStage.Static) } getRuntimeStageEndTime() { - // The Runtime stage ended when the stage after it began. - return ( - this.triggers[getNextStage(RenderStage.Runtime)].triggeredAt ?? Infinity - ) + return this.getStageEndTime(RenderStage.Runtime) } private abandonRender() { diff --git a/test/e2e/app-dir/instant-validation/app/suspense-in-root/blocking-attribution/dynamic-then-dynamic/page.tsx b/test/e2e/app-dir/instant-validation/app/suspense-in-root/blocking-attribution/dynamic-then-dynamic/page.tsx new file mode 100644 index 000000000000..4897693ee502 --- /dev/null +++ b/test/e2e/app-dir/instant-validation/app/suspense-in-root/blocking-attribution/dynamic-then-dynamic/page.tsx @@ -0,0 +1,23 @@ +export const instant = { level: 'experimental-error' } + +export default async function Page() { + await loadOuter() + return ( +
+

+ This page awaits a couple blocking (dynamic) things in sequence. We + should point to the first one as the cause. +

+
+ ) +} + +async function loadOuter() { + await new Promise((resolve) => setTimeout(resolve)) // 1 (correct) + await loadInner() +} + +async function loadInner() { + await new Promise((resolve) => setTimeout(resolve)) // 2 (incorrect) + await new Promise((resolve) => setTimeout(resolve)) // 3 (incorrect) +} diff --git a/test/e2e/app-dir/instant-validation/app/suspense-in-root/blocking-attribution/runtime-in-static/[slug]/layout.tsx b/test/e2e/app-dir/instant-validation/app/suspense-in-root/blocking-attribution/runtime-in-static/[slug]/layout.tsx new file mode 100644 index 000000000000..a760ce9df53f --- /dev/null +++ b/test/e2e/app-dir/instant-validation/app/suspense-in-root/blocking-attribution/runtime-in-static/[slug]/layout.tsx @@ -0,0 +1,14 @@ +import { cookies } from 'next/headers' + +export const instant = { level: 'experimental-error' } + +export default async function Layout({ children, params }) { + await params // 1 (correct) + await cookies() // 2 (incorrect) + return ( +
+

This layout is blocked on params.

+ {children} +
+ ) +} diff --git a/test/e2e/app-dir/instant-validation/app/suspense-in-root/blocking-attribution/runtime-in-static/[slug]/page.tsx b/test/e2e/app-dir/instant-validation/app/suspense-in-root/blocking-attribution/runtime-in-static/[slug]/page.tsx new file mode 100644 index 000000000000..7c4f12dadb0b --- /dev/null +++ b/test/e2e/app-dir/instant-validation/app/suspense-in-root/blocking-attribution/runtime-in-static/[slug]/page.tsx @@ -0,0 +1,21 @@ +import { Instant } from 'next' +import { cookies } from 'next/headers' + +export const prefetch = 'allow-runtime' + +export const instant: Instant = { + level: 'experimental-error', + unstable_samples: [{ params: { slug: '123' } }], +} + +export default async function Page() { + await cookies() // Not blocking due to allow-runtime + return ( +
+

+ This page component is valid because it's allow-runtime, but its parent + layout is static and blocked on runtime data. +

+
+ ) +} diff --git a/test/e2e/app-dir/instant-validation/app/suspense-in-root/blocking-attribution/runtime-then-dynamic/page.tsx b/test/e2e/app-dir/instant-validation/app/suspense-in-root/blocking-attribution/runtime-then-dynamic/page.tsx new file mode 100644 index 000000000000..a8a9deea124b --- /dev/null +++ b/test/e2e/app-dir/instant-validation/app/suspense-in-root/blocking-attribution/runtime-then-dynamic/page.tsx @@ -0,0 +1,26 @@ +import { Instant } from 'next' +import { cookies } from 'next/headers' + +export const instant: Instant = { level: 'experimental-error' } + +export default async function Page() { + await loadOuter() + return ( +
+

+ This page awaits a couple blocking (runtime, then dynamic) things in + sequence. We should point to the first one as the cause. +

+
+ ) +} + +async function loadOuter() { + await cookies() // 1 (correct) + await loadInner() +} + +async function loadInner() { + await new Promise((resolve) => setTimeout(resolve)) // 2 (not correct, but expected) + await new Promise((resolve) => setTimeout(resolve)) // 3 (incorrect) +} diff --git a/test/e2e/app-dir/instant-validation/app/suspense-in-root/page.tsx b/test/e2e/app-dir/instant-validation/app/suspense-in-root/page.tsx index c210a548d550..31eeb7ac45f4 100644 --- a/test/e2e/app-dir/instant-validation/app/suspense-in-root/page.tsx +++ b/test/e2e/app-dir/instant-validation/app/suspense-in-root/page.tsx @@ -289,6 +289,19 @@ export default async function Page() { + +

Blocking await attribution

+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
) } diff --git a/test/e2e/app-dir/instant-validation/instant-validation.test.ts b/test/e2e/app-dir/instant-validation/instant-validation.test.ts index 822e3805a8ba..7c96c58a38e0 100644 --- a/test/e2e/app-dir/instant-validation/instant-validation.test.ts +++ b/test/e2e/app-dir/instant-validation/instant-validation.test.ts @@ -61,13 +61,19 @@ describe('instant validation', () => { return next.cliOutput.slice(currentCliOutputIndex) } - const prerender = async (pathname: string) => { + const prerender = async ( + pathname: string, + { debugPrerender = false }: { debugPrerender?: boolean } = {} + ) => { const args = [ '--experimental-build-mode', 'generate', '--debug-build-paths', `app${pathname}/page.tsx`, ] + if (debugPrerender) { + args.push('--debug-prerender') + } return await next.build({ args }) } @@ -2455,11 +2461,11 @@ describe('instant validation', () => { "description": "Next.js encountered uncached data in generateViewport().", "environmentLabel": "Server", "label": "Blocking Route", - "source": "app/suspense-in-root/head/invalid-dynamic-viewport-in-blocking-inside-static/page.tsx (6:23) @ Module.generateViewport - > 6 | export async function generateViewport(): Promise { - | ^", + "source": "app/suspense-in-root/head/invalid-dynamic-viewport-in-blocking-inside-static/page.tsx (7:19) @ Module.generateViewport + > 7 | await connection() + | ^", "stack": [ - "Module.generateViewport app/suspense-in-root/head/invalid-dynamic-viewport-in-blocking-inside-static/page.tsx (6:23)", + "Module.generateViewport app/suspense-in-root/head/invalid-dynamic-viewport-in-blocking-inside-static/page.tsx (7:19)", ], } `) @@ -3093,6 +3099,203 @@ describe('instant validation', () => { } }) + describe('validation failures point to the first blocking await', () => { + it('multiple dynamic awaits in sequence', async () => { + if (isNextDev) { + const browser = await navigateTo( + '/suspense-in-root/blocking-attribution/dynamic-then-dynamic' + ) + await expect(browser).toDisplayCollapsedRedbox(` + { + "cause": [ + { + "label": "Caused by: Instant Validation", + "source": "app/suspense-in-root/blocking-attribution/dynamic-then-dynamic/page.tsx (1:24) @ instant + > 1 | export const instant = { level: 'experimental-error' } + | ^", + "stack": [ + "instant app/suspense-in-root/blocking-attribution/dynamic-then-dynamic/page.tsx (1:24)", + "Set.forEach ", + ], + }, + ], + "code": "E1317", + "description": "Next.js encountered uncached data during a navigation.", + "environmentLabel": "Server", + "label": "Instant", + "source": "app/suspense-in-root/blocking-attribution/dynamic-then-dynamic/page.tsx (16:9) @ loadOuter + > 16 | await new Promise((resolve) => setTimeout(resolve)) // 1 (correct) + | ^", + "stack": [ + "loadOuter app/suspense-in-root/blocking-attribution/dynamic-then-dynamic/page.tsx (16:9)", + "Page app/suspense-in-root/blocking-attribution/dynamic-then-dynamic/page.tsx (4:9)", + ], + } + `) + } else { + const result = await prerender( + '/suspense-in-root/blocking-attribution/dynamic-then-dynamic' + ) + expect(extractBuildValidationError(result.cliOutput)) + .toMatchInlineSnapshot(` + "Error: Route "/suspense-in-root/blocking-attribution/dynamic-then-dynamic": Next.js encountered uncached data during prerendering or a navigation. + + \`fetch(...)\` or \`connection()\` accessed outside of \`\` prevents the route from being prerendered or the navigation from being instant, leading to a slower user experience. + + Ways to fix this: + - [stream] Provide a placeholder with \`\` around the data access + https://nextjs.org/docs/messages/blocking-prerender-dynamic#wrap-in-or-move-into-suspense + - [cache] Cache the data access with \`"use cache"\` + https://nextjs.org/docs/messages/blocking-prerender-dynamic#cache-the-component-or-data + - [block] Set \`export const instant = false\` to silence this warning and allow a blocking route + https://nextjs.org/docs/messages/blocking-prerender-dynamic#allow-blocking-route + at body () + at html () + at a () + Build-time instant validation failed for route "/suspense-in-root/blocking-attribution/dynamic-then-dynamic". + To get a more detailed stack trace and pinpoint the issue, try one of the following: + - Start the app in development mode by running \`next dev\`, then open "/suspense-in-root/blocking-attribution/dynamic-then-dynamic" in your browser to investigate the error. + - Rerun the production build with \`next build --debug-prerender\` to generate better stack traces. + Stopping prerender due to instant validation errors." + `) + expect(result.exitCode).toBe(1) + } + }) + + it('runtime data then dynamic data', async () => { + // TODO(instant-validation): Arguably, we should point to `await cookies()` first, + // but due to our the discriminated error message strategy we favor holes that are still + // present in the runtime retry render. This can be confusing if we have runtime and then dynamic data + // in the same place. At least we can assert that we're pointing to the first await that was blocking in the runtime stage. + if (isNextDev) { + const browser = await navigateTo( + '/suspense-in-root/blocking-attribution/runtime-then-dynamic' + ) + await expect(browser).toDisplayCollapsedRedbox(` + { + "cause": [ + { + "label": "Caused by: Instant Validation", + "source": "app/suspense-in-root/blocking-attribution/runtime-then-dynamic/page.tsx (4:33) @ instant + > 4 | export const instant: Instant = { level: 'experimental-error' } + | ^", + "stack": [ + "instant app/suspense-in-root/blocking-attribution/runtime-then-dynamic/page.tsx (4:33)", + "Set.forEach ", + ], + }, + ], + "code": "E1317", + "description": "Next.js encountered uncached data during a navigation.", + "environmentLabel": "Server", + "label": "Instant", + "source": "app/suspense-in-root/blocking-attribution/runtime-then-dynamic/page.tsx (24:9) @ loadInner + > 24 | await new Promise((resolve) => setTimeout(resolve)) // 2 (not correct, but expected) + | ^", + "stack": [ + "loadInner app/suspense-in-root/blocking-attribution/runtime-then-dynamic/page.tsx (24:9)", + "loadOuter app/suspense-in-root/blocking-attribution/runtime-then-dynamic/page.tsx (20:9)", + "Page app/suspense-in-root/blocking-attribution/runtime-then-dynamic/page.tsx (7:3)", + ], + } + `) + } else { + const result = await prerender( + '/suspense-in-root/blocking-attribution/runtime-then-dynamic' + ) + expect(extractBuildValidationError(result.cliOutput)) + .toMatchInlineSnapshot(` + "Error: Route "/suspense-in-root/blocking-attribution/runtime-then-dynamic": Next.js encountered uncached data during prerendering or a navigation. + + \`fetch(...)\` or \`connection()\` accessed outside of \`\` prevents the route from being prerendered or the navigation from being instant, leading to a slower user experience. + + Ways to fix this: + - [stream] Provide a placeholder with \`\` around the data access + https://nextjs.org/docs/messages/blocking-prerender-dynamic#wrap-in-or-move-into-suspense + - [cache] Cache the data access with \`"use cache"\` + https://nextjs.org/docs/messages/blocking-prerender-dynamic#cache-the-component-or-data + - [block] Set \`export const instant = false\` to silence this warning and allow a blocking route + https://nextjs.org/docs/messages/blocking-prerender-dynamic#allow-blocking-route + at body () + at html () + at a () + Build-time instant validation failed for route "/suspense-in-root/blocking-attribution/runtime-then-dynamic". + To get a more detailed stack trace and pinpoint the issue, try one of the following: + - Start the app in development mode by running \`next dev\`, then open "/suspense-in-root/blocking-attribution/runtime-then-dynamic" in your browser to investigate the error. + - Rerun the production build with \`next build --debug-prerender\` to generate better stack traces. + Stopping prerender due to instant validation errors." + `) + expect(result.exitCode).toBe(1) + } + }) + + it('(failing) runtime data in static segment when a runtime prefetchable segment is also present', async () => { + // TODO(instant-validation): This test is here to show a problem with our implementation. + // If an 'allow-runtime' segment is in the tree, instant-validation.tsx has to use the runtime + // stage for the `endTime` (which cuts off debug info and prevents us from pointing beyond the first blocking await). + // `endTime` is ultimately shared across the whole render, which means that we'll show "too much" for the static segment + // and end up pointing to the second await instead of the first one. + if (isNextDev) { + const browser = await navigateTo( + '/suspense-in-root/blocking-attribution/runtime-in-static/123' + ) + await expect(browser).toDisplayCollapsedRedbox(` + { + "cause": [ + { + "label": "Caused by: Instant Validation", + "source": "app/suspense-in-root/blocking-attribution/runtime-in-static/[slug]/layout.tsx (3:24) @ instant + > 3 | export const instant = { level: 'experimental-error' } + | ^", + "stack": [ + "instant app/suspense-in-root/blocking-attribution/runtime-in-static/[slug]/layout.tsx (3:24)", + "Set.forEach ", + ], + }, + ], + "code": "E1319", + "description": "Next.js encountered runtime data during a navigation.", + "environmentLabel": "Server", + "label": "Instant", + "source": "app/suspense-in-root/blocking-attribution/runtime-in-static/[slug]/layout.tsx (7:16) @ Layout + > 7 | await cookies() // 2 (incorrect) + | ^", + "stack": [ + "Layout app/suspense-in-root/blocking-attribution/runtime-in-static/[slug]/layout.tsx (7:16)", + ], + } + `) + } else { + const result = await prerender( + '/suspense-in-root/blocking-attribution/runtime-in-static/[slug]' + ) + expect(extractBuildValidationError(result.cliOutput)) + .toMatchInlineSnapshot(` + "Error: Route "/suspense-in-root/blocking-attribution/runtime-in-static/[slug]": Next.js encountered runtime data during prerendering or a navigation. + + \`cookies()\`, \`headers()\`, \`params\`, or \`searchParams\` accessed outside of \`\` prevents the route from being prerendered or the navigation from being instant, leading to a slower user experience. + + Ways to fix this: + - [stream] Provide a placeholder with \`\` around the data access + https://nextjs.org/docs/messages/blocking-prerender-runtime#wrap-in-or-move-into-suspense + - [cache] For \`params\`: if the params are known, prerender them with \`generateStaticParams\` + https://nextjs.org/docs/messages/blocking-prerender-runtime#for-known-params-prerender + - [block] Set \`export const instant = false\` to silence this warning and allow a blocking route + https://nextjs.org/docs/messages/blocking-prerender-runtime#allow-blocking-route + at body () + at html () + at a () + Build-time instant validation failed for route "/suspense-in-root/blocking-attribution/runtime-in-static/[slug]". + To get a more detailed stack trace and pinpoint the issue, try one of the following: + - Start the app in development mode by running \`next dev\`, then open "/suspense-in-root/blocking-attribution/runtime-in-static/[slug]" in your browser to investigate the error. + - Rerun the production build with \`next build --debug-prerender\` to generate better stack traces. + Stopping prerender due to instant validation errors." + `) + expect(result.exitCode).toBe(1) + } + }) + }) + describe('config depth preference', () => { // When multiple slots have instant configs at different depths, // the deepest config is preferred as the root cause. At equal