Skip to content

Commit eba3d2b

Browse files
committed
Restore separate runtime/dynamic error messages and fix cookies() classification
Revert the merged bodyMessage/metadataMessage/viewportMessage back to separate runtimeBodyMessage/dynamicBodyMessage etc. so the dev overlay can distinguish runtime APIs from uncached data. Restore the original isRuntimeVariant detection that matches on 'request-time API', 'cookies()', and 'Runtime data' — the tightened check broke classification when the second validation pass overrides with DynamicHoleKind.Dynamic. Also stamp syncDynamicErrorWithStack errors with runtimeBodyMessage since sync dynamic errors are always from runtime APIs. Made-with: Cursor
1 parent 1ac9f81 commit eba3d2b

3 files changed

Lines changed: 86 additions & 40 deletions

File tree

packages/next/src/next-devtools/dev-overlay/components/blocking-route-guidance/blocking-route-error-details.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,11 @@ export type DynamicMetadataErrorDetails = {
1010
}
1111

1212
function isRuntimeVariant(message: string): boolean {
13-
if (message.includes('Runtime data')) return true
14-
if (
15-
message.includes('A request-time API') &&
16-
!message.includes('Either a request-time API')
17-
) {
18-
return true
19-
}
20-
return false
13+
return (
14+
message.includes('request-time API') ||
15+
message.includes('cookies()') ||
16+
message.includes('Runtime data')
17+
)
2118
}
2219

