File size: 9,852 Bytes
b91e262 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | import { workAsyncStorage } from '../app-render/work-async-storage.external'
import { workUnitAsyncStorage } from '../app-render/work-unit-async-storage.external'
import { abortOnSynchronousPlatformIOAccess } from '../app-render/dynamic-rendering'
import { InvariantError } from '../../shared/lib/invariant-error'
import { RenderStage } from '../app-render/staged-rendering'
import { getServerReact, getClientReact } from '../runtime-reacts.external'
type ApiType = 'time' | 'random' | 'crypto'
export function io(expression: string, type: ApiType) {
const workUnitStore = workUnitAsyncStorage.getStore()
const workStore = workAsyncStorage.getStore()
if (!workUnitStore || !workStore) {
return
}
switch (workUnitStore.type) {
case 'prerender':
case 'prerender-runtime': {
const prerenderSignal = workUnitStore.controller.signal
if (prerenderSignal.aborted === false) {
// If the prerender signal is already aborted we don't need to construct
// any stacks because something else actually terminated the prerender.
let message: string
switch (type) {
case 'time':
message = `Route "${workStore.route}" used ${expression} before accessing either uncached data (e.g. \`fetch()\`) or Request data (e.g. \`cookies()\`, \`headers()\`, \`connection()\`, and \`searchParams\`). Accessing the current time in a Server Component requires reading one of these data sources first. Alternatively, consider moving this expression into a Client Component or Cache Component. See more info here: https://nextjs.org/docs/messages/next-prerender-current-time`
break
case 'random':
message = `Route "${workStore.route}" used ${expression} before accessing either uncached data (e.g. \`fetch()\`) or Request data (e.g. \`cookies()\`, \`headers()\`, \`connection()\`, and \`searchParams\`). Accessing random values synchronously in a Server Component requires reading one of these data sources first. Alternatively, consider moving this expression into a Client Component or Cache Component. See more info here: https://nextjs.org/docs/messages/next-prerender-random`
break
case 'crypto':
message = `Route "${workStore.route}" used ${expression} before accessing either uncached data (e.g. \`fetch()\`) or Request data (e.g. \`cookies()\`, \`headers()\`, \`connection()\`, and \`searchParams\`). Accessing random cryptographic values synchronously in a Server Component requires reading one of these data sources first. Alternatively, consider moving this expression into a Client Component or Cache Component. See more info here: https://nextjs.org/docs/messages/next-prerender-crypto`
break
default:
throw new InvariantError(
'Unknown expression type in abortOnSynchronousPlatformIOAccess.'
)
}
abortOnSynchronousPlatformIOAccess(
workStore.route,
expression,
applyOwnerStack(new Error(message)),
workUnitStore
)
}
break
}
case 'prerender-client': {
const prerenderSignal = workUnitStore.controller.signal
if (prerenderSignal.aborted === false) {
// If the prerender signal is already aborted we don't need to construct
// any stacks because something else actually terminated the prerender.
let message: string
switch (type) {
case 'time':
message = `Route "${workStore.route}" used ${expression} inside a Client Component without a Suspense boundary above it. See more info here: https://nextjs.org/docs/messages/next-prerender-current-time-client`
break
case 'random':
message = `Route "${workStore.route}" used ${expression} inside a Client Component without a Suspense boundary above it. See more info here: https://nextjs.org/docs/messages/next-prerender-random-client`
break
case 'crypto':
message = `Route "${workStore.route}" used ${expression} inside a Client Component without a Suspense boundary above it. See more info here: https://nextjs.org/docs/messages/next-prerender-crypto-client`
break
default:
throw new InvariantError(
'Unknown expression type in abortOnSynchronousPlatformIOAccess.'
)
}
abortOnSynchronousPlatformIOAccess(
workStore.route,
expression,
applyOwnerStack(new Error(message)),
workUnitStore
)
}
break
}
case 'request':
if (process.env.NODE_ENV === 'development') {
const stageController = workUnitStore.stagedRendering
if (stageController && stageController.canSyncInterrupt()) {
let message: string
if (stageController.currentStage === RenderStage.Static) {
switch (type) {
case 'time':
message = `Route "${workStore.route}" used ${expression} before accessing either uncached data (e.g. \`fetch()\`) or Request data (e.g. \`cookies()\`, \`headers()\`, \`connection()\`, and \`searchParams\`). Accessing the current time in a Server Component requires reading one of these data sources first. Alternatively, consider moving this expression into a Client Component or Cache Component. See more info here: https://nextjs.org/docs/messages/next-prerender-current-time`
break
case 'random':
message = `Route "${workStore.route}" used ${expression} before accessing either uncached data (e.g. \`fetch()\`) or Request data (e.g. \`cookies()\`, \`headers()\`, \`connection()\`, and \`searchParams\`). Accessing random values synchronously in a Server Component requires reading one of these data sources first. Alternatively, consider moving this expression into a Client Component or Cache Component. See more info here: https://nextjs.org/docs/messages/next-prerender-random`
break
case 'crypto':
message = `Route "${workStore.route}" used ${expression} before accessing either uncached data (e.g. \`fetch()\`) or Request data (e.g. \`cookies()\`, \`headers()\`, \`connection()\`, and \`searchParams\`). Accessing random cryptographic values synchronously in a Server Component requires reading one of these data sources first. Alternatively, consider moving this expression into a Client Component or Cache Component. See more info here: https://nextjs.org/docs/messages/next-prerender-crypto`
break
default:
throw new InvariantError(
'Unknown expression type in abortOnSynchronousPlatformIOAccess.'
)
}
} else {
// We're in the Runtime stage.
// We only error for Sync IO in the Runtime stage if the route has a runtime prefetch config.
// This check is implemented in `stageController.canSyncInterrupt()` --
// if runtime prefetching isn't enabled, then we won't get here.
let accessStatement: string
let additionalInfoLink: string
switch (type) {
case 'time':
accessStatement = 'the current time'
additionalInfoLink =
'https://nextjs.org/docs/messages/next-prerender-runtime-current-time'
break
case 'random':
accessStatement = 'random values synchronously'
additionalInfoLink =
'https://nextjs.org/docs/messages/next-prerender-runtime-random'
break
case 'crypto':
accessStatement = 'random cryptographic values synchronously'
additionalInfoLink =
'https://nextjs.org/docs/messages/next-prerender-runtime-crypto'
break
default:
throw new InvariantError(
'Unknown expression type in abortOnSynchronousPlatformIOAccess.'
)
}
message = `Route "${workStore.route}" used ${expression} before accessing either uncached data (e.g. \`fetch()\`) or awaiting \`connection()\`. When configured for Runtime prefetching, accessing ${accessStatement} in a Server Component requires reading one of these data sources first. Alternatively, consider moving this expression into a Client Component or Cache Component. See more info here: ${additionalInfoLink}`
}
const syncIOError = applyOwnerStack(new Error(message))
stageController.syncInterruptCurrentStageWithReason(syncIOError)
}
}
break
case 'prerender-ppr':
case 'prerender-legacy':
case 'cache':
case 'private-cache':
case 'unstable-cache':
break
default:
workUnitStore satisfies never
}
}
function applyOwnerStack(error: Error) {
// TODO: Instead of stitching the stacks here, we should log the original
// error as-is when it occurs, and let `patchErrorInspect` handle adding the
// owner stack, instead of logging it deferred in the `LogSafely` component
// via `throwIfDisallowedDynamic`.
if (process.env.NODE_ENV !== 'production') {
const ownerStack =
getClientReact()?.captureOwnerStack?.() ??
getServerReact()?.captureOwnerStack?.()
if (ownerStack) {
let stack = ownerStack
if (error.stack) {
const frames: string[] = []
for (const frame of error.stack.split('\n').slice(1)) {
if (frame.includes('react_stack_bottom_frame')) {
break
}
frames.push(frame)
}
stack = '\n' + frames.join('\n') + stack
}
error.stack = error.name + ': ' + error.message + stack
}
}
return error
}
|