2320
export function getBlockingRouteErrorDetails(

packages/next/src/server/app-render/blocking-route-messages.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,22 @@
1-
export function bodyMessage(route: string): string {
1+
export function runtimeBodyMessage(route: string): string {
2+
return (
3+
`Route "${route}" can't load instantly.\n\n` +
4+
`Cause: A request-time API was used without a surrounding ` +
5+
`<Suspense> boundary. This prevents Next.js from prerendering ` +
6+
`any part of the page.\n\n` +
7+
`Common triggers:\n` +
8+
` - cookies(), headers()\n` +
9+
` - await params, await searchParams\n\n` +
10+
`The right fix depends on which API triggered this and what ` +
11+
`behavior you want. Possible fixes:\n` +
12+
` - <Suspense>: wrap the dynamic component so the rest loads instantly\n` +
13+
` - Move the API call into a child component wrapped in <Suspense>\n` +
14+
` - loading.js: add a route-level fallback\n\n` +
15+
`Learn more: https://nextjs.org/docs/messages/blocking-route`
16+
)
17+
}
18+
19+
export function dynamicBodyMessage(route: string): string {
220
return (
321
`Route "${route}" can't load instantly.\n\n` +
422
`Cause: Uncached data or a request-time API was used without a ` +
@@ -7,8 +25,7 @@ export function bodyMessage(route: string): string {
725
`Common triggers:\n` +
826
` - cookies(), headers(), connection()\n` +
927
` - await params, await searchParams\n` +
10-
` - fetch() or database calls without "use cache"\n` +
11-
` - draftMode()\n\n` +
28+
` - fetch() or database calls without "use cache"\n\n` +
1229
`The right fix depends on which API triggered this and what ` +
1330
`behavior you want. Possible fixes:\n` +
1431
` - <Suspense>: wrap the dynamic component so the rest loads instantly\n` +
@@ -18,7 +35,19 @@ export function bodyMessage(route: string): string {
1835
)
1936
}
2037

21-
export function metadataMessage(route: string): string {
38+
export function runtimeMetadataMessage(route: string): string {
39+
return (
40+
`Route "${route}" has metadata that blocks loading.\n\n` +
41+
`Cause: A request-time API was used inside generateMetadata() ` +
42+
`(or you have file-based metadata like icons that depend on ` +
43+
`dynamic params). The rest of the page could have been fully ` +
44+
`prerendered.\n\n` +
45+
`Learn more: ` +
46+
`https://nextjs.org/docs/messages/next-prerender-dynamic-metadata`
47+
)
48+
}
49+
50+
export function dynamicMetadataMessage(route: string): string {
2251
return (
2352
`Route "${route}" has metadata that blocks loading.\n\n` +
2453
`Cause: generateMetadata() depends on data that can't be resolved ` +
@@ -29,7 +58,18 @@ export function metadataMessage(route: string): string {
2958
)
3059
}
3160

32-
export function viewportMessage(route: string): string {
61+
export function runtimeViewportMessage(route: string): string {
62+
return (
63+
`Route "${route}" has viewport config that blocks loading.\n\n` +
64+
`Cause: A request-time API was used inside generateViewport(). ` +
65+
`Viewport metadata must be available on page load, so this ` +
66+
`prevents prerendering.\n\n` +
67+
`Learn more: ` +
68+
`https://nextjs.org/docs/messages/next-prerender-dynamic-viewport`
69+
)
70+
}
71+
72+
export function dynamicViewportMessage(route: string): string {
3373
return (
3474
`Route "${route}" has viewport config that blocks loading.\n\n` +
3575
`Cause: generateViewport() depends on data that can't be resolved ` +

packages/next/src/server/app-render/dynamic-rendering.ts

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,12 @@ import {
4848
import { scheduleOnNextTick } from '../../lib/scheduler'
4949
import { BailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr'
5050
import {
51-
bodyMessage,
52-
metadataMessage,
53-
viewportMessage,
51+
runtimeBodyMessage,
52+
dynamicBodyMessage,
53+
runtimeMetadataMessage,
54+
dynamicMetadataMessage,
55+
runtimeViewportMessage,
56+
dynamicViewportMessage,
5457
disallowedDynamicViewportMessage,
5558
disallowedDynamicMetadataMessage,
5659
} from './blocking-route-messages'
@@ -817,13 +820,12 @@ export function trackAllowedDynamicAccess(
817820
dynamicValidation.hasAllowedDynamic = true
818821
return
819822
} else if (clientDynamic.syncDynamicErrorWithStack) {
820-
// This task was the task that called the sync error.
821-
dynamicValidation.dynamicErrors.push(
822-
clientDynamic.syncDynamicErrorWithStack
823-
)
823+
const syncError = clientDynamic.syncDynamicErrorWithStack
824+
syncError.message = runtimeBodyMessage(workStore.route)
825+
dynamicValidation.dynamicErrors.push(syncError)
824826
return
825827
} else {
826-
const message = bodyMessage(workStore.route)
828+
const message = dynamicBodyMessage(workStore.route)
827829
const error = addErrorContext(new Error(message), componentStack, null)
828830
dynamicValidation.dynamicErrors.push(error)
829831
return
@@ -873,7 +875,7 @@ export function trackDynamicHoleInNavigation(
873875
componentStack: string,
874876
dynamicValidation: InstantValidationState,
875877
clientDynamic: DynamicTrackingState,
876-
_kind: DynamicHoleKind,
878+
kind: DynamicHoleKind,
877879
boundaryState: ValidationBoundaryTracking
878880
) {
879881
if (hasOutletRegex.test(componentStack)) {
@@ -889,7 +891,10 @@ export function trackDynamicHoleInNavigation(
889891
)
890892

891893
if (hasMetadataRegex.test(componentStack)) {
892-
const message = metadataMessage(workStore.route)
894+
const message =
895+
kind === DynamicHoleKind.Runtime
896+
? runtimeMetadataMessage(workStore.route)
897+
: dynamicMetadataMessage(workStore.route)
893898
const error = addErrorContext(
894899
new Error(message),
895900
componentStack,
@@ -899,7 +904,10 @@ export function trackDynamicHoleInNavigation(
899904
return
900905
}
901906
if (hasViewportRegex.test(componentStack)) {
902-
const message = viewportMessage(workStore.route)
907+
const message =
908+
kind === DynamicHoleKind.Runtime
909+
? runtimeViewportMessage(workStore.route)
910+
: dynamicViewportMessage(workStore.route)
903911
const error = addErrorContext(
904912
new Error(message),
905913
componentStack,
@@ -971,16 +979,19 @@ export function trackDynamicHoleInNavigation(
971979
}
972980

973981
if (clientDynamic.syncDynamicErrorWithStack) {
974-
// This task was the task that called the sync error.
975982
const syncError = clientDynamic.syncDynamicErrorWithStack
983+
syncError.message = runtimeBodyMessage(workStore.route)
976984
if (effectiveCreateInstantStack !== null && syncError.cause === undefined) {
977985
syncError.cause = effectiveCreateInstantStack()
978986
}
979987
dynamicValidation.dynamicErrors.push(syncError)
980988
return
981989
}
982990

983-
const message = bodyMessage(workStore.route)
991+
const message =
992+
kind === DynamicHoleKind.Runtime
993+
? runtimeBodyMessage(workStore.route)
994+
: dynamicBodyMessage(workStore.route)
984995
const error = addErrorContext(
985996
new Error(message),
986997
componentStack,
@@ -1052,15 +1063,15 @@ export function trackDynamicHoleInRuntimeShell(
10521063
// We don't need to track that this is dynamic. It is only so when something else is also dynamic.
10531064
return
10541065
} else if (hasMetadataRegex.test(componentStack)) {
1055-
const message = metadataMessage(workStore.route)
1066+
const message = dynamicMetadataMessage(workStore.route)
10561067
const error = addErrorContext(new Error(message), componentStack, null)
10571068
dynamicValidation.dynamicMetadata = error
10581069
return
10591070
} else if (hasViewportRegex.test(componentStack)) {
10601071
// TODO(instant-validation): If the page only has holes caused by runtime data,
10611072
// we won't find out if there's a suspense-above-body and error for dynamic viewport
10621073
// even if there is in fact a suspense-above-body
1063-
const message = viewportMessage(workStore.route)
1074+
const message = dynamicViewportMessage(workStore.route)
10641075
const error = addErrorContext(new Error(message), componentStack, null)
10651076
dynamicValidation.dynamicErrors.push(error)
10661077
return
@@ -1081,14 +1092,13 @@ export function trackDynamicHoleInRuntimeShell(
10811092
dynamicValidation.hasAllowedDynamic = true
10821093
return
10831094
} else if (clientDynamic.syncDynamicErrorWithStack) {
1084-
// This task was the task that called the sync error.
1085-
dynamicValidation.dynamicErrors.push(
1086-
clientDynamic.syncDynamicErrorWithStack
1087-
)
1095+
const syncError = clientDynamic.syncDynamicErrorWithStack
1096+
syncError.message = runtimeBodyMessage(workStore.route)
1097+
dynamicValidation.dynamicErrors.push(syncError)
10881098
return
10891099
}
10901100

1091-
const message = bodyMessage(workStore.route)
1101+
const message = runtimeBodyMessage(workStore.route)
10921102
const error = addErrorContext(new Error(message), componentStack, null)
10931103
dynamicValidation.dynamicErrors.push(error)
10941104
return
@@ -1104,12 +1114,12 @@ export function trackDynamicHoleInStaticShell(
11041114
// We don't need to track that this is dynamic. It is only so when something else is also dynamic.
11051115
return
11061116
} else if (hasMetadataRegex.test(componentStack)) {
1107-
const message = metadataMessage(workStore.route)
1117+
const message = runtimeMetadataMessage(workStore.route)
11081118
const error = addErrorContext(new Error(message), componentStack, null)
11091119
dynamicValidation.dynamicMetadata = error
11101120
return
11111121
} else if (hasViewportRegex.test(componentStack)) {
1112-
const message = viewportMessage(workStore.route)
1122+
const message = runtimeViewportMessage(workStore.route)
11131123
const error = addErrorContext(new Error(message), componentStack, null)
11141124
dynamicValidation.dynamicErrors.push(error)
11151125
return
@@ -1130,13 +1140,12 @@ export function trackDynamicHoleInStaticShell(
11301140
dynamicValidation.hasAllowedDynamic = true
11311141
return
11321142
} else if (clientDynamic.syncDynamicErrorWithStack) {
1133-
// This task was the task that called the sync error.
1134-
dynamicValidation.dynamicErrors.push(
1135-
clientDynamic.syncDynamicErrorWithStack
1136-
)
1143+
const syncError = clientDynamic.syncDynamicErrorWithStack
1144+
syncError.message = runtimeBodyMessage(workStore.route)
1145+
dynamicValidation.dynamicErrors.push(syncError)
11371146
return
11381147
} else {
1139-
const message = bodyMessage(workStore.route)
1148+
const message = dynamicBodyMessage(workStore.route)
11401149
const error = addErrorContext(new Error(message), componentStack, null)
11411150
dynamicValidation.dynamicErrors.push(error)
11421151
return

0 commit comments

Comments
 (0)