diff --git a/errors/next-prerender-dynamic-metadata.mdx b/errors/next-prerender-dynamic-metadata.mdx index 17f835df14fbc..40c4ba0a0aa6e 100644 --- a/errors/next-prerender-dynamic-metadata.mdx +++ b/errors/next-prerender-dynamic-metadata.mdx @@ -1,12 +1,16 @@ --- -title: Cannot access Request information or uncached data in `generateMetadata()` in an otherwise entirely static route +title: Cannot access Runtime data or uncached data in `generateMetadata()` or file-based Metadata in an otherwise entirely static route --- ## Why This Error Occurred -When `cacheComponents` is enabled, Next.js requires that `generateMetadata()` not depend on uncached data or Request data unless some other part of the page also has similar requirements. The reason for this is that while you normally control your intention for what is allowed to be dynamic by adding or removing Suspense boundaries in your Layout and Page components you are not in control of rendering metadata itself. +When `cacheComponents` is enabled, Next.js requires that Document Metadata not depend on uncached data or Runtime data (`cookies()`, `headers()`, `params`, `searchParams`) unless some other part of the page also has similar requirements. -The heuristic Next.js uses to understand your intent with `generateMetadata()` is to look at the data requirements of the rest of the route. If other components depend on Request data or uncached data, then we allow `generateMetadata()` to have similar data requirements. If the rest of your page has no dependence on this type of data, we require that `generateMetadata()` also not have this type of data dependence. +Next.js determines if a page is entirely static or partially static by looking at whether any part of the page cannot be prerendered. + +Typically you control the which parts of a page can be prerendered by adding `"use cache"` to Components or data functions and by avoiding Runtime data like `cookies()` or `searchParams`. However, Metadata can be defined in functions (`generateMetadata()`) defined far from your Page content. Additionally Metadata can implicitly depend on Runtime data when using file-based Metadata such as an icon inside a route with a dynamic param. It would be easy for Metadata to accidentally make an otherwise entirely static page have a dynamic component. + +To prevent anwanted partially dynamic pages, Next.js expects pages that are otherwise entirely prerenderable to also have prerenderable Metadata. ## Possible Ways to Fix It @@ -141,7 +145,7 @@ export default function Page() { } ``` -Note: The reason to structure this `DynamicMarker` as a self-contained Suspense boundary is to avoid blocking the actual content of the page from being prerendered. When Partial Prerendering is enabled alongside `cacheComponents`, the static shell will still contain all of the prerenderable content, and only the metadata will stream in dynamically. +Note: The reason to structure this `DynamicMarker` as a self-contained Suspense boundary is to avoid blocking the actual content of the page from being prerendered. ## Useful Links diff --git a/errors/next-prerender-dynamic-viewport.mdx b/errors/next-prerender-dynamic-viewport.mdx index 59993c95a0f67..4603cf2d0791c 100644 --- a/errors/next-prerender-dynamic-viewport.mdx +++ b/errors/next-prerender-dynamic-viewport.mdx @@ -1,16 +1,61 @@ --- -title: Cannot access Request information or uncached data in `generateViewport()` +title: Cannot access Runtime data or uncached data in `generateViewport()` --- ## Why This Error Occurred -When `cacheComponents` is enabled, Next.js requires that `generateViewport()` not depend on uncached data or Request data unless you explicitly opt into having a fully dynamic page. If you encountered this error, it means that `generateViewport` depends on one of these types of data and you have not specifically indicated that the affected route should be entirely dynamic. +When `cacheComponents` is enabled, Next.js requires that `generateViewport()` not depend on uncached data or Runtime data (`cookies()`, `headers()`, `params`, `searchParams`) unless you explicitly opt into having a fully dynamic page. If you encountered this error, it means that `generateViewport` depends on one of these types of data and you have not specifically indicated that blocking navigations are acceptable. ## Possible Ways to Fix It To fix this issue, you must first determine your goal for the affected route. -Normally, the way you indicate to Next.js that you want to allow reading Request data or uncached external data is by performing this data access inside a component with an ancestor Suspense boundary. With Viewport, however, you aren't directly in control of wrapping the location where this metadata will be rendered, and even if you could wrap it in a Suspense boundary, it would not be correct to render it with a fallback. This is because this metadata is critical to properly loading resources such as images and must be part of the initial App Shell (the initial HTML containing the document head as well as the first paintable UI). +Normally, Next.js ensures every page can produce an initial UI that allows the page to start loading even before uncached data and Runtime data is available. This is accomplished by defining prerenderable UI with Suspense. However viewport metadata is not able to be deferred until after the page loads because it affects initial page load UI. + +Ideally, you update `generateViewport` so it does not depend on any uncached data or Runtime data. This allows navigations to appear instant. + +However if this is not possibl you can instruct Next.js to allow all navigations to be potentially blocking by wrapping your document `` in a Suspense boundary. + +### Caching External Data + +When external data is cached, Next.js can prerender with it, which ensures that the App Shell always has the complete viewport metadata available. Consider using `"use cache"` to mark the function producing the external data as cacheable. + +Before: + +```jsx filename="app/.../layout.tsx" +import { db } from './db' + +export async function generateViewport() { + const { width, initialScale } = await db.query('viewport-size') + return { + width, + initialScale, + } +} + +export default async function Layout({ children }) { + return ... +} +``` + +After: + +```jsx filename="app/.../layout.tsx" +import { db } from './db' + +export async function generateViewport() { + "use cache" + const { width, initialScale } = await db.query('viewport-size') + return { + width, + initialScale, + } +} + +export default async function Layout({ children }) { + return ... +} +``` ### If you must access Request Data or your external data is uncacheable @@ -61,47 +106,6 @@ export default function RootLayout({ children }) { } ``` -### Caching External Data - -When external data is cached, Next.js can prerender with it, which ensures that the App Shell always has the complete viewport metadata available. Consider using `"use cache"` to mark the function producing the external data as cacheable. - -Before: - -```jsx filename="app/.../layout.tsx" -import { db } from './db' - -export async function generateViewport() { - const { width, initialScale } = await db.query('viewport-size') - return { - width, - initialScale, - } -} - -export default async function Layout({ children }) { - return ... -} -``` - -After: - -```jsx filename="app/.../layout.tsx" -import { db } from './db' - -export async function generateViewport() { - "use cache" - const { width, initialScale } = await db.query('viewport-size') - return { - width, - initialScale, - } -} - -export default async function Layout({ children }) { - return ... -} -``` - ## Useful Links - [`generateViewport()`](/docs/app/api-reference/functions/generate-viewport) diff --git a/errors/next-prerender-runtime-crypto.mdx b/errors/next-prerender-runtime-crypto.mdx new file mode 100644 index 0000000000000..8366bcbb62070 --- /dev/null +++ b/errors/next-prerender-runtime-crypto.mdx @@ -0,0 +1,145 @@ +--- +title: Cannot access `crypto.getRandomValue()`, `crypto.randomUUID()`, or another web or node crypto API that generates random values synchronously before other uncached data or `connection()` in a Server Component +--- + +## Why This Error Occurred + +An API that produces a random value synchronously from the Web Crypto API or from Node's `crypto` package was used in a Server Component before accessing other uncached data through APIs like `fetch()` and native database drivers, or the `connection()` API. While typically random crypto values can be guarded behind Runtime data like `cookies()`, `headers()`, `params`, and `searchParams`, this particular route is configured for Runtime Prefetching which makes these APIs available as part of the prefetch request. Accessing random values synchronously without preceding it with uncached data or `await connection()` interferes with the framework's ability to produce a correct prefetch result. + +## Possible Ways to Fix It + +If the random crypto value is appropriate to be prefetched consider moving it into a Cache Component or Cache Function with the `"use cache"` directive. + +If the random crypto value is intended to be generated on every user navigation consider whether an async API exists that achieves the same result. If not consider whether you can move the random crypto value generation later, behind other existing uncached data or Request data access. If there is no way to do this you can always precede the random crypto value generation with Request data access by using `await connection()`. + +### Cache the token value + +If you are generating a token to talk to a database that itself should be cached move the token generation inside the `"use cache"`. + +Before: + +```jsx filename="app/page.js" +export const unstable_prefetch = { + mode: 'runtime', + samples: [...], +} + +async function getCachedData(token: string, userId: string) { + "use cache" + return db.query(token, userId, ...) +} + +export default async function Page({ params }) { + const { userId } = await params + const token = crypto.randomUUID() + const data = await getCachedData(token, userId); + return ... +} +``` + +After: + +```jsx filename="app/page.js" +export const unstable_prefetch = { + mode: 'runtime', + samples: [...], +} + +async function getCachedData(userId: string) { + "use cache" + const token = crypto.randomUUID() + return db.query(token, userId, ...) +} + +export default async function Page({ params }) { + const { userId } = await params + const data = await getCachedData(userId); + return ... +} +``` + +### Use an async API at request-time + +If you require this random value to be unique per Request and an async version of the API exists switch to it instead. Also ensure that there is a parent Suspense boundary that defines a fallback UI Next.js can use while rendering this component on each Request. + +Before: + +```jsx filename="app/page.js" +export const unstable_prefetch = { + mode: 'runtime', + samples: [...], +} + +import { generateKeySync } from 'node:crypto' + +export default async function Page({ params }) { + const { dataId } = await params + const data = await fetchData(dataId) + const key = generateKeySync('hmac', { ... }) + const digestedData = await digestDataWithKey(data, key); + return ... +} +``` + +After: + +```jsx filename="app/page.js" +export const unstable_prefetch = { + mode: 'runtime', + samples: [...], +} + +import { generateKey } from 'node:crypto' + +export default async function Page({ params }) { + const { dataId } = await params + const data = await fetchData(dataId) + const key = await new Promise(resolve => generateKey('hmac', { ... }, key => resolve(key))) + const digestedData = await digestDataWithKey(data, key); + return ... +} +``` + +### Use `await connection()` at request-time + +If you require this random value to be unique per Request and an async version of the API does not exist, call `await connection()`. Also ensure that there is a parent Suspense boundary that defines a fallback UI Next.js can use while rendering this component on each Request. + +Before: + +```jsx filename="app/page.js" +export const unstable_prefetch = { + mode: 'runtime', + samples: [...], +} + +export default async function Page({ params }) { + const { sessionId } = await params + const uuid = crypto.randomUUID() + return +} +``` + +After: + +```jsx filename="app/page.js" +export const unstable_prefetch = { + mode: 'runtime', + samples: [...], +} + +import { connection } from 'next/server' + +export default async function Page({ params }) { + await connection() + const { sessionId } = await params + const uuid = crypto.randomUUID() + return +} +``` + +## Useful Links + +- [`connection` function](/docs/app/api-reference/functions/connection) +- [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) +- [Node Crypto API](https://nodejs.org/docs/latest/api/crypto.html) +- [`Suspense` React API](https://react.dev/reference/react/Suspense) diff --git a/errors/next-prerender-runtime-current-time.mdx b/errors/next-prerender-runtime-current-time.mdx new file mode 100644 index 0000000000000..7125acd2dbe60 --- /dev/null +++ b/errors/next-prerender-runtime-current-time.mdx @@ -0,0 +1,292 @@ +--- +title: Cannot access `Date.now()`, `Date()`, or `new Date()` before other uncached data or `connection()` in a Server Component +--- + +## Why This Error Occurred + +`Date.now()`, `Date()`, or `new Date()` was used in a Server Component before accessing other uncached data through APIs like `fetch()` and native database drivers, or the `connection()` API. While typically reading the current time can be guarded behind Runtime data like `cookies()`, `headers()`, `params`, and `searchParams`, this particular route is configured for Runtime Prefetching which makes these APIs available as part of the prefetch request. Reading the current time without preceding it with uncached data or `await connection()` interferes with the framework's ability to produce a correct prefetch result. + +## Possible Ways to Fix It + +If the current time is being used for diagnostic purposes such as logging or performance tracking consider using `performance.now()` instead. + +If the current time is appropriate to be prefetched consider moving it into a Cache Component or Cache Function with the `"use cache"` directive. + +If the current time is intended to be accessed dynamically on every user navigation first consider whether it is more appropriate to access it in a Client Component, which can often be the case when reading the time for display purposes. If a Client Component isn't the right choice then consider whether you can move the current time access later, behind other existing uncached data or Request data access. If there is no way to do this you can always precede the current time access with Request data access by using `await connection()`. + +> **Note**: Sometimes the place that accesses the current time is inside 3rd party code. While you can't easily convert the time access to `performance.now()` the other strategies can be applied in your own project code regardless of how deeply the time is read. + +### Performance use case + +If you are using the current time for performance tracking with elapsed time use `performance.now()`. + +Before: + +```jsx filename="app/page.js" +export const unstable_prefetch = { + mode: 'runtime', + samples: [...], +} + +export default async function Page({ params }) { + const { id } = await params + const start = Date.now(); + const data = computeDataSlowly(id, ...); + const end = Date.now(); + console.log(`somethingSlow took ${end - start} milliseconds to complete`) + + return ... +} +``` + +After: + +```jsx filename="app/page.js" +export const unstable_prefetch = { + mode: 'runtime', + samples: [...], +} + +export default async function Page({ params }) { + const { id } = await params + const start = performance.now(); + const data = computeDataSlowly(id, ...); + const end = performance.now(); + console.log(`somethingSlow took ${end - start} milliseconds to complete`) + return ... +} +``` + +> **Note**: If you need report an absolute time to an observability tool you can also use `performance.timeOrigin + performance.now()`. +> **Note**: It is essential that the values provided by `performance.now()` do not influence the rendered output of your Component and should never be passed into Cache Functions as arguments or props. + +### Cacheable use cases + +If you want to read the time when some cache entry is created (such as when a Next.js page is rendered at build-time or when revalidating a static page), move the current time read inside a cached function using `"use cache"`. + +Before: + +```jsx filename="app/page.js" +export const unstable_prefetch = { + mode: 'runtime', + samples: [...], +} + +async function InformationTable({ id }) { + const data = await fetch(urlFrom(id)) + return ( +
+

Latest Info...

+ {renderData(data)}
+
+ ) +} + +export default async function Page({ params }) { + const { id } = await params + return ( +
+ + Last Refresh: {new Date().toString()} +
+ ) +} +``` + +After: + +```jsx filename="app/page.js" +export const unstable_prefetch = { + mode: 'runtime', + samples: [...], +} + +async function InformationTable({ id }) { + "use cache" + const data = await fetch(urlFrom(id)) + return ( + <> +
+

Latest Info...

+ {renderData(data)}
+
+ Last Refresh: {new Date().toString()} + + ) +} + +export default async function Page({ params }) { + const { id } = await params + return ( +
+ +
+ ) +} +``` + +### Request-time use case + +#### Moving time to the client + +If the current time must be evaluated on each user Request consider moving the current time read into a Client Component. You might also find that this is more convenient when you want to do things like update the time independent of a page navigation. For instance imagine you have a relative time component. Instead of rendering the relative time in a Server Component on each Request you can render the relative time when the Client Component renders and then update it periodically. + +If you go with this approach you will need to ensure the Client Component which reads the time during render has a Suspense boundary above it. You may be able to improve the loading experience by adopting a more narrowly scoped Suspense boundary. Use your judgement about what kind of UI loading sequence you want your users to experience to guide your decision here. + +Before: + +```jsx filename="app/page.js" +export const unstable_prefetch = { + mode: 'runtime', + samples: [...], +} + +function RelativeTime({ when }) { + return computeTimeAgo(new Date(), when) +} + +async function getData(token) { + "use cache" + return ... +} + +export default async function Page({ params }) { + const token = (await cookies()).get('token')?.value + const data = await getData(token) + return ( +
+ ... + + + +
+ ) +} +``` + +After: + +```jsx filename="app/relative-time.js" +'use client' + +import { useReducer } from 'react' + +export function RelativeTime({ when }) { + const [_, update] = useReducer(() => ({}), {}) + const timeAgo = computeTimeAgo(new Date(), when) + + // Whenever the timeAgo value changes a new timeout is + // scheduled to update the component. Now the time can + // rerender without having the Server Component render again. + useEffect(() => { + const updateAfter = computeTimeUntilNextUpdate(timeAgo) + let timeout = setTimeout(() => { + update() + }, updateAfter) + return () => { + clearTimeout(timeout) + } + }) + + return timeAgo +} +``` + +```jsx filename="app/page.js" +export const unstable_prefetch = { + mode: 'runtime', + samples: [...], +} + +import { RelativeTime } from './relative-time' + +async function getData(token) { + "use cache" + return ... +} + +export default async function Page({ params }) { + const token = (await cookies()).get('token')?.value + const data = await getData(token) + return ( +
+ ... + + + +
+ ) +} +``` + +> **Note**: Accessing the current time in a Client Component will still cause it to be excluded from prerendered server HTML but Next.js allows this within Client Components because it can either compute the time dynamically when the user requests the HTML page or in the browser. + +#### Guarding the time with `await connection()` + +It may be that you want to make some rendering determination using the current time on the server and thus cannot move the time read into a Client Component. In this case you must instruct Next.js that the time read is meant to be evaluated at request time by preceding it with `await connection()`. + +Next.js enforces that it can always produce at least a partially static initial HTML page so you will also need to ensure that there is a Suspense boundary somewhere above this component that informs Next.js about the intended fallback UI to use while prerendering this page. + +Before: + +```jsx filename="app/page.js" +export const unstable_prefetch = { + mode: 'runtime', + samples: [...], +} + +export default async function Page() { + const lastImpressionTime = (await cookies()).get('last-impression-time')?.value + const currentTime = Date.now() + if (currentTime > lastImpressionTime + SOME_INTERVAL) { + return + } else { + return + } +} +``` + +After: + +```jsx filename="app/page.js" +export const unstable_prefetch = { + mode: 'runtime', + samples: [...], +} + +import { Suspense } from 'react' +import { connection } from 'next/server' + +async function BannerSkeleton() { + ... +} + +export default async function Page() { + return }> + + +} + +async function DynamicBanner() { + await connection(); + const lastImpressionTime = (await cookies()).get('last-impression-time')?.value + const currentTime = Date.now() + if (currentTime > lastImpressionTime + SOME_INTERVAL) { + return + } else { + return + } +} +``` + +> **Note**: This example illustrates using `await connection()`, but you could alternatively move where a uncached fetch happens or read cookies before as well. + +## Useful Links + +- [`Date.now` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now) +- [`Date constructor` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date) +- [`connection` function](/docs/app/api-reference/functions/connection) +- [`performance` Web API](https://developer.mozilla.org/en-US/docs/Web/API/Performance) +- [`Suspense` React API](https://react.dev/reference/react/Suspense) +- [`useLayoutEffect` React Hook](https://react.dev/reference/react/useLayoutEffect) +- [`useEffect` React Hook](https://react.dev/reference/react/useEffect) diff --git a/errors/next-prerender-runtime-random.mdx b/errors/next-prerender-runtime-random.mdx new file mode 100644 index 0000000000000..c7ad97a8680b6 --- /dev/null +++ b/errors/next-prerender-runtime-random.mdx @@ -0,0 +1,120 @@ +--- +title: Cannot access `Math.random()` before other uncached data or `connection()` in a Server Component +--- + +## Why This Error Occurred + +`Math.random()` was called in a Server Component before accessing other uncached data through APIs like `fetch()` and native database drivers, or the `connection()` API. While typically random values can be guarded behind Runtime data like `cookies()`, `headers()`, `params`, and `searchParams`, this particular route is configured for Runtime Prefetching which makes these APIs available as part of the prefetch request. Accessing random values without preceding it with uncached data or `await connection()` interferes with the framework's ability to produce a correct prefetch result. + +## Possible Ways to Fix It + +If the random value is appropriate to be prefetched consider moving it into a Cache Component or Cache Function with the `"use cache"` directive. + +If the random value is intended to be generated on every user navigation consider whether you can move the random value generation later, behind other existing uncached data or Request data access. If there is no way to do this you can always precede the random value generation with Request data access by using `await connection()`. + +If the random value is being used as a unique identifier for diagnostic purposes such as logging or tracking consider using an alternate method of id generation that does not rely on random number generation such as incrementing an integer. + +> **Note**: Sometimes the place that generates a random value synchronously is inside 3rd party code. While you can't easily replace the `Math.random()` call directly, the other strategies can be applied in your own project code regardless of how deep random generation is. + +### Cache the random value + +If your random value is cacheable, move the `Math.random()` call to a `"use cache"` function. For instance, imagine you have a product page and you want to randomize the product order periodically but you are fine with the random order being re-used for different users. + +Before: + +```jsx filename="app/page.js" +export const unstable_prefetch = { + mode: 'runtime', + samples: [...], +} + +export default async function Page({ params }) { + const { category } = await params + const products = await getCachedProducts(category) + const randomSeed = Math.random() + const randomizedProducts = randomize(products, randomSeed) + return +} +``` + +After: + +```jsx filename="app/page.js" +export const unstable_prefetch = { + mode: 'runtime', + samples: [...], +} + +async function RandomizedProductsView({ category }) { + 'use cache' + const products = await getCachedProducts(category) + const randomSeed = Math.random() + const randomizedProducts = randomize(products, randomSeed) + return +} + +export default async function Page({ params }) { + const { category } = await params + return +} +``` + +> **Note**: `"use cache"` is a powerful API with some nuances. If your cache lifetime is too short Next.js may still exclude it from prerendering. Check out the docs for `"use cache"` to learn more. + +### Indicate the random value is unique per Request + +If you want the random value to be evaluated on each Request precede it with `await connection()`. Next.js will exclude this Server Component from the prerendered HTML and include the fallback UI from the nearest Suspense boundary wrapping this component instead. When a user makes a Request for this page the Server Component will be rendered and the updated UI will stream in dynamically. + +Before: + +```jsx filename="app/page.js" +export const unstable_prefetch = { + mode: 'runtime', + samples: [...], +} + +export default async function Page({ params }) { + const { category } = await params + const products = await getCachedProducts(category) + const randomSeed = Math.random() + const randomizedProducts = randomize(products, randomSeed) + return +} +``` + +After: + +```jsx filename="app/page.js" +export const unstable_prefetch = { + mode: 'runtime', + samples: [...], +} + +import { connection } from 'next/server' + +async function ProductsSkeleton() { + ... +} + +export default async function Page({ params }) { + const { category } = await params + const products = await getCachedProducts(category); + return ( + }> + + + ) +} + +async function DynamicProductsView({ products }) { + await connection(); + const randomSeed = Math.random() + const randomizedProducts = randomize(products, randomSeed) + return +} +``` + +## Useful Links + +- [`connection` function](/docs/app/api-reference/functions/connection) +- [`Suspense` React API](https://react.dev/reference/react/Suspense) diff --git a/packages/next/errors.json b/packages/next/errors.json index 8ef0f033634fa..592dc24ee4faa 100644 --- a/packages/next/errors.json +++ b/packages/next/errors.json @@ -903,5 +903,10 @@ "902": "Invalid handler fields configured for \"cacheHandlers\":\\n%s", "903": "The file \"%s\" must export a function, either as a default export or as a named \"%s\" export.\\nThis function is what Next.js runs for every request handled by this %s.\\n\\nWhy this happens:\\n%s- The file exists but doesn't export a function.\\n- The export is not a function (e.g., an object or constant).\\n- There's a syntax error preventing the export from being recognized.\\n\\nTo fix it:\\n- Ensure this file has either a default or \"%s\" function export.\\n\\nLearn more: https://nextjs.org/docs/messages/middleware-to-proxy", "904": "The file \"%s\" must export a function, either as a default export or as a named \"%s\" export.", - "905": "Page \"%s\" cannot use \\`export const unstable_prefetch = ...\\` without enabling \\`cacheComponents\\`." + "905": "Page \"%s\" cannot use \\`export const unstable_prefetch = ...\\` without enabling \\`cacheComponents\\`.", + "906": "Route \"%s\" did not produce a static shell and Next.js was unable to determine a reason.", + "907": "The next-server runtime is not available in Edge runtime.", + "908": "`abandonRender` called on a stage controller that cannot be abandoned.", + "909": "Expected debug stream to be a ReadableStream", + "910": "Expected debug stream to be a Readable" } diff --git a/packages/next/src/client/app-index.tsx b/packages/next/src/client/app-index.tsx index aeef8bda32587..23767b42bdd3d 100644 --- a/packages/next/src/client/app-index.tsx +++ b/packages/next/src/client/app-index.tsx @@ -214,7 +214,6 @@ if (clientResumeFetch) { callServer, findSourceMapURL, debugChannel, - // @ts-expect-error This is not yet part of the React types startTime: 0, } ) diff --git a/packages/next/src/lib/metadata/metadata.tsx b/packages/next/src/lib/metadata/metadata.tsx index 475ecd480d354..83ec9bb5127d4 100644 --- a/packages/next/src/lib/metadata/metadata.tsx +++ b/packages/next/src/lib/metadata/metadata.tsx @@ -81,8 +81,8 @@ export function createMetadataComponents({ workStore ) - function Viewport() { - const pendingViewportTags = getResolvedViewport( + async function Viewport() { + const tags = await getResolvedViewport( tree, searchParams, getDynamicParamFromSegment, @@ -107,17 +107,20 @@ export function createMetadataComponents({ return null }) + return tags + } + Viewport.displayName = 'Next.Viewport' + + function ViewportWrapper() { return ( - {/* @ts-expect-error -- Promise not considered a valid child even though it is */} - {pendingViewportTags} + ) } - Viewport.displayName = 'Next.Viewport' - function Metadata() { - const pendingMetadataTags = getResolvedMetadata( + async function Metadata() { + const tags = await getResolvedMetadata( tree, pathnameForMetadata, searchParams, @@ -146,14 +149,18 @@ export function createMetadataComponents({ return null }) + return tags + } + Metadata.displayName = 'Next.Metadata' + + function MetadataWrapper() { // TODO: We shouldn't change what we render based on whether we are streaming or not. // If we aren't streaming we should just block the response until we have resolved the // metadata. if (!serveStreamingMetadata) { return ( - {/* @ts-expect-error -- Promise not considered a valid child even though it is */} - {pendingMetadataTags} + ) } @@ -161,14 +168,12 @@ export function createMetadataComponents({ ) } - Metadata.displayName = 'Next.Metadata' function MetadataOutlet() { const pendingOutlet = Promise.all([ @@ -205,8 +210,8 @@ export function createMetadataComponents({ MetadataOutlet.displayName = 'Next.MetadataOutlet' return { - Viewport, - Metadata, + Viewport: ViewportWrapper, + Metadata: MetadataWrapper, MetadataOutlet, } } diff --git a/packages/next/src/next-devtools/dev-overlay/components/errors/error-type-label/error-type-label.tsx b/packages/next/src/next-devtools/dev-overlay/components/errors/error-type-label/error-type-label.tsx index a1d3c5c760f79..7f3291e5a333b 100644 --- a/packages/next/src/next-devtools/dev-overlay/components/errors/error-type-label/error-type-label.tsx +++ b/packages/next/src/next-devtools/dev-overlay/components/errors/error-type-label/error-type-label.tsx @@ -4,6 +4,7 @@ export type ErrorType = | `Console ${string}` | `Recoverable ${string}` | 'Blocking Route' + | 'Ambiguous Metadata' type ErrorTypeLabelProps = { errorType: ErrorType @@ -13,7 +14,7 @@ export function ErrorTypeLabel({ errorType }: ErrorTypeLabelProps) { return ( {errorType} diff --git a/packages/next/src/next-devtools/dev-overlay/container/errors.tsx b/packages/next/src/next-devtools/dev-overlay/container/errors.tsx index 47a2a37e0c84f..6929aef9da5c2 100644 --- a/packages/next/src/next-devtools/dev-overlay/container/errors.tsx +++ b/packages/next/src/next-devtools/dev-overlay/container/errors.tsx @@ -1,4 +1,4 @@ -import { useMemo, useRef, Suspense, useCallback } from 'react' +import React, { useMemo, useRef, Suspense, useCallback } from 'react' import type { DebugInfo } from '../../shared/types' import { Overlay, OverlayBackdrop } from '../components/overlay' import { RuntimeError } from './runtime-error' @@ -56,111 +56,524 @@ function GenericErrorDescription({ error }: { error: Error }) { ) } -function BlockingPageLoadErrorDescription() { - return ( -
-

- Uncached data was accessed outside of {''} -

-

- This delays the entire page from rendering, resulting in a slow user - experience. Next.js uses this error to ensure your app loads instantly - on every navigation. -

-

To fix this, you can either:

-

- Wrap the component in a {''} boundary. This - allows Next.js to stream its contents to the user as soon as it's ready, - without blocking the rest of the app. -

-

- or -

-

- - Move the asynchronous await into a Cache Component ( - "use cache") - - . This allows Next.js to statically prerender the component as part of - the HTML document, so it's instantly visible to the user. -

-

- Note that request-specific information — such as params, cookies, - and headers — is not available during static prerendering, so must - be wrapped in {''}. -

-

- Learn more:{' '} - - https://nextjs.org/docs/messages/blocking-route - -

-
- ) +function DynamicMetadataErrorDescription({ + variant, +}: { + variant: 'navigation' | 'runtime' +}) { + if (variant === 'navigation') { + return ( +
+

+ Data that blocks navigation was accessed inside{' '} + generateMetadata() in an otherwise prerenderable page +

+

+ When Document metadata is the only part of a page that cannot be + prerendered Next.js expects you to either make it prerenderable or + make some other part of the page non-prerenderable to avoid + unintentional partially dynamic pages. Uncached data such as{' '} + fetch(...), cached data with a low expire time, or{' '} + connection() are all examples of data that only resolve + on navigation. +

+

To fix this:

+

+ + Move the asynchronous await into a Cache Component ( + "use cache") + + . This allows Next.js to statically prerender{' '} + generateMetadata() as part of the HTML document, so it's + instantly visible to the user. +

+

+ or +

+

+ + add connection() inside a {''} + {' '} + somewhere in a Page or Layout. This tells Next.js that the page is + intended to have some non-prerenderable parts. +

+

+ Learn more:{' '} + + https://nextjs.org/docs/messages/next-prerender-dynamic-metadata + +

+
+ ) + } else { + return ( +
+

+ Runtime data was accessed inside generateMetadata() or + file-based metadata +

+

+ When Document metadata is the only part of a page that cannot be + prerendered Next.js expects you to either make it prerenderable or + make some other part of the page non-prerenderable to avoid + unintentional partially dynamic pages. +

+

To fix this:

+

+ + Remove the Runtime data access from generateMetadata() + + . This allows Next.js to statically prerender{' '} + generateMetadata() as part of the HTML document, so it's + instantly visible to the user. +

+

+ or +

+

+ + add connection() inside a {''} + {' '} + somewhere in a Page or Layout. This tells Next.js that the page is + intended to have some non-prerenderable parts. +

+

+ Note that if you are using file-based metadata, such as icons, inside + a route with dynamic params then the only recourse is to make some + other part of the page non-prerenderable. +

+

+ Learn more:{' '} + + https://nextjs.org/docs/messages/next-prerender-dynamic-metadata + +

+
+ ) + } +} + +function BlockingPageLoadErrorDescription({ + variant, + refinement, +}: { + variant: 'navigation' | 'runtime' + refinement: '' | 'generateViewport' | 'generateMetadata' +}) { + if (refinement === 'generateViewport') { + if (variant === 'navigation') { + return ( +
+

+ Data that blocks navigation was accessed inside{' '} + generateViewport() +

+

+ Viewport metadata needs to be available on page load so accessing + data that waits for a user navigation while producing it prevents + Next.js from prerendering an initial UI. Uncached data such as{' '} + fetch(...), cached data with a low expire time, or{' '} + connection() are all examples of data that only resolve + on navigation. +

+

To fix this:

+

+ + Move the asynchronous await into a Cache Component ( + "use cache") + + . This allows Next.js to statically prerender{' '} + generateViewport() as part of the HTML document, so + it's instantly visible to the user. +

+

+ or +

+

+ + Put a {''} around your document{' '} + {''}. + + This indicate to Next.js that you are opting into allowing blocking + navigations for any page. +

+

+ Learn more:{' '} + + https://nextjs.org/docs/messages/next-prerender-dynamic-viewport + +

+
+ ) + } else { + return ( +
+

+ Runtime data was accessed inside generateViewport() +

+

+ Viewport metadata needs to be available on page load so accessing + data that comes from a user Request while producing it prevents + Next.js from prerendering an initial UI. + cookies(), headers(), and{' '} + searchParams, are examples of Runtime data that can + only come from a user request. +

+

To fix this:

+

+ Remove the Runtime data requirement from{' '} + generateViewport. This allows Next.js to statically + prerender generateViewport() as part of the HTML + document, so it's instantly visible to the user. +

+

+ or +

+

+ + Put a {''} around your document{' '} + {''}. + + This indicate to Next.js that you are opting into allowing blocking + navigations for any page. +

+

+ params are usually considered Runtime data but if all + params are provided a value using generateStaticParams{' '} + they can be statically prerendered. +

+

+ Learn more:{' '} + + https://nextjs.org/docs/messages/next-prerender-dynamic-viewport + +

+
+ ) + } + } else if (refinement === 'generateMetadata') { + if (variant === 'navigation') { + return ( +
+

+ Data that blocks navigation was accessed inside{' '} + generateMetadata() in an otherwise prerenderable page +

+

+ When Document metadata is the only part of a page that cannot be + prerendered Next.js expects you to either make it prerenderable or + make some other part of the page non-prerenderable to avoid + unintentional partially dynamic pages. Uncached data such as{' '} + fetch(...), cached data with a low expire time, or{' '} + connection() are all examples of data that only resolve + on navigation. +

+

To fix this:

+

+ + Move the asynchronous await into a Cache Component ( + "use cache") + + . This allows Next.js to statically prerender{' '} + generateMetadata() as part of the HTML document, so + it's instantly visible to the user. +

+

+ or +

+

+ + add connection() inside a {''} + {' '} + somewhere in a Page or Layout. This tells Next.js that the page is + intended to have some non-prerenderable parts. +

+

+ Learn more:{' '} + + https://nextjs.org/docs/messages/next-prerender-dynamic-metadata + +

+
+ ) + } else { + return ( +
+

+ Runtime data was accessed inside generateMetadata() or + file-based metadata +

+

+ When Document metadata is the only part of a page that cannot be + prerendered Next.js expects you to either make it prerenderable or + make some other part of the page non-prerenderable to avoid + unintentional partially dynamic pages. +

+

To fix this:

+

+ + Remove the Runtime data access from{' '} + generateMetadata() + + . This allows Next.js to statically prerender{' '} + generateMetadata() as part of the HTML document, so + it's instantly visible to the user. +

+

+ or +

+

+ + add connection() inside a {''} + {' '} + somewhere in a Page or Layout. This tells Next.js that the page is + intended to have some non-prerenderable parts. +

+

+ Note that if you are using file-based metadata, such as icons, + inside a route with dynamic params then the only recourse is to make + some other part of the page non-prerenderable. +

+

+ Learn more:{' '} + + https://nextjs.org/docs/messages/next-prerender-dynamic-metadata + +

+
+ ) + } + } + + if (variant === 'runtime') { + return ( +
+

+ Runtime data was accessed outside of {''} +

+

+ This delays the entire page from rendering, resulting in a slow user + experience. Next.js uses this error to ensure your app loads instantly + on every navigation. + cookies(), headers(), and{' '} + searchParams, are examples of Runtime data that can only + come from a user request. +

+

To fix this:

+

+ Provide a fallback UI using {''} around + this component. +

+

+ or +

+

+ + Move the Runtime data access into a deeper component wrapped in{' '} + {''}. + +

+

+ In either case this allows Next.js to stream its contents to the user + when they request the page, while still providing an initial UI that + is prerendered and prefetchable for instant navigations. +

+

+ Learn more:{' '} + + https://nextjs.org/docs/messages/blocking-route + +

+
+ ) + } else { + return ( +
+

+ Data that blocks navigation was accessed outside of {''} +

+

+ This delays the entire page from rendering, resulting in a slow user + experience. Next.js uses this error to ensure your app loads instantly + on every navigation. Uncached data such as fetch(...), + cached data with a low expire time, or connection() are + all examples of data that only resolve on navigation. +

+

To fix this, you can either:

+

+ Provide a fallback UI using {''} around + this component. This allows Next.js to stream its contents to the user + as soon as it's ready, without blocking the rest of the app. +

+

+ or +

+

+ + Move the asynchronous await into a Cache Component ( + "use cache") + + . This allows Next.js to statically prerender the component as part of + the HTML document, so it's instantly visible to the user. +

+

+ Learn more:{' '} + + https://nextjs.org/docs/messages/blocking-route + +

+
+ ) + } } export function getErrorTypeLabel( error: Error, - type: ReadyRuntimeError['type'] + type: ReadyRuntimeError['type'], + errorDetails: ErrorDetails ): ErrorOverlayLayoutProps['errorType'] { + if (errorDetails.type === 'blocking-route') { + return `Blocking Route` + } + if (errorDetails.type === 'dynamic-metadata') { + return `Ambiguous Metadata` + } if (type === 'recoverable') { return `Recoverable ${error.name}` } if (type === 'console') { - const isBlockingPageLoadError = error.message.includes( - 'https://nextjs.org/docs/messages/blocking-route' - ) - if (isBlockingPageLoadError) { - return 'Blocking Route' - } return `Console ${error.name}` } return `Runtime ${error.name}` } -const noErrorDetails = { - hydrationWarning: null, - notes: null, - reactOutputComponentDiff: null, +type ErrorDetails = + | NoErrorDetails + | HydrationErrorDetails + | BlockingRouteErrorDetails + | DynamicMetadataErrorDetails + +type NoErrorDetails = { + type: 'empty' +} + +type HydrationErrorDetails = { + type: 'hydration' + warning: string | null + notes: string | null + reactOutputComponentDiff: string | null } + +type BlockingRouteErrorDetails = { + type: 'blocking-route' + variant: 'navigation' | 'runtime' + refinement: '' | 'generateViewport' +} + +type DynamicMetadataErrorDetails = { + type: 'dynamic-metadata' + variant: 'navigation' | 'runtime' +} + +const noErrorDetails: ErrorDetails = { + type: 'empty', +} + export function useErrorDetails( error: Error | undefined, getSquashedHydrationErrorDetails: (error: Error) => HydrationErrorState | null -): { - hydrationWarning: string | null - notes: string | null - reactOutputComponentDiff: string | null -} { +): ErrorDetails { return useMemo(() => { if (error === undefined) { return noErrorDetails } - const pagesRouterErrorDetails = getSquashedHydrationErrorDetails(error) - if (pagesRouterErrorDetails !== null) { - return { - hydrationWarning: pagesRouterErrorDetails.warning ?? null, - notes: null, - reactOutputComponentDiff: - pagesRouterErrorDetails.reactOutputComponentDiff ?? null, - } + const hydrationErrorDetails = getHydrationErrorDetails( + error, + getSquashedHydrationErrorDetails + ) + if (hydrationErrorDetails) { + return hydrationErrorDetails } - if (!isHydrationError(error)) { - return noErrorDetails + const blockingRouteErrorDetails = getBlockingRouteErrorDetails(error) + if (blockingRouteErrorDetails) { + return blockingRouteErrorDetails } - const { message, notes, diff } = getHydrationErrorStackInfo(error) - if (message === null) { - return noErrorDetails + return noErrorDetails + }, [error, getSquashedHydrationErrorDetails]) +} + +function getHydrationErrorDetails( + error: Error, + getSquashedHydrationErrorDetails: (error: Error) => HydrationErrorState | null +): null | HydrationErrorDetails { + const pagesRouterErrorDetails = getSquashedHydrationErrorDetails(error) + if (pagesRouterErrorDetails !== null) { + return { + type: 'hydration', + warning: pagesRouterErrorDetails.warning ?? null, + notes: null, + reactOutputComponentDiff: + pagesRouterErrorDetails.reactOutputComponentDiff ?? null, + } + } + + if (!isHydrationError(error)) { + return null + } + + const { message, notes, diff } = getHydrationErrorStackInfo(error) + if (message === null) { + return null + } + + return { + type: 'hydration', + warning: message, + notes, + reactOutputComponentDiff: diff, + } +} + +function getBlockingRouteErrorDetails(error: Error): null | ErrorDetails { + const isBlockingPageLoadError = error.message.includes('/blocking-route') + + if (isBlockingPageLoadError) { + const isRuntimeData = error.message.includes('cookies()') + + return { + type: 'blocking-route', + variant: isRuntimeData ? 'runtime' : 'navigation', + refinement: '', } + } + const isDynamicMetadataError = error.message.includes( + '/next-prerender-dynamic-metadata' + ) + if (isDynamicMetadataError) { + const isRuntimeData = error.message.includes('cookies()') return { - hydrationWarning: message, - notes, - reactOutputComponentDiff: diff, + type: 'dynamic-metadata', + variant: isRuntimeData ? 'runtime' : 'navigation', } - }, [error, getSquashedHydrationErrorDetails]) + } + + const isBlockingViewportError = error.message.includes( + '/next-prerender-dynamic-viewport' + ) + if (isBlockingViewportError) { + const isRuntimeData = error.message.includes('cookies()') + return { + type: 'blocking-route', + variant: isRuntimeData ? 'runtime' : 'navigation', + refinement: 'generateViewport', + } + } + + return null } export function Errors({ @@ -176,8 +589,6 @@ export function Errors({ isLoading, errorCode, errorType, - notes, - hydrationWarning, activeIdx, errorDetails, activeError, @@ -279,19 +690,72 @@ Next.js version: ${props.versionInfo.installed} (${process.env.__NEXT_BUNDLER})\ getErrorSource(error) || '' ) + let errorMessage: React.ReactNode + let maybeNotes: React.ReactNode = null + let maybeDiff: React.ReactNode = null + switch (errorDetails.type) { + case 'hydration': + errorMessage = errorDetails.warning ? ( + + ) : ( + + ) + maybeNotes = ( +
+ {errorDetails.notes ? ( + <> +

+ {errorDetails.notes} +

+ + ) : null} + {errorDetails.warning ? ( + + ) : null} +
+ ) + if (errorDetails.reactOutputComponentDiff) { + maybeDiff = ( + + ) + } + break + case 'blocking-route': + errorMessage = ( + + ) + break + case 'dynamic-metadata': + errorMessage = ( + + ) + break + default: + errorMessage = + } + return ( - ) : errorType === 'Blocking Route' ? ( - - ) : ( - - ) - } + errorMessage={errorMessage} onClose={isServerError ? undefined : onClose} debugInfo={debugInfo} error={error} @@ -302,34 +766,8 @@ Next.js version: ${props.versionInfo.installed} (${process.env.__NEXT_BUNDLER})\ generateErrorInfo={generateErrorInfo} {...props} > -
- {notes ? ( - <> -

- {notes} -

- - ) : null} - {hydrationWarning ? ( - - ) : null} -
- - {errorDetails.reactOutputComponentDiff ? ( - - ) : null} + {maybeNotes} + {maybeDiff} }> ( export function pipelineInSequentialTasks( one: () => A, two: (a: A) => B, - three: (b: B) => C | Promise + three: (b: B) => C ): Promise { if (process.env.NEXT_RUNTIME === 'edge') { throw new InvariantError( @@ -46,18 +46,19 @@ export function pipelineInSequentialTasks( ) } else { return new Promise((resolve, reject) => { - let oneResult: A | undefined = undefined + let oneResult: A setTimeout(() => { try { oneResult = one() } catch (err) { clearTimeout(twoId) clearTimeout(threeId) + clearTimeout(fourId) reject(err) } }, 0) - let twoResult: B | undefined = undefined + let twoResult: B const twoId = setTimeout(() => { // if `one` threw, then this timeout would've been cleared, // so if we got here, we're guaranteed to have a value. @@ -65,19 +66,27 @@ export function pipelineInSequentialTasks( twoResult = two(oneResult!) } catch (err) { clearTimeout(threeId) + clearTimeout(fourId) reject(err) } }, 0) + let threeResult: C const threeId = setTimeout(() => { // if `two` threw, then this timeout would've been cleared, // so if we got here, we're guaranteed to have a value. try { - resolve(three(twoResult!)) + threeResult = three(twoResult!) } catch (err) { + clearTimeout(fourId) reject(err) } }, 0) + + // We wait a task before resolving/rejecting + const fourId = setTimeout(() => { + resolve(threeResult) + }, 0) }) } } diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 9a8c32533ad6f..80d8d0f391ced 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -11,6 +11,7 @@ import type { InitialRSCPayload, FlightDataPath, } from '../../shared/lib/app-router-types' +import type { Readable } from 'node:stream' import { workAsyncStorage, type WorkStore, @@ -118,7 +119,7 @@ import { } from './postponed-state' import { isDynamicServerError } from '../../client/components/hooks-server-context' import { - useFlightStream, + getFlightStream, createInlinedDataReadableStream, } from './use-flight-response' import { @@ -139,6 +140,9 @@ import { consumeDynamicAccess, type DynamicAccess, logDisallowedDynamicError, + trackDynamicHoleInRuntimeShell, + trackDynamicHoleInStaticShell, + getStaticShellDisallowedDynamicReasons, } from './dynamic-rendering' import { getClientComponentLoaderMetrics, @@ -676,14 +680,27 @@ async function stagedRenderToReadableStreamWithoutCachesInDev( // which relies on mechanisms we've set up for staged rendering, // so we do a 2-task version (Static -> Dynamic) instead. - const stageController = new StagedRenderingController() + // We aren't doing any validation in this kind of render so we say there + // is not runtime prefetch regardless of whether there is or not + const hasRuntimePrefetch = false + + // We aren't filling caches so we don't need to abort this render, it'll + // stream in a single pass + const abortSignal = null + + const stageController = new StagedRenderingController( + abortSignal, + hasRuntimePrefetch + ) const environmentName = () => { const currentStage = stageController.currentStage switch (currentStage) { + case RenderStage.Before: case RenderStage.Static: return 'Prerender' case RenderStage.Runtime: case RenderStage.Dynamic: + case RenderStage.Abandoned: return 'Server' default: currentStage satisfies never @@ -705,6 +722,7 @@ async function stagedRenderToReadableStreamWithoutCachesInDev( requestStore, scheduleInSequentialTasks, () => { + stageController.advanceStage(RenderStage.Static) return renderToReadableStream( rscPayload, clientReferenceManifest.clientModules, @@ -728,7 +746,8 @@ async function generateDynamicFlightRenderResultWithStagesInDev( req: BaseNextRequest, ctx: AppRenderContext, initialRequestStore: RequestStore, - createRequestStore: (() => RequestStore) | undefined + createRequestStore: (() => RequestStore) | undefined, + devValidatingFallbackParams: OpaqueFallbackRouteParams | null ): Promise { const { htmlRequestId, @@ -736,6 +755,7 @@ async function generateDynamicFlightRenderResultWithStagesInDev( requestId, workStore, componentMod: { createElement }, + url, } = ctx const { @@ -745,6 +765,7 @@ async function generateDynamicFlightRenderResultWithStagesInDev( setCacheStatus, clientReferenceManifest, } = renderOpts + assertClientReferenceManifest(clientReferenceManifest) function onFlightDataRenderError(err: DigestedError) { return onInstrumentationRequestError?.( @@ -758,14 +779,20 @@ async function generateDynamicFlightRenderResultWithStagesInDev( onFlightDataRenderError ) + // If we decide to validate this render we will assign this function when the + // payload is constructed. + let resolveValidation: null | ReturnType[0] = + null + const getPayload = async (requestStore: RequestStore) => { - const payload: RSCPayload & RSCPayloadDevProperties = - await workUnitAsyncStorage.run( - requestStore, - generateDynamicRSCPayload, - ctx, - undefined - ) + const payload: RSCPayload & + RSCPayloadDevProperties & + RSCInitialPayloadPartialDev = await workUnitAsyncStorage.run( + requestStore, + generateDynamicRSCPayload, + ctx, + undefined + ) if (isBypassingCachesInDev(renderOpts, requestStore)) { // Mark the RSC payload to indicate that caches were bypassed in dev. @@ -773,6 +800,19 @@ async function generateDynamicFlightRenderResultWithStagesInDev( payload._bypassCachesInDev = createElement(WarnForBypassCachesInDev, { route: workStore.route, }) + } else if (requestStore.isHmrRefresh) { + // We only validate RSC requests if it is for HMR refreshes since + // we know we will render all the layouts necessary to perform the validation. + // We also must add the canonical URL part of the payload + + // Placing the validation outlet in the payload is safe + // even if we end up discarding a render and restarting, + // because we're not going to wait for the stream to complete, + // so leaving the validation unresolved is fine. + const [validationResolver, validationOutlet] = createValidationOutlet() + resolveValidation = validationResolver + payload._validation = validationOutlet + payload.c = prepareInitialCanonicalUrl(url) } return payload @@ -795,21 +835,57 @@ async function generateDynamicFlightRenderResultWithStagesInDev( setCacheStatus('ready', htmlRequestId) } - const result = await renderWithRestartOnCacheMissInDev( + const { + stream: serverStream, + staticChunks, + runtimeChunks, + dynamicChunks, + staticInterruptReason, + runtimeInterruptReason, + staticStageEndTime, + runtimeStageEndTime, + debugChannel: returnedDebugChannel, + requestStore: finalRequestStore, + } = await renderWithRestartOnCacheMissInDev( ctx, initialRequestStore, createRequestStore, getPayload, onError ) - debugChannel = result.debugChannel - stream = result.stream + + if (resolveValidation) { + let validationDebugChannelClient: Readable | undefined = undefined + if (returnedDebugChannel) { + const [t1, t2] = returnedDebugChannel.clientSide.readable.tee() + returnedDebugChannel.clientSide.readable = t1 + validationDebugChannelClient = nodeStreamFromReadableStream(t2) + } + consoleAsyncStorage.run( + { dim: true }, + spawnStaticShellValidationInDev, + resolveValidation, + staticChunks, + runtimeChunks, + dynamicChunks, + staticInterruptReason, + runtimeInterruptReason, + staticStageEndTime, + runtimeStageEndTime, + ctx, + clientReferenceManifest, + finalRequestStore, + devValidatingFallbackParams, + validationDebugChannelClient + ) + } + + debugChannel = returnedDebugChannel + stream = serverStream } else { // We're either bypassing caches or we can't restart the render. // Do a dynamic render, but with (basic) environment labels. - assertClientReferenceManifest(clientReferenceManifest) - // Set cache status to bypass when specifically bypassing caches in dev if (setCacheStatus) { setCacheStatus('bypass', htmlRequestId) @@ -1613,6 +1689,7 @@ function assertClientReferenceManifest( function App({ reactServerStream, reactDebugStream, + debugEndTime, preinitScripts, clientReferenceManifest, ServerInsertedHTMLProvider, @@ -1620,8 +1697,9 @@ function App({ images, }: { /* eslint-disable @next/internal/no-ambiguous-jsx -- React Client */ - reactServerStream: BinaryStreamOf - reactDebugStream: ReadableStream | undefined + reactServerStream: Readable | BinaryStreamOf + reactDebugStream: Readable | ReadableStream | undefined + debugEndTime: number | undefined preinitScripts: () => void clientReferenceManifest: NonNullable ServerInsertedHTMLProvider: ComponentType<{ @@ -1632,9 +1710,10 @@ function App({ }): JSX.Element { preinitScripts() const response = ReactClient.use( - useFlightStream( + getFlightStream( reactServerStream, reactDebugStream, + debugEndTime, clientReferenceManifest, nonce ) @@ -1680,7 +1759,6 @@ function App({ // consistent for now. function ErrorApp({ reactServerStream, - reactDebugStream, preinitScripts, clientReferenceManifest, ServerInsertedHTMLProvider, @@ -1688,7 +1766,6 @@ function ErrorApp({ images, }: { reactServerStream: BinaryStreamOf - reactDebugStream: ReadableStream | undefined preinitScripts: () => void clientReferenceManifest: NonNullable ServerInsertedHTMLProvider: ComponentType<{ @@ -1700,9 +1777,10 @@ function ErrorApp({ /* eslint-disable @next/internal/no-ambiguous-jsx -- React Client */ preinitScripts() const response = ReactClient.use( - useFlightStream( + getFlightStream( reactServerStream, - reactDebugStream, + undefined, + undefined, clientReferenceManifest, nonce ) @@ -2150,7 +2228,8 @@ async function renderToHTMLOrFlightImpl( req, ctx, requestStore, - createRequestStore + createRequestStore, + devValidatingFallbackParams ) } else { return generateDynamicFlightRenderResult(req, ctx, requestStore) @@ -2444,6 +2523,10 @@ type RSCPayloadDevProperties = { _bypassCachesInDev?: ReactNode } +type RSCInitialPayloadPartialDev = { + c?: InitialRSCPayload['c'] +} + async function renderToStream( requestStore: RequestStore, req: BaseNextRequest, @@ -2602,11 +2685,6 @@ async function renderToStream( ctx, res.statusCode === 404 ) - // Placing the validation outlet in the payload is safe - // even if we end up discarding a render and restarting, - // because we're not going to wait for the stream to complete, - // so leaving the validation unresolved is fine. - payload._validation = validationOutlet if (isBypassingCachesInDev(renderOpts, requestStore)) { // Mark the RSC payload to indicate that caches were bypassed in dev. @@ -2618,6 +2696,12 @@ async function renderToStream( payload._bypassCachesInDev = createElement(WarnForBypassCachesInDev, { route: workStore.route, }) + } else { + // Placing the validation outlet in the payload is safe + // even if we end up discarding a render and restarting, + // because we're not going to wait for the stream to complete, + // so leaving the validation unresolved is fine. + payload._validation = validationOutlet } return payload @@ -2633,6 +2717,13 @@ async function renderToStream( ) { const { stream: serverStream, + staticChunks, + runtimeChunks, + dynamicChunks, + staticInterruptReason, + runtimeInterruptReason, + staticStageEndTime, + runtimeStageEndTime, debugChannel: returnedDebugChannel, requestStore: finalRequestStore, } = await renderWithRestartOnCacheMissInDev( @@ -2643,6 +2734,31 @@ async function renderToStream( serverComponentsErrorHandler ) + let validationDebugChannelClient: Readable | undefined = undefined + if (returnedDebugChannel) { + const [t1, t2] = returnedDebugChannel.clientSide.readable.tee() + returnedDebugChannel.clientSide.readable = t1 + validationDebugChannelClient = nodeStreamFromReadableStream(t2) + } + + consoleAsyncStorage.run( + { dim: true }, + spawnStaticShellValidationInDev, + resolveValidation, + staticChunks, + runtimeChunks, + dynamicChunks, + staticInterruptReason, + runtimeInterruptReason, + staticStageEndTime, + runtimeStageEndTime, + ctx, + clientReferenceManifest, + finalRequestStore, + devValidatingFallbackParams, + validationDebugChannelClient + ) + reactServerResult = new ReactServerResult(serverStream) requestStore = finalRequestStore debugChannel = returnedDebugChannel @@ -2679,21 +2795,6 @@ async function renderToStream( requestId ) } - - // TODO(restart-on-cache-miss): - // This can probably be optimized to do less work, - // because we've already made sure that we have warm caches. - consoleAsyncStorage.run( - { dim: true }, - spawnDynamicValidationInDev, - resolveValidation, - tree, - ctx, - res.statusCode === 404, - clientReferenceManifest, - requestStore, - devValidatingFallbackParams - ) } else { // This is a dynamic render. We don't do dynamic tracking because we're not prerendering const RSCPayload: RSCPayload & RSCPayloadDevProperties = @@ -2771,6 +2872,7 @@ async function renderToStream( { const currentStage = requestStore.stagedRendering!.currentStage switch (currentStage) { + case RenderStage.Before: case RenderStage.Static: return 'Prerender' case RenderStage.Runtime: return hasRuntimePrefetch ? 'Prefetch' : 'Prefetchable' case RenderStage.Dynamic: + case RenderStage.Abandoned: return 'Server' default: currentStage satisfies never @@ -3116,7 +3220,8 @@ async function renderWithRestartOnCacheMissInDev( const initialReactController = new AbortController() const initialDataController = new AbortController() // Controls hanging promises we create const initialStageController = new StagedRenderingController( - initialDataController.signal + initialDataController.signal, + true ) requestStore.prerenderResumeDataCache = prerenderResumeDataCache @@ -3134,13 +3239,22 @@ async function renderWithRestartOnCacheMissInDev( let debugChannel = setReactDebugChannel && createDebugChannel() + const staticChunks: Array = [] + const runtimeChunks: Array = [] + const dynamicChunks: Array = [] + + // Note: The stage controller starts out in the `Before` stage, + // where sync IO does not cause aborts, so it's okay if it happens before render. const initialRscPayload = await getPayload(requestStore) + const maybeInitialServerStream = await workUnitAsyncStorage.run( requestStore, () => pipelineInSequentialTasks( () => { // Static stage + initialStageController.advanceStage(RenderStage.Static) + const stream = ComponentMod.renderToReadableStream( initialRscPayload, clientReferenceManifest.clientModules, @@ -3158,36 +3272,65 @@ async function renderWithRestartOnCacheMissInDev( initialReactController.signal.addEventListener('abort', () => { initialDataController.abort(initialReactController.signal.reason) }) - return stream + + const [continuationStream, accumulatingStream] = stream.tee() + accumulateStreamChunks( + accumulatingStream, + staticChunks, + runtimeChunks, + dynamicChunks, + initialStageController, + initialDataController.signal + ) + return continuationStream }, (stream) => { // Runtime stage - initialStageController.advanceStage(RenderStage.Runtime) - // If we had a cache miss in the static stage, we'll have to disard this stream + if (initialStageController.currentStage === RenderStage.Abandoned) { + // If we abandoned the render in the static stage, we won't proceed further. + return null + } + + // If we had a cache miss in the static stage, we'll have to discard this stream // and render again once the caches are warm. + // If we already advanced stages we similarly had sync IO that might be from module loading + // and need to render again once the caches are warm. if (cacheSignal.hasPendingReads()) { + // Regardless of whether we are going to abandon this + // render we need the unblock runtime b/c it's essential + // filling caches. + initialStageController.abandonRender() return null } - // If there's no cache misses, we'll continue rendering, - // and see if there's any cache misses in the runtime stage. + initialStageController.advanceStage(RenderStage.Runtime) return stream }, - async (maybeStream) => { + async (stream) => { // Dynamic stage + if ( + stream === null || + initialStageController.currentStage === RenderStage.Abandoned + ) { + // If we abandoned the render in the static or runtime stage, we won't proceed further. + return null + } // If we had cache misses in either of the previous stages, // then we'll only use this render for filling caches. // We won't advance the stage, and thus leave dynamic APIs hanging, // because they won't be cached anyway, so it'd be wasted work. - if (maybeStream === null || cacheSignal.hasPendingReads()) { + if (cacheSignal.hasPendingReads()) { + initialStageController.abandonRender() return null } - // If there's no cache misses, we'll use this render, so let it advance to the dynamic stage. + // Regardless of whether we are going to abandon this + // render we need the unblock runtime b/c it's essential + // filling caches. initialStageController.advanceStage(RenderStage.Dynamic) - return maybeStream + return stream } ) ) @@ -3196,6 +3339,14 @@ async function renderWithRestartOnCacheMissInDev( // No cache misses. We can use the stream as is. return { stream: maybeInitialServerStream, + staticChunks, + runtimeChunks, + dynamicChunks, + staticInterruptReason: initialStageController.getStaticInterruptReason(), + runtimeInterruptReason: + initialStageController.getRuntimeInterruptReason(), + staticStageEndTime: initialStageController.getStaticStageEndTime(), + runtimeStageEndTime: initialStageController.getRuntimeStageEndTime(), debugChannel, requestStore, } @@ -3224,7 +3375,13 @@ async function renderWithRestartOnCacheMissInDev( // The initial render acted as a prospective render to warm the caches. requestStore = createRequestStore() - const finalStageController = new StagedRenderingController() + // We are going to render this pass all the way through because we've already + // filled any caches so we won't be aborting this time. + const abortSignal = null + const finalStageController = new StagedRenderingController( + abortSignal, + hasRuntimePrefetch + ) // We've filled the caches, so now we can render as usual, // without any cache-filling mechanics. @@ -3245,12 +3402,23 @@ async function renderWithRestartOnCacheMissInDev( // We're not using it, so we need to create a new one. debugChannel = setReactDebugChannel && createDebugChannel() + // We had a cache miss and need to restart after filling caches. Let's clear out the + // staticChunks and runtimeChunks we previously accumulated + staticChunks.length = 0 + runtimeChunks.length = 0 + dynamicChunks.length = 0 + + // Note: The stage controller starts out in the `Before` stage, + // where sync IO does not cause aborts, so it's okay if it happens before render. const finalRscPayload = await getPayload(requestStore) + const finalServerStream = await workUnitAsyncStorage.run(requestStore, () => pipelineInSequentialTasks( () => { // Static stage - return ComponentMod.renderToReadableStream( + finalStageController.advanceStage(RenderStage.Static) + + const stream = ComponentMod.renderToReadableStream( finalRscPayload, clientReferenceManifest.clientModules, { @@ -3260,6 +3428,17 @@ async function renderWithRestartOnCacheMissInDev( debugChannel: debugChannel?.serverSide, } ) + + const [continuationStream, accumulatingStream] = stream.tee() + accumulateStreamChunks( + accumulatingStream, + staticChunks, + runtimeChunks, + dynamicChunks, + finalStageController, + null + ) + return continuationStream }, (stream) => { // Runtime stage @@ -3280,11 +3459,63 @@ async function renderWithRestartOnCacheMissInDev( return { stream: finalServerStream, + staticChunks, + runtimeChunks, + dynamicChunks, + staticInterruptReason: finalStageController.getStaticInterruptReason(), + runtimeInterruptReason: finalStageController.getRuntimeInterruptReason(), + staticStageEndTime: finalStageController.getStaticStageEndTime(), + runtimeStageEndTime: finalStageController.getRuntimeStageEndTime(), debugChannel, requestStore, } } +async function accumulateStreamChunks( + stream: ReadableStream, + staticTarget: Array, + runtimeTarget: Array, + dynamicTarget: Array, + stageController: StagedRenderingController, + signal: AbortSignal | null +): Promise { + const reader = stream.getReader() + + let cancelled = false + function cancel() { + if (!cancelled) { + cancelled = true + reader.cancel() + } + } + + if (signal) { + signal.addEventListener('abort', cancel, { once: true }) + } + + try { + while (!cancelled) { + const { done, value } = await reader.read() + if (done) { + cancel() + break + } + switch (stageController.currentStage) { + case RenderStage.Static: + staticTarget.push(value) + // fall through + case RenderStage.Runtime: + runtimeTarget.push(value) + // fall through + default: + dynamicTarget.push(value) + } + } + } catch { + // When we release the lock we may reject the read + } +} + function createAsyncApiPromisesInDev( stagedRendering: StagedRenderingController, cookies: RequestStore['cookies'], @@ -3348,7 +3579,7 @@ function createDebugChannel(): DebugChannelPair | undefined { let readableController: ReadableStreamDefaultController | undefined - const clientSideReadable = new ReadableStream({ + let clientSideReadable = new ReadableStream({ start(controller) { readableController = controller }, @@ -3368,9 +3599,7 @@ function createDebugChannel(): DebugChannelPair | undefined { }, }), }, - clientSide: { - readable: clientSideReadable, - }, + clientSide: { readable: clientSideReadable }, } } @@ -3388,31 +3617,33 @@ function createValidationOutlet() { * prerender semantics to prerenderToStream and should update it * in conjunction with any changes to that function. */ -async function spawnDynamicValidationInDev( +async function spawnStaticShellValidationInDev( resolveValidation: (validatingElement: ReactNode) => void, - tree: LoaderTree, + staticServerChunks: Array, + runtimeServerChunks: Array, + dynamicServerChunks: Array, + staticInterruptReason: Error | null, + runtimeInterruptReason: Error | null, + staticStageEndTime: number, + runtimeStageEndTime: number, ctx: AppRenderContext, - isNotFound: boolean, clientReferenceManifest: NonNullable, requestStore: RequestStore, - fallbackRouteParams: OpaqueFallbackRouteParams | null + fallbackRouteParams: OpaqueFallbackRouteParams | null, + debugChannelClient: Readable | undefined ): Promise { + // TODO replace this with a delay on the entire dev render once the result is propagated + // via the websocket and not the main render itself + await new Promise((r) => setTimeout(r, 2000)) const { componentMod: ComponentMod, getDynamicParamFromSegment, - implicitTags, - nonce, renderOpts, workStore, } = ctx const { allowEmptyStaticShell = false } = renderOpts - // These values are placeholder values for this validating render - // that are provided during the actual prerenderToStream. - const preinitScripts = () => {} - const { ServerInsertedHTMLProvider } = createServerInsertedHTML() - const rootParams = getRootParams( ComponentMod.routeModule.userland.loaderTree, getDynamicParamFromSegment @@ -3422,107 +3653,168 @@ async function spawnDynamicValidationInDev( NEXT_HMR_REFRESH_HASH_COOKIE )?.value - // The prerender controller represents the lifetime of the prerender. It will - // be aborted when a task is complete or a synchronously aborting API is - // called. Notably, during prospective prerenders, this does not actually - // terminate the prerender itself, which will continue until all caches are - // filled. - const initialServerPrerenderController = new AbortController() + const { createElement } = ComponentMod - // This controller is used to abort the React prerender. - const initialServerReactController = new AbortController() - - // This controller represents the lifetime of the React prerender. Its signal - // can be used for any I/O operation to abort the I/O and/or to reject, when - // prerendering aborts. This includes our own hanging promises for accessing - // request data, and for fetch calls. It might be replaced in the future by - // React.cacheSignal(). It's aborted after the React controller, so that no - // pending I/O can register abort listeners that are called before React's - // abort listener is called. This ensures that pending I/O is not rejected too - // early when aborting the prerender. Notably, during the prospective - // prerender, it is different from the prerender controller because we don't - // want to end the React prerender until all caches are filled. - const initialServerRenderController = new AbortController() + // We don't need to continue the prerender process if we already + // detected invalid dynamic usage in the initial prerender phase. + const { invalidDynamicUsageError } = workStore + if (invalidDynamicUsageError) { + resolveValidation( + createElement(ReportValidation, { + messages: [invalidDynamicUsageError], + }) + ) + return + } - // The cacheSignal helps us track whether caches are still filling or we are - // ready to cut the render off. - const cacheSignal = new CacheSignal() + if (staticInterruptReason) { + resolveValidation( + createElement(ReportValidation, { + messages: [staticInterruptReason], + }) + ) + return + } - const { createElement } = ComponentMod + if (runtimeInterruptReason) { + resolveValidation( + createElement(ReportValidation, { + messages: [runtimeInterruptReason], + }) + ) + return + } - // The resume data cache here should use a fresh instance as it's - // performing a fresh prerender. If we get to implementing the - // prerendering of an already prerendered page, we should use the passed - // resume data cache instead. - const prerenderResumeDataCache = createPrerenderResumeDataCache() - const initialServerPayloadPrerenderStore: PrerenderStore = { - type: 'prerender', - phase: 'render', + // First we warmup SSR with the runtime chunks. This ensures that when we do + // the full prerender pass with dynamic tracking module loading won't + // interrupt the prerender and can properly observe the entire content + await warmupModuleCacheForRuntimeValidationInDev( + runtimeServerChunks, + dynamicServerChunks, rootParams, fallbackRouteParams, - implicitTags, - // While this render signal isn't going to be used to abort a React render while getting the RSC payload - // various request data APIs bind to this controller to reject after completion. - renderSignal: initialServerRenderController.signal, - // When we generate the RSC payload we might abort this controller due to sync IO - // but we don't actually care about sync IO in this phase so we use a throw away controller - // that isn't connected to anything - controller: new AbortController(), - // During the initial prerender we need to track all cache reads to ensure - // we render long enough to fill every cache it is possible to visit during - // the final prerender. - cacheSignal, - dynamicTracking: null, allowEmptyStaticShell, - revalidate: INFINITE_CACHE, - expire: INFINITE_CACHE, - stale: INFINITE_CACHE, - tags: [...implicitTags.tags], - prerenderResumeDataCache, - renderResumeDataCache: null, + ctx, + clientReferenceManifest + ) + + let debugChunks: Uint8Array[] | null = null + if (debugChannelClient) { + debugChunks = [] + debugChannelClient.on('data', (c) => debugChunks!.push(c)) + } + + const runtimeResult = await validateStagedShell( + runtimeServerChunks, + dynamicServerChunks, + debugChunks, + runtimeStageEndTime, + rootParams, + fallbackRouteParams, + allowEmptyStaticShell, + ctx, + clientReferenceManifest, hmrRefreshHash, + trackDynamicHoleInRuntimeShell + ) + + if (runtimeResult.length > 0) { + // We have something to report from the runtime validation + // We can skip the static validation + resolveValidation( + createElement(ReportValidation, { messages: runtimeResult }) + ) + return } - // We're not going to use the result of this render because the only time it could be used - // is if it completes in a microtask and that's likely very rare for any non-trivial app - const initialServerPayload = await workUnitAsyncStorage.run( - initialServerPayloadPrerenderStore, - getRSCPayload, - tree, + const staticResult = await validateStagedShell( + staticServerChunks, + dynamicServerChunks, + debugChunks, + staticStageEndTime, + rootParams, + fallbackRouteParams, + allowEmptyStaticShell, ctx, - isNotFound + clientReferenceManifest, + hmrRefreshHash, + trackDynamicHoleInStaticShell ) - const initialServerPrerenderStore: PrerenderStore = { - type: 'prerender', + // We always resolve with whatever results we got. It might be empty in which + // case there will be nothing to report once + resolveValidation(createElement(ReportValidation, { messages: staticResult })) + + return +} + +async function warmupModuleCacheForRuntimeValidationInDev( + runtimeServerChunks: Array, + allServerChunks: Array, + rootParams: Params, + fallbackRouteParams: OpaqueFallbackRouteParams | null, + allowEmptyStaticShell: boolean, + ctx: AppRenderContext, + clientReferenceManifest: NonNullable +) { + const { implicitTags, nonce, workStore } = ctx + + // Warmup SSR + const initialClientPrerenderController = new AbortController() + const initialClientReactController = new AbortController() + const initialClientRenderController = new AbortController() + + const preinitScripts = () => {} + const { ServerInsertedHTMLProvider } = createServerInsertedHTML() + + const initialClientPrerenderStore: PrerenderStore = { + type: 'prerender-client', phase: 'render', rootParams, fallbackRouteParams, implicitTags, - renderSignal: initialServerRenderController.signal, - controller: initialServerPrerenderController, - // During the initial prerender we need to track all cache reads to ensure - // we render long enough to fill every cache it is possible to visit during - // the final prerender. - cacheSignal, + renderSignal: initialClientRenderController.signal, + controller: initialClientPrerenderController, + // For HTML Generation the only cache tracked activity + // is module loading, which has it's own cache signal + cacheSignal: null, dynamicTracking: null, allowEmptyStaticShell, revalidate: INFINITE_CACHE, expire: INFINITE_CACHE, stale: INFINITE_CACHE, tags: [...implicitTags.tags], - prerenderResumeDataCache, + // TODO should this be removed from client stores? + prerenderResumeDataCache: null, renderResumeDataCache: null, - hmrRefreshHash, + hmrRefreshHash: undefined, } - const pendingInitialServerResult = workUnitAsyncStorage.run( - initialServerPrerenderStore, - ComponentMod.prerender, - initialServerPayload, - clientReferenceManifest.clientModules, + const runtimeServerStream = createNodeStreamFromChunks( + runtimeServerChunks, + allServerChunks, + initialClientReactController.signal + ) + + const prerender = ( + require('react-dom/static') as typeof import('react-dom/static') + ).prerender + const pendingInitialClientResult = workUnitAsyncStorage.run( + initialClientPrerenderStore, + prerender, + // eslint-disable-next-line @next/internal/no-ambiguous-jsx -- React Client + , { - filterStackFrame, + signal: initialClientReactController.signal, onError: (err) => { const digest = getDigestForWellKnownError(err) @@ -3536,67 +3828,37 @@ async function spawnDynamicValidationInDev( return undefined } - if (initialServerPrerenderController.signal.aborted) { - // The render aborted before this error was handled which indicates - // the error is caused by unfinished components within the render - return + if (initialClientReactController.signal.aborted) { + // These are expected errors that might error the prerender. we ignore them. } else if ( process.env.NEXT_DEBUG_BUILD || process.env.__NEXT_VERBOSE_LOGGING ) { + // 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, workStore.route) } }, - // we don't care to track postpones during the prospective render because we need - // to always do a final render anyway - onPostpone: undefined, - // We don't want to stop rendering until the cacheSignal is complete so we pass - // a different signal to this render call than is used by dynamic APIs to signify - // transitioning out of the prerender environment - signal: initialServerReactController.signal, + // We don't need bootstrap scripts in this prerender + // bootstrapScripts: [bootstrapScript], } ) // The listener to abort our own render controller must be added after React - // has added its listener, to ensure that pending I/O is not aborted/rejected - // too early. - initialServerReactController.signal.addEventListener( + // has added its listener, to ensure that pending I/O is not + // aborted/rejected too early. + initialClientReactController.signal.addEventListener( 'abort', () => { - initialServerRenderController.abort() + initialClientRenderController.abort() }, { once: true } ) - // Wait for all caches to be finished filling and for async imports to resolve - trackPendingModules(cacheSignal) - await cacheSignal.cacheReady() - - initialServerReactController.abort() - - // We don't need to continue the prerender process if we already - // detected invalid dynamic usage in the initial prerender phase. - const { invalidDynamicUsageError } = workStore - if (invalidDynamicUsageError) { - resolveValidation( - createElement(LogSafely, { - fn: () => { - console.error(invalidDynamicUsageError) - }, - }) - ) - return - } - - let initialServerResult - try { - initialServerResult = await createReactServerPrerenderResult( - pendingInitialServerResult - ) - } catch (err) { + pendingInitialClientResult.catch((err) => { if ( - initialServerReactController.signal.aborted || - initialServerPrerenderController.signal.aborted + initialClientReactController.signal.aborted || + isPrerenderInterruptedError(err) ) { // These are expected errors that might error the prerender. we ignore them. } else if ( @@ -3607,235 +3869,41 @@ async function spawnDynamicValidationInDev( // it can be useful for debugging Next.js itself to get visibility here when needed printDebugThrownValueForProspectiveRender(err, workStore.route) } - } - - if (initialServerResult) { - const initialClientPrerenderController = new AbortController() - const initialClientReactController = new AbortController() - const initialClientRenderController = new AbortController() - - const initialClientPrerenderStore: PrerenderStore = { - type: 'prerender-client', - phase: 'render', - rootParams, - fallbackRouteParams, - implicitTags, - renderSignal: initialClientRenderController.signal, - controller: initialClientPrerenderController, - // For HTML Generation the only cache tracked activity - // is module loading, which has it's own cache signal - cacheSignal: null, - dynamicTracking: null, - allowEmptyStaticShell, - revalidate: INFINITE_CACHE, - expire: INFINITE_CACHE, - stale: INFINITE_CACHE, - tags: [...implicitTags.tags], - prerenderResumeDataCache, - renderResumeDataCache: null, - hmrRefreshHash: undefined, - } - - const prerender = ( - require('react-dom/static') as typeof import('react-dom/static') - ).prerender - const pendingInitialClientResult = workUnitAsyncStorage.run( - initialClientPrerenderStore, - prerender, - // eslint-disable-next-line @next/internal/no-ambiguous-jsx -- React Client - , - { - signal: initialClientReactController.signal, - onError: (err) => { - const digest = getDigestForWellKnownError(err) - - if (digest) { - return digest - } - - if (isReactLargeShellError(err)) { - // TODO: Aggregate - console.error(err) - return undefined - } - - if (initialClientReactController.signal.aborted) { - // These are expected errors that might error the prerender. we ignore them. - } else if ( - process.env.NEXT_DEBUG_BUILD || - process.env.__NEXT_VERBOSE_LOGGING - ) { - // 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, workStore.route) - } - }, - // We don't need bootstrap scripts in this prerender - // bootstrapScripts: [bootstrapScript], - } - ) - - // The listener to abort our own render controller must be added after React - // has added its listener, to ensure that pending I/O is not - // aborted/rejected too early. - initialClientReactController.signal.addEventListener( - 'abort', - () => { - initialClientRenderController.abort() - }, - { once: true } - ) - - pendingInitialClientResult.catch((err) => { - if ( - initialClientReactController.signal.aborted || - isPrerenderInterruptedError(err) - ) { - // These are expected errors that might error the prerender. we ignore them. - } else if ( - process.env.NEXT_DEBUG_BUILD || - process.env.__NEXT_VERBOSE_LOGGING - ) { - // 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, workStore.route) - } - }) - - // This is mostly needed for dynamic `import()`s in client components. - // Promises passed to client were already awaited above (assuming that they came from cached functions) - trackPendingModules(cacheSignal) - await cacheSignal.cacheReady() - initialClientReactController.abort() - } - - const finalServerReactController = new AbortController() - const finalServerRenderController = new AbortController() - - const finalServerPayloadPrerenderStore: PrerenderStore = { - type: 'prerender', - phase: 'render', - rootParams, - fallbackRouteParams, - implicitTags, - // While this render signal isn't going to be used to abort a React render while getting the RSC payload - // various request data APIs bind to this controller to reject after completion. - renderSignal: finalServerRenderController.signal, - // When we generate the RSC payload we might abort this controller due to sync IO - // but we don't actually care about sync IO in this phase so we use a throw away controller - // that isn't connected to anything - controller: new AbortController(), - // All caches we could read must already be filled so no tracking is necessary - cacheSignal: null, - dynamicTracking: null, - allowEmptyStaticShell, - revalidate: INFINITE_CACHE, - expire: INFINITE_CACHE, - stale: INFINITE_CACHE, - tags: [...implicitTags.tags], - prerenderResumeDataCache, - renderResumeDataCache: null, - hmrRefreshHash, - } - - const finalAttemptRSCPayload = await workUnitAsyncStorage.run( - finalServerPayloadPrerenderStore, - getRSCPayload, - tree, - ctx, - isNotFound - ) - - const serverDynamicTracking = createDynamicTrackingState( - false // isDebugDynamicAccesses - ) - - const finalServerPrerenderStore: PrerenderStore = { - type: 'prerender', - phase: 'render', - rootParams, - fallbackRouteParams, - implicitTags, - renderSignal: finalServerRenderController.signal, - controller: finalServerReactController, - // All caches we could read must already be filled so no tracking is necessary - cacheSignal: null, - dynamicTracking: serverDynamicTracking, - allowEmptyStaticShell, - revalidate: INFINITE_CACHE, - expire: INFINITE_CACHE, - stale: INFINITE_CACHE, - tags: [...implicitTags.tags], - prerenderResumeDataCache, - renderResumeDataCache: null, - hmrRefreshHash, - } - - const reactServerResult = await createReactServerPrerenderResult( - prerenderAndAbortInSequentialTasks( - async () => { - const pendingPrerenderResult = workUnitAsyncStorage.run( - // The store to scope - finalServerPrerenderStore, - // The function to run - ComponentMod.prerender, - // ... the arguments for the function to run - finalAttemptRSCPayload, - clientReferenceManifest.clientModules, - { - filterStackFrame, - onError: (err: unknown) => { - if ( - finalServerReactController.signal.aborted && - isPrerenderInterruptedError(err) - ) { - return err.digest - } - - if (isReactLargeShellError(err)) { - // TODO: Aggregate - console.error(err) - return undefined - } - - return getDigestForWellKnownError(err) - }, - signal: finalServerReactController.signal, - } - ) + }) - // The listener to abort our own render controller must be added after - // React has added its listener, to ensure that pending I/O is not - // aborted/rejected too early. - finalServerReactController.signal.addEventListener( - 'abort', - () => { - finalServerRenderController.abort() - }, - { once: true } - ) + // This is mostly needed for dynamic `import()`s in client components. + // Promises passed to client were already awaited above (assuming that they came from cached functions) + const cacheSignal = new CacheSignal() + trackPendingModules(cacheSignal) + await cacheSignal.cacheReady() + initialClientReactController.abort() +} - return pendingPrerenderResult - }, - () => { - finalServerReactController.abort() - } - ) - ) +async function validateStagedShell( + stageChunks: Array, + allServerChunks: Array, + debugChunks: null | Array, + debugEndTime: number | undefined, + rootParams: Params, + fallbackRouteParams: OpaqueFallbackRouteParams | null, + allowEmptyStaticShell: boolean, + ctx: AppRenderContext, + clientReferenceManifest: NonNullable, + hmrRefreshHash: string | undefined, + trackDynamicHole: + | typeof trackDynamicHoleInStaticShell + | typeof trackDynamicHoleInRuntimeShell +): Promise> { + const { implicitTags, nonce, workStore } = ctx const clientDynamicTracking = createDynamicTrackingState( false //isDebugDynamicAccesses ) - const finalClientReactController = new AbortController() - const finalClientRenderController = new AbortController() + const clientReactController = new AbortController() + const clientRenderController = new AbortController() + + const preinitScripts = () => {} + const { ServerInsertedHTMLProvider } = createServerInsertedHTML() const finalClientPrerenderStore: PrerenderStore = { type: 'prerender-client', @@ -3843,8 +3911,8 @@ async function spawnDynamicValidationInDev( rootParams, fallbackRouteParams, implicitTags, - renderSignal: finalClientRenderController.signal, - controller: finalClientReactController, + renderSignal: clientRenderController.signal, + controller: clientReactController, // No APIs require a cacheSignal through the workUnitStore during the HTML prerender cacheSignal: null, dynamicTracking: clientDynamicTracking, @@ -3853,17 +3921,32 @@ async function spawnDynamicValidationInDev( expire: INFINITE_CACHE, stale: INFINITE_CACHE, tags: [...implicitTags.tags], - prerenderResumeDataCache, + // TODO should this be removed from client stores? + prerenderResumeDataCache: null, renderResumeDataCache: null, hmrRefreshHash, } - let dynamicValidation = createDynamicValidationState() + let runtimeDynamicValidation = createDynamicValidationState() + const serverStream = createNodeStreamFromChunks( + stageChunks, + allServerChunks, + clientReactController.signal + ) + + const debugChannelClient = debugChunks + ? createNodeStreamFromChunks( + debugChunks, + debugChunks, + clientReactController.signal + ) + : undefined + + const prerender = ( + require('react-dom/static') as typeof import('react-dom/static') + ).prerender try { - const prerender = ( - require('react-dom/static') as typeof import('react-dom/static') - ).prerender let { prelude: unprocessedPrelude } = await prerenderAndAbortInSequentialTasks( () => { @@ -3872,8 +3955,9 @@ async function spawnDynamicValidationInDev( prerender, // eslint-disable-next-line @next/internal/no-ambiguous-jsx -- React Client , { - signal: finalClientReactController.signal, + signal: clientReactController.signal, onError: (err: unknown, errorInfo: ErrorInfo) => { if ( isPrerenderInterruptedError(err) || - finalClientReactController.signal.aborted + clientReactController.signal.aborted ) { const componentStack = errorInfo.componentStack if (typeof componentStack === 'string') { - trackAllowedDynamicAccess( + trackDynamicHole( workStore, componentStack, - dynamicValidation, + runtimeDynamicValidation, clientDynamicTracking ) } @@ -3915,10 +3999,10 @@ async function spawnDynamicValidationInDev( // The listener to abort our own render controller must be added after // React has added its listener, to ensure that pending I/O is not // aborted/rejected too early. - finalClientReactController.signal.addEventListener( + clientReactController.signal.addEventListener( 'abort', () => { - finalClientRenderController.abort() + clientRenderController.abort() }, { once: true } ) @@ -3926,59 +4010,40 @@ async function spawnDynamicValidationInDev( return pendingFinalClientResult }, () => { - finalClientReactController.abort() + clientReactController.abort() } ) const { preludeIsEmpty } = await processPrelude(unprocessedPrelude) - resolveValidation( - createElement(LogSafely, { - fn: throwIfDisallowedDynamic.bind( - null, - workStore, - preludeIsEmpty ? PreludeState.Empty : PreludeState.Full, - dynamicValidation, - serverDynamicTracking - ), - }) + return getStaticShellDisallowedDynamicReasons( + workStore, + preludeIsEmpty ? PreludeState.Empty : PreludeState.Full, + runtimeDynamicValidation ) } catch (thrownValue) { // Even if the root errors we still want to report any cache components errors // that were discovered before the root errored. - - let loggingFunction = throwIfDisallowedDynamic.bind( - null, + let errors: Array = getStaticShellDisallowedDynamicReasons( workStore, PreludeState.Errored, - dynamicValidation, - serverDynamicTracking + runtimeDynamicValidation ) if (process.env.NEXT_DEBUG_BUILD || process.env.__NEXT_VERBOSE_LOGGING) { - // 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 - const originalLoggingFunction = loggingFunction - loggingFunction = () => { - console.error( - 'During dynamic validation the root of the page errored. The next logged error is the thrown value. It may be a duplicate of errors reported during the normal development mode render.' - ) - console.error(thrownValue) - originalLoggingFunction() - } + errors.unshift( + 'During dynamic validation the root of the page errored. The next logged error is the thrown value. It may be a duplicate of errors reported during the normal development mode render.', + thrownValue + ) } - resolveValidation( - createElement(LogSafely, { - fn: loggingFunction, - }) - ) + return errors } } -async function LogSafely({ fn }: { fn: () => unknown }) { - try { - await fn() - } catch {} +function ReportValidation({ messages }: { messages: Array }): null { + for (const message of messages) { + console.error(message) + } return null } @@ -4392,6 +4457,7 @@ async function prerenderToStream( {}} clientReferenceManifest={clientReferenceManifest} ServerInsertedHTMLProvider={ServerInsertedHTMLProvider} @@ -4941,6 +5009,7 @@ async function prerenderToStream( {}} clientReferenceManifest={clientReferenceManifest} ServerInsertedHTMLProvider={ServerInsertedHTMLProvider} @@ -5170,6 +5240,7 @@ async function prerenderToStream( (stream: ReadableStream) { + const reader = stream.getReader() + + const { Readable } = require('node:stream') as typeof import('node:stream') + + return new Readable({ + read() { + reader + .read() + .then(({ done, value }) => { + if (done) { + this.push(null) + } else { + this.push(value) + } + }) + .catch((err) => this.destroy(err)) + }, + }) +} + +function createNodeStreamFromChunks( + partialChunks: Array, + allChunks: Array, + signal: AbortSignal +): Readable { + const { Readable } = require('node:stream') as typeof import('node:stream') + + let nextIndex = 0 + + const readable = new Readable({ + read() { + while (nextIndex < partialChunks.length) { + this.push(partialChunks[nextIndex]) + nextIndex++ + } + }, + }) + + signal.addEventListener( + 'abort', + () => { + // Flush any remaining chunks from the original set + while (nextIndex < partialChunks.length) { + readable.push(partialChunks[nextIndex]) + nextIndex++ + } + // Flush all chunks since we're now aborted and can't schedule + // any new work but these chunks might unblock debugInfo + while (nextIndex < allChunks.length) { + readable.push(allChunks[nextIndex]) + nextIndex++ + } + + setImmediate(() => { + readable.push(null) + }) + }, + { once: true } + ) + + return readable +} diff --git a/packages/next/src/server/app-render/dynamic-rendering.ts b/packages/next/src/server/app-render/dynamic-rendering.ts index 7c9f72ef381dc..b986b37131b47 100644 --- a/packages/next/src/server/app-render/dynamic-rendering.ts +++ b/packages/next/src/server/app-render/dynamic-rendering.ts @@ -23,7 +23,6 @@ import type { WorkStore } from '../app-render/work-async-storage.external' import type { WorkUnitStore, - RequestStore, PrerenderStoreLegacy, PrerenderStoreModern, PrerenderStoreModernRuntime, @@ -50,7 +49,6 @@ import { import { scheduleOnNextTick } from '../../lib/scheduler' import { BailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr' import { InvariantError } from '../../shared/lib/invariant-error' -import { RenderStage } from './staged-rendering' const hasPostpone = typeof React.unstable_postpone === 'function' @@ -87,6 +85,7 @@ export type DynamicTrackingState = { export type DynamicValidationState = { hasSuspenseAboveBody: boolean hasDynamicMetadata: boolean + dynamicMetadata: null | Error hasDynamicViewport: boolean hasAllowedDynamic: boolean dynamicErrors: Array @@ -106,6 +105,7 @@ export function createDynamicValidationState(): DynamicValidationState { return { hasSuspenseAboveBody: false, hasDynamicMetadata: false, + dynamicMetadata: null, hasDynamicViewport: false, hasAllowedDynamic: false, dynamicErrors: [], @@ -295,18 +295,6 @@ export function abortOnSynchronousPlatformIOAccess( } } -export function trackSynchronousPlatformIOAccessInDev( - requestStore: RequestStore -): void { - // We don't actually have a controller to abort but we do the semantic equivalent by - // advancing the request store out of the prerender stage - if (requestStore.stagedRendering) { - // TODO: error for sync IO in the runtime stage - // (which is not currently covered by the validation render in `spawnDynamicValidationInDev`) - requestStore.stagedRendering.advanceStage(RenderStage.Dynamic) - } -} - /** * use this function when prerendering with cacheComponents. If we are doing a * prospective prerender we don't actually abort because we want to discover @@ -770,6 +758,104 @@ export function trackAllowedDynamicAccess( } } +export function trackDynamicHoleInRuntimeShell( + workStore: WorkStore, + componentStack: string, + dynamicValidation: DynamicValidationState, + clientDynamic: DynamicTrackingState +) { + if (hasOutletRegex.test(componentStack)) { + // We don't need to track that this is dynamic. It is only so when something else is also dynamic. + return + } else if (hasMetadataRegex.test(componentStack)) { + const message = `Route "${workStore.route}": Uncached data or \`connection()\` was accessed inside \`generateMetadata\`. Except for this instance, the page would have been entirely prerenderable which may have been the intended behavior. See more info here: https://nextjs.org/docs/messages/next-prerender-dynamic-metadata` + const error = createErrorWithComponentOrOwnerStack(message, componentStack) + dynamicValidation.dynamicMetadata = error + return + } else if (hasViewportRegex.test(componentStack)) { + const message = `Route "${workStore.route}": Uncached data or \`connection()\` was accessed inside \`generateViewport\`. This delays the entire page from rendering, resulting in a slow user experience. Learn more: Learn more: 'https://nextjs.org/docs/messages/next-prerender-dynamic-viewport'` + const error = createErrorWithComponentOrOwnerStack(message, componentStack) + dynamicValidation.dynamicErrors.push(error) + return + } else if ( + hasSuspenseBeforeRootLayoutWithoutBodyOrImplicitBodyRegex.test( + componentStack + ) + ) { + // For Suspense within body, the prelude wouldn't be empty so it wouldn't violate the empty static shells rule. + // But if you have Suspense above body, the prelude is empty but we allow that because having Suspense + // is an explicit signal from the user that they acknowledge the empty shell and want dynamic rendering. + dynamicValidation.hasAllowedDynamic = true + dynamicValidation.hasSuspenseAboveBody = true + return + } else if (hasSuspenseRegex.test(componentStack)) { + // this error had a Suspense boundary above it so we don't need to report it as a source + // of disallowed + dynamicValidation.hasAllowedDynamic = true + return + } else if (clientDynamic.syncDynamicErrorWithStack) { + // This task was the task that called the sync error. + dynamicValidation.dynamicErrors.push( + clientDynamic.syncDynamicErrorWithStack + ) + return + } else { + const message = `Route "${workStore.route}": Uncached data or \`connection()\` was accessed outside of \`\`. This delays the entire page from rendering, resulting in a slow user experience. Learn more: https://nextjs.org/docs/messages/blocking-route'` + const error = createErrorWithComponentOrOwnerStack(message, componentStack) + dynamicValidation.dynamicErrors.push(error) + return + } +} + +export function trackDynamicHoleInStaticShell( + workStore: WorkStore, + componentStack: string, + dynamicValidation: DynamicValidationState, + clientDynamic: DynamicTrackingState +) { + if (hasOutletRegex.test(componentStack)) { + // We don't need to track that this is dynamic. It is only so when something else is also dynamic. + return + } else if (hasMetadataRegex.test(componentStack)) { + const message = `Route "${workStore.route}": Runtime data such as \`cookies()\`, \`headers()\`, \`params\`, or \`searchParams\` was accessed inside \`generateMetadata\` or you have file-based metadata such as icons that depend on dynamic params segments. Except for this instance, the page would have been entirely prerenderable which may have been the intended behavior. See more info here: https://nextjs.org/docs/messages/next-prerender-dynamic-metadata` + const error = createErrorWithComponentOrOwnerStack(message, componentStack) + dynamicValidation.dynamicMetadata = error + return + } else if (hasViewportRegex.test(componentStack)) { + const message = `Route "${workStore.route}": Runtime data such as \`cookies()\`, \`headers()\`, \`params\`, or \`searchParams\` was accessed inside \`generateViewport\`. This delays the entire page from rendering, resulting in a slow user experience. Learn more: Learn more: 'https://nextjs.org/docs/messages/next-prerender-dynamic-viewport'` + const error = createErrorWithComponentOrOwnerStack(message, componentStack) + dynamicValidation.dynamicErrors.push(error) + return + } else if ( + hasSuspenseBeforeRootLayoutWithoutBodyOrImplicitBodyRegex.test( + componentStack + ) + ) { + // For Suspense within body, the prelude wouldn't be empty so it wouldn't violate the empty static shells rule. + // But if you have Suspense above body, the prelude is empty but we allow that because having Suspense + // is an explicit signal from the user that they acknowledge the empty shell and want dynamic rendering. + dynamicValidation.hasAllowedDynamic = true + dynamicValidation.hasSuspenseAboveBody = true + return + } else if (hasSuspenseRegex.test(componentStack)) { + // this error had a Suspense boundary above it so we don't need to report it as a source + // of disallowed + dynamicValidation.hasAllowedDynamic = true + return + } else if (clientDynamic.syncDynamicErrorWithStack) { + // This task was the task that called the sync error. + dynamicValidation.dynamicErrors.push( + clientDynamic.syncDynamicErrorWithStack + ) + return + } else { + const message = `Route "${workStore.route}": Runtime data such as \`cookies()\`, \`headers()\`, \`params\`, or \`searchParams\` was accessed outside of \`\`. This delays the entire page from rendering, resulting in a slow user experience. Learn more: 'https://nextjs.org/docs/messages/blocking-route'` + const error = createErrorWithComponentOrOwnerStack(message, componentStack) + dynamicValidation.dynamicErrors.push(error) + return + } +} + /** * In dev mode, we prefer using the owner stack, otherwise the provided * component stack is used. @@ -784,7 +870,9 @@ function createErrorWithComponentOrOwnerStack( : null const error = new Error(message) - error.stack = error.name + ': ' + message + (ownerStack ?? componentStack) + // TODO go back to owner stack here if available. This is temporarily using componentStack to get the right + // + error.stack = error.name + ': ' + message + (ownerStack || componentStack) return error } @@ -880,6 +968,51 @@ export function throwIfDisallowedDynamic( } } +export function getStaticShellDisallowedDynamicReasons( + workStore: WorkStore, + prelude: PreludeState, + dynamicValidation: DynamicValidationState +): Array { + if (dynamicValidation.hasSuspenseAboveBody) { + // This route has opted into allowing fully dynamic rendering + // by including a Suspense boundary above the body. In this case + // a lack of a shell is not considered disallowed so we simply return + return [] + } + + if (prelude !== PreludeState.Full) { + // We didn't have any sync bailouts but there may be user code which + // blocked the root. We would have captured these during the prerender + // and can log them here and then terminate the build/validating render + const dynamicErrors = dynamicValidation.dynamicErrors + if (dynamicErrors.length > 0) { + return dynamicErrors + } + + if (prelude === PreludeState.Empty) { + // If we ever get this far then we messed up the tracking of invalid dynamic. + // We still adhere to the constraint that you must produce a shell but invite the + // user to report this as a bug in Next.js. + return [ + new InvariantError( + `Route "${workStore.route}" did not produce a static shell and Next.js was unable to determine a reason.` + ), + ] + } + } else { + // We have a prelude but we might still have dynamic metadata without any other dynamic access + if ( + dynamicValidation.hasAllowedDynamic === false && + dynamicValidation.dynamicErrors.length === 0 && + dynamicValidation.dynamicMetadata + ) { + return [dynamicValidation.dynamicMetadata] + } + } + // We had a non-empty prelude and there are no dynamic holes + return [] +} + export function delayUntilRuntimeStage( prerenderStore: PrerenderStoreModernRuntime, result: Promise diff --git a/packages/next/src/server/app-render/staged-rendering.ts b/packages/next/src/server/app-render/staged-rendering.ts index e31d13fa95bea..51c9b971a8936 100644 --- a/packages/next/src/server/app-render/staged-rendering.ts +++ b/packages/next/src/server/app-render/staged-rendering.ts @@ -2,20 +2,35 @@ import { InvariantError } from '../../shared/lib/invariant-error' import { createPromiseWithResolvers } from '../../shared/lib/promise-with-resolvers' export enum RenderStage { - Static = 1, - Runtime = 2, - Dynamic = 3, + Before = 1, + Static = 2, + Runtime = 3, + Dynamic = 4, + Abandoned = 5, } export type NonStaticRenderStage = RenderStage.Runtime | RenderStage.Dynamic export class StagedRenderingController { - currentStage: RenderStage = RenderStage.Static + currentStage: RenderStage = RenderStage.Before + + staticInterruptReason: Error | null = null + runtimeInterruptReason: Error | null = null + staticStageEndTime: number = Infinity + runtimeStageEndTime: number = Infinity + + private runtimeStageListeners: Array<() => void> = [] + private dynamicStageListeners: Array<() => void> = [] private runtimeStagePromise = createPromiseWithResolvers() private dynamicStagePromise = createPromiseWithResolvers() - constructor(private abortSignal: AbortSignal | null = null) { + private mayAbandon: boolean = false + + constructor( + private abortSignal: AbortSignal | null = null, + private hasRuntimePrefetch: boolean + ) { if (abortSignal) { abortSignal.addEventListener( 'abort', @@ -32,24 +47,171 @@ export class StagedRenderingController { }, { once: true } ) + + this.mayAbandon = true + } + } + + onStage(stage: NonStaticRenderStage, callback: () => void) { + if (this.currentStage >= stage) { + callback() + } else if (stage === RenderStage.Runtime) { + this.runtimeStageListeners.push(callback) + } else if (stage === RenderStage.Dynamic) { + this.dynamicStageListeners.push(callback) + } else { + // This should never happen + throw new InvariantError(`Invalid render stage: ${stage}`) + } + } + + canSyncInterrupt() { + // If we haven't started the render yet, it can't be interrupted. + if (this.currentStage === RenderStage.Before) { + return false + } + + const boundaryStage = this.hasRuntimePrefetch + ? RenderStage.Dynamic + : RenderStage.Runtime + return this.currentStage < boundaryStage + } + + syncInterruptCurrentStageWithReason(reason: Error) { + if (this.currentStage === RenderStage.Before) { + return + } + + // If Sync IO occurs during the initial (abandonable) render, we'll retry it, + // so we want a slightly different flow. + // See the implementation of `abandonRenderImpl` for more explanation. + if (this.mayAbandon) { + return this.abandonRenderImpl() + } + + // If we're in the final render, we cannot abandon it. We need to advance to the Dynamic stage + // and capture the interruption reason. + switch (this.currentStage) { + case RenderStage.Static: { + this.staticInterruptReason = reason + this.advanceStage(RenderStage.Dynamic) + return + } + case RenderStage.Runtime: { + // We only error for Sync IO in the runtime stage if the route + // is configured to use runtime prefetching. + // We do this to reflect the fact that during a runtime prefetch, + // Sync IO aborts aborts the render. + // Note that `canSyncInterrupt` should prevent us from getting here at all + // if runtime prefetching isn't enabled. + if (this.hasRuntimePrefetch) { + this.runtimeInterruptReason = reason + this.advanceStage(RenderStage.Dynamic) + } + return + } + default: + } + } + + getStaticInterruptReason() { + return this.staticInterruptReason + } + + getRuntimeInterruptReason() { + return this.runtimeInterruptReason + } + + getStaticStageEndTime() { + return this.staticStageEndTime + } + + getRuntimeStageEndTime() { + return this.runtimeStageEndTime + } + + abandonRender() { + if (!this.mayAbandon) { + throw new InvariantError( + '`abandonRender` called on a stage controller that cannot be abandoned.' + ) + } + + this.abandonRenderImpl() + } + + private abandonRenderImpl() { + // In staged rendering, only the initial render is abandonable. + // We can abandon the initial render if + // 1. We notice a cache miss, and need to wait for caches to fill + // 2. A sync IO error occurs, and the render should be interrupted + // (this might be a lazy intitialization of a module, + // so we still want to restart in this case and see if it still occurs) + // In either case, we'll be doing another render after this one, + // so we only want to unblock the Runtime stage, not Dynamic, because + // unblocking the dynamic stage would likely lead to wasted (uncached) IO. + const { currentStage } = this + switch (currentStage) { + case RenderStage.Static: { + this.currentStage = RenderStage.Abandoned + this.resolveRuntimeStage() + return + } + case RenderStage.Runtime: { + this.currentStage = RenderStage.Abandoned + return + } + case RenderStage.Dynamic: + case RenderStage.Before: + case RenderStage.Abandoned: + break + default: { + currentStage satisfies never + } } } - advanceStage(stage: NonStaticRenderStage) { + advanceStage( + stage: RenderStage.Static | RenderStage.Runtime | RenderStage.Dynamic + ) { // If we're already at the target stage or beyond, do nothing. // (this can happen e.g. if sync IO advanced us to the dynamic stage) - if (this.currentStage >= stage) { + if (stage <= this.currentStage) { return } + + let currentStage = this.currentStage this.currentStage = stage - // Note that we might be going directly from Static to Dynamic, - // so we need to resolve the runtime stage as well. - if (stage >= RenderStage.Runtime) { - this.runtimeStagePromise.resolve() + + if (currentStage < RenderStage.Runtime && stage >= RenderStage.Runtime) { + this.staticStageEndTime = performance.now() + performance.timeOrigin + this.resolveRuntimeStage() } - if (stage >= RenderStage.Dynamic) { - this.dynamicStagePromise.resolve() + if (currentStage < RenderStage.Dynamic && stage >= RenderStage.Dynamic) { + this.runtimeStageEndTime = performance.now() + performance.timeOrigin + this.resolveDynamicStage() + return + } + } + + /** Fire the `onStage` listeners for the runtime stage and unblock any promises waiting for it. */ + private resolveRuntimeStage() { + const runtimeListeners = this.runtimeStageListeners + for (let i = 0; i < runtimeListeners.length; i++) { + runtimeListeners[i]() + } + runtimeListeners.length = 0 + this.runtimeStagePromise.resolve() + } + + /** Fire the `onStage` listeners for the dynamic stage and unblock any promises waiting for it. */ + private resolveDynamicStage() { + const dynamicListeners = this.dynamicStageListeners + for (let i = 0; i < dynamicListeners.length; i++) { + dynamicListeners[i]() } + dynamicListeners.length = 0 + this.dynamicStagePromise.resolve() } private getStagePromise(stage: NonStaticRenderStage): Promise { diff --git a/packages/next/src/server/app-render/use-flight-response.tsx b/packages/next/src/server/app-render/use-flight-response.tsx index e5f2be0712e3c..33d3b0ded2d67 100644 --- a/packages/next/src/server/app-render/use-flight-response.tsx +++ b/packages/next/src/server/app-render/use-flight-response.tsx @@ -1,5 +1,6 @@ import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight-manifest-plugin' import type { BinaryStreamOf } from './app-render' +import type { Readable } from 'stream' import { htmlEscapeJsonString } from '../htmlescape' import type { DeepReadonly } from '../../shared/lib/deep-readonly' @@ -13,7 +14,10 @@ const INLINE_FLIGHT_PAYLOAD_DATA = 1 const INLINE_FLIGHT_PAYLOAD_FORM_STATE = 2 const INLINE_FLIGHT_PAYLOAD_BINARY = 3 -const flightResponses = new WeakMap, Promise>() +const flightResponses = new WeakMap< + Readable | BinaryStreamOf, + Promise +>() const encoder = new TextEncoder() const findSourceMapURL = @@ -26,9 +30,10 @@ const findSourceMapURL = * Render Flight stream. * This is only used for renderToHTML, the Flight response does not need additional wrappers. */ -export function useFlightStream( - flightStream: BinaryStreamOf, - debugStream: ReadableStream | undefined, +export function getFlightStream( + flightStream: Readable | BinaryStreamOf, + debugStream: Readable | ReadableStream | undefined, + debugEndTime: number | undefined, clientReferenceManifest: DeepReadonly, nonce: string | undefined ): Promise { @@ -38,23 +43,61 @@ export function useFlightStream( return response } - // react-server-dom-webpack/client.edge must not be hoisted for require cache clearing to work correctly - const { createFromReadableStream } = - // eslint-disable-next-line import/no-extraneous-dependencies - require('react-server-dom-webpack/client') as typeof import('react-server-dom-webpack/client') - - const newResponse = createFromReadableStream(flightStream, { - findSourceMapURL, - serverConsumerManifest: { - moduleLoading: clientReferenceManifest.moduleLoading, - moduleMap: isEdgeRuntime - ? clientReferenceManifest.edgeSSRModuleMapping - : clientReferenceManifest.ssrModuleMapping, - serverModuleMap: null, - }, - nonce, - debugChannel: debugStream ? { readable: debugStream } : undefined, - }) + let newResponse: Promise + if (flightStream instanceof ReadableStream) { + // The types of flightStream and debugStream should match. + if (debugStream && !(debugStream instanceof ReadableStream)) { + throw new InvariantError('Expected debug stream to be a ReadableStream') + } + + // react-server-dom-webpack/client.edge must not be hoisted for require cache clearing to work correctly + const { createFromReadableStream } = + // eslint-disable-next-line import/no-extraneous-dependencies + require('react-server-dom-webpack/client') as typeof import('react-server-dom-webpack/client') + + newResponse = createFromReadableStream(flightStream, { + findSourceMapURL, + serverConsumerManifest: { + moduleLoading: clientReferenceManifest.moduleLoading, + moduleMap: isEdgeRuntime + ? clientReferenceManifest.edgeSSRModuleMapping + : clientReferenceManifest.ssrModuleMapping, + serverModuleMap: null, + }, + nonce, + debugChannel: debugStream ? { readable: debugStream } : undefined, + endTime: debugEndTime, + }) + } else { + const { Readable } = require('stream') as typeof import('stream') + + // The types of flightStream and debugStream should match. + if (debugStream && !(debugStream instanceof Readable)) { + throw new InvariantError('Expected debug stream to be a Readable') + } + + // react-server-dom-webpack/client.edge must not be hoisted for require cache clearing to work correctly + const { createFromNodeStream } = + // eslint-disable-next-line import/no-extraneous-dependencies + require('react-server-dom-webpack/client') as typeof import('react-server-dom-webpack/client') + + newResponse = createFromNodeStream( + flightStream, + { + moduleLoading: clientReferenceManifest.moduleLoading, + moduleMap: isEdgeRuntime + ? clientReferenceManifest.edgeSSRModuleMapping + : clientReferenceManifest.ssrModuleMapping, + serverModuleMap: null, + }, + { + findSourceMapURL, + nonce, + debugChannel: debugStream, + endTime: debugEndTime, + } + ) + } // Edge pages are never prerendered so they necessarily cannot have a workUnitStore type // that requires the nextTick behavior. This is why it is safe to access a node only API here @@ -68,7 +111,9 @@ export function useFlightStream( switch (workUnitStore.type) { case 'prerender-client': const responseOnNextTick = new Promise((resolve) => { - process.nextTick(() => resolve(newResponse)) + process.nextTick(() => { + resolve(newResponse) + }) }) flightResponses.set(flightStream, responseOnNextTick) return responseOnNextTick diff --git a/packages/next/src/server/node-environment-extensions/utils.tsx b/packages/next/src/server/node-environment-extensions/utils.tsx index 30213664c9bf0..07bca2ee8e441 100644 --- a/packages/next/src/server/node-environment-extensions/utils.tsx +++ b/packages/next/src/server/node-environment-extensions/utils.tsx @@ -1,10 +1,8 @@ import { workAsyncStorage } from '../app-render/work-async-storage.external' import { workUnitAsyncStorage } from '../app-render/work-unit-async-storage.external' -import { - abortOnSynchronousPlatformIOAccess, - trackSynchronousPlatformIOAccessInDev, -} from '../app-render/dynamic-rendering' +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' @@ -86,7 +84,62 @@ export function io(expression: string, type: ApiType) { } case 'request': if (process.env.NODE_ENV === 'development') { - trackSynchronousPlatformIOAccessInDev(workUnitStore) + 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': diff --git a/packages/next/src/server/web/spec-extension/revalidate.ts b/packages/next/src/server/web/spec-extension/revalidate.ts index 1ca8b6906879b..aa051d7cf3097 100644 --- a/packages/next/src/server/web/spec-extension/revalidate.ts +++ b/packages/next/src/server/web/spec-extension/revalidate.ts @@ -179,6 +179,8 @@ function revalidate( // status being flipped when revalidating a static page with a server // action. workUnitStore.usedDynamic = true + // TODO(restart-on-cache-miss): we should do a sync IO error here in dev + // to match prerender behavior } break default: diff --git a/packages/next/types/$$compiled.internal.d.ts b/packages/next/types/$$compiled.internal.d.ts index 90beeab79d763..ba8444d15c2bb 100644 --- a/packages/next/types/$$compiled.internal.d.ts +++ b/packages/next/types/$$compiled.internal.d.ts @@ -66,6 +66,15 @@ declare module 'react-server-dom-webpack/client' { options?: Options ): Promise + export function createFromNodeStream( + stream: import('node:stream').Readable, + serverConsumerManifest: Options['serverConsumerManifest'], + options?: Omit & { + // For the Node.js client we only support a single-direction debug channel. + debugChannel?: import('node:stream').Readable + } + ): Promise + export function createServerReference( id: string, callServer: CallServerCallback, @@ -106,6 +115,8 @@ declare module 'react-server-dom-webpack/client.browser' { replayConsoleLogs?: boolean temporaryReferences?: TemporaryReferenceSet debugChannel?: { readable?: ReadableStream; writable?: WritableStream } + startTime?: number + endTime?: number } export function createFromFetch( @@ -309,6 +320,8 @@ declare module 'react-server-dom-webpack/client.edge' { replayConsoleLogs?: boolean environmentName?: string debugChannel?: { readable?: ReadableStream } + startTime?: number + endTime?: number } export type EncodeFormActionCallback = ( diff --git a/test/development/app-dir/cache-components-dev-fallback-validation/cache-components-dev-fallback-validation.test.ts b/test/development/app-dir/cache-components-dev-fallback-validation/cache-components-dev-fallback-validation.test.ts index 8f4e2f93b5835..d6d0ecef4f336 100644 --- a/test/development/app-dir/cache-components-dev-fallback-validation/cache-components-dev-fallback-validation.test.ts +++ b/test/development/app-dir/cache-components-dev-fallback-validation/cache-components-dev-fallback-validation.test.ts @@ -52,19 +52,19 @@ describe('Cache Components Fallback Validation', () => { if (isTurbopack) { await expect(browser).toDisplayCollapsedRedbox(` { - "description": "Uncached data was accessed outside of + "description": "Runtime data was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. - To fix this, you can either: + To fix this: - Wrap the component in a boundary. This allows Next.js to stream its contents to the user as soon as it's ready, without blocking the rest of the app. + Provide a fallback UI using around this component. or - Move the asynchronous await into a Cache Component ("use cache"). This allows Next.js to statically prerender the component as part of the HTML document, so it's instantly visible to the user. + Move the Runtime data access into a deeper component wrapped in . - Note that request-specific information — such as params, cookies, and headers — is not available during static prerendering, so must be wrapped in . + In either case this allows Next.js to stream its contents to the user when they request the page, while still providing an initial UI that is prerendered and prefetchable for instant navigations. Learn more: https://nextjs.org/docs/messages/blocking-route", "environmentLabel": "Server", @@ -74,7 +74,7 @@ describe('Cache Components Fallback Validation', () => { | ^", "stack": [ "Page app/partial/[top]/unwrapped/[bottom]/page.tsx (6:26)", - "LogSafely ", + "ReportValidation ", ], } `) @@ -113,19 +113,19 @@ describe('Cache Components Fallback Validation', () => { if (isTurbopack) { await expect(browser).toDisplayCollapsedRedbox(` { - "description": "Uncached data was accessed outside of + "description": "Runtime data was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. - To fix this, you can either: + To fix this: - Wrap the component in a boundary. This allows Next.js to stream its contents to the user as soon as it's ready, without blocking the rest of the app. + Provide a fallback UI using around this component. or - Move the asynchronous await into a Cache Component ("use cache"). This allows Next.js to statically prerender the component as part of the HTML document, so it's instantly visible to the user. + Move the Runtime data access into a deeper component wrapped in . - Note that request-specific information — such as params, cookies, and headers — is not available during static prerendering, so must be wrapped in . + In either case this allows Next.js to stream its contents to the user when they request the page, while still providing an initial UI that is prerendered and prefetchable for instant navigations. Learn more: https://nextjs.org/docs/messages/blocking-route", "environmentLabel": "Server", @@ -135,7 +135,7 @@ describe('Cache Components Fallback Validation', () => { | ^", "stack": [ "Page app/partial/[top]/unwrapped/[bottom]/page.tsx (6:26)", - "LogSafely ", + "ReportValidation ", ], } `) @@ -174,19 +174,19 @@ describe('Cache Components Fallback Validation', () => { if (isTurbopack) { await expect(browser).toDisplayCollapsedRedbox(` { - "description": "Uncached data was accessed outside of + "description": "Runtime data was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. - To fix this, you can either: + To fix this: - Wrap the component in a boundary. This allows Next.js to stream its contents to the user as soon as it's ready, without blocking the rest of the app. + Provide a fallback UI using around this component. or - Move the asynchronous await into a Cache Component ("use cache"). This allows Next.js to statically prerender the component as part of the HTML document, so it's instantly visible to the user. + Move the Runtime data access into a deeper component wrapped in . - Note that request-specific information — such as params, cookies, and headers — is not available during static prerendering, so must be wrapped in . + In either case this allows Next.js to stream its contents to the user when they request the page, while still providing an initial UI that is prerendered and prefetchable for instant navigations. Learn more: https://nextjs.org/docs/messages/blocking-route", "environmentLabel": "Server", @@ -196,7 +196,7 @@ describe('Cache Components Fallback Validation', () => { | ^", "stack": [ "Page app/partial/[top]/unwrapped/[bottom]/page.tsx (6:26)", - "LogSafely ", + "ReportValidation ", ], } `) @@ -239,19 +239,19 @@ describe('Cache Components Fallback Validation', () => { if (isTurbopack) { await expect(browser).toDisplayCollapsedRedbox(` { - "description": "Uncached data was accessed outside of + "description": "Runtime data was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. - To fix this, you can either: + To fix this: - Wrap the component in a boundary. This allows Next.js to stream its contents to the user as soon as it's ready, without blocking the rest of the app. + Provide a fallback UI using around this component. or - Move the asynchronous await into a Cache Component ("use cache"). This allows Next.js to statically prerender the component as part of the HTML document, so it's instantly visible to the user. + Move the Runtime data access into a deeper component wrapped in . - Note that request-specific information — such as params, cookies, and headers — is not available during static prerendering, so must be wrapped in . + In either case this allows Next.js to stream its contents to the user when they request the page, while still providing an initial UI that is prerendered and prefetchable for instant navigations. Learn more: https://nextjs.org/docs/messages/blocking-route", "environmentLabel": "Server", @@ -261,7 +261,7 @@ describe('Cache Components Fallback Validation', () => { | ^", "stack": [ "Layout app/none/[top]/wrapped/layout.tsx (10:3)", - "LogSafely ", + "ReportValidation ", ], } `) @@ -300,19 +300,19 @@ describe('Cache Components Fallback Validation', () => { if (isTurbopack) { await expect(browser).toDisplayCollapsedRedbox(` { - "description": "Uncached data was accessed outside of + "description": "Runtime data was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. - To fix this, you can either: + To fix this: - Wrap the component in a boundary. This allows Next.js to stream its contents to the user as soon as it's ready, without blocking the rest of the app. + Provide a fallback UI using around this component. or - Move the asynchronous await into a Cache Component ("use cache"). This allows Next.js to statically prerender the component as part of the HTML document, so it's instantly visible to the user. + Move the Runtime data access into a deeper component wrapped in . - Note that request-specific information — such as params, cookies, and headers — is not available during static prerendering, so must be wrapped in . + In either case this allows Next.js to stream its contents to the user when they request the page, while still providing an initial UI that is prerendered and prefetchable for instant navigations. Learn more: https://nextjs.org/docs/messages/blocking-route", "environmentLabel": "Server", @@ -322,7 +322,7 @@ describe('Cache Components Fallback Validation', () => { | ^", "stack": [ "Layout app/none/[top]/wrapped/layout.tsx (10:3)", - "LogSafely ", + "ReportValidation ", ], } `) @@ -361,19 +361,19 @@ describe('Cache Components Fallback Validation', () => { if (isTurbopack) { await expect(browser).toDisplayCollapsedRedbox(` { - "description": "Uncached data was accessed outside of + "description": "Runtime data was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. - To fix this, you can either: + To fix this: - Wrap the component in a boundary. This allows Next.js to stream its contents to the user as soon as it's ready, without blocking the rest of the app. + Provide a fallback UI using around this component. or - Move the asynchronous await into a Cache Component ("use cache"). This allows Next.js to statically prerender the component as part of the HTML document, so it's instantly visible to the user. + Move the Runtime data access into a deeper component wrapped in . - Note that request-specific information — such as params, cookies, and headers — is not available during static prerendering, so must be wrapped in . + In either case this allows Next.js to stream its contents to the user when they request the page, while still providing an initial UI that is prerendered and prefetchable for instant navigations. Learn more: https://nextjs.org/docs/messages/blocking-route", "environmentLabel": "Server", @@ -383,7 +383,7 @@ describe('Cache Components Fallback Validation', () => { | ^", "stack": [ "Layout app/none/[top]/wrapped/layout.tsx (10:3)", - "LogSafely ", + "ReportValidation ", ], } `) @@ -422,19 +422,19 @@ describe('Cache Components Fallback Validation', () => { if (isTurbopack) { await expect(browser).toDisplayCollapsedRedbox(` { - "description": "Uncached data was accessed outside of + "description": "Runtime data was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. - To fix this, you can either: + To fix this: - Wrap the component in a boundary. This allows Next.js to stream its contents to the user as soon as it's ready, without blocking the rest of the app. + Provide a fallback UI using around this component. or - Move the asynchronous await into a Cache Component ("use cache"). This allows Next.js to statically prerender the component as part of the HTML document, so it's instantly visible to the user. + Move the Runtime data access into a deeper component wrapped in . - Note that request-specific information — such as params, cookies, and headers — is not available during static prerendering, so must be wrapped in . + In either case this allows Next.js to stream its contents to the user when they request the page, while still providing an initial UI that is prerendered and prefetchable for instant navigations. Learn more: https://nextjs.org/docs/messages/blocking-route", "environmentLabel": "Server", @@ -444,7 +444,7 @@ describe('Cache Components Fallback Validation', () => { | ^", "stack": [ "Layout app/none/[top]/unwrapped/layout.tsx (8:3)", - "LogSafely ", + "ReportValidation ", ], } `) @@ -483,19 +483,19 @@ describe('Cache Components Fallback Validation', () => { if (isTurbopack) { await expect(browser).toDisplayCollapsedRedbox(` { - "description": "Uncached data was accessed outside of + "description": "Runtime data was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. - To fix this, you can either: + To fix this: - Wrap the component in a boundary. This allows Next.js to stream its contents to the user as soon as it's ready, without blocking the rest of the app. + Provide a fallback UI using around this component. or - Move the asynchronous await into a Cache Component ("use cache"). This allows Next.js to statically prerender the component as part of the HTML document, so it's instantly visible to the user. + Move the Runtime data access into a deeper component wrapped in . - Note that request-specific information — such as params, cookies, and headers — is not available during static prerendering, so must be wrapped in . + In either case this allows Next.js to stream its contents to the user when they request the page, while still providing an initial UI that is prerendered and prefetchable for instant navigations. Learn more: https://nextjs.org/docs/messages/blocking-route", "environmentLabel": "Server", @@ -505,7 +505,7 @@ describe('Cache Components Fallback Validation', () => { | ^", "stack": [ "Layout app/none/[top]/unwrapped/layout.tsx (8:3)", - "LogSafely ", + "ReportValidation ", ], } `) @@ -544,19 +544,19 @@ describe('Cache Components Fallback Validation', () => { if (isTurbopack) { await expect(browser).toDisplayCollapsedRedbox(` { - "description": "Uncached data was accessed outside of + "description": "Runtime data was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. - To fix this, you can either: + To fix this: - Wrap the component in a boundary. This allows Next.js to stream its contents to the user as soon as it's ready, without blocking the rest of the app. + Provide a fallback UI using around this component. or - Move the asynchronous await into a Cache Component ("use cache"). This allows Next.js to statically prerender the component as part of the HTML document, so it's instantly visible to the user. + Move the Runtime data access into a deeper component wrapped in . - Note that request-specific information — such as params, cookies, and headers — is not available during static prerendering, so must be wrapped in . + In either case this allows Next.js to stream its contents to the user when they request the page, while still providing an initial UI that is prerendered and prefetchable for instant navigations. Learn more: https://nextjs.org/docs/messages/blocking-route", "environmentLabel": "Server", @@ -566,7 +566,7 @@ describe('Cache Components Fallback Validation', () => { | ^", "stack": [ "Layout app/none/[top]/unwrapped/layout.tsx (8:3)", - "LogSafely ", + "ReportValidation ", ], } `) diff --git a/test/development/app-dir/react-performance-track/react-performance-track.test.ts b/test/development/app-dir/react-performance-track/react-performance-track.test.ts index f235b53c156f8..b2bc9c0438d58 100644 --- a/test/development/app-dir/react-performance-track/react-performance-track.test.ts +++ b/test/development/app-dir/react-performance-track/react-performance-track.test.ts @@ -100,18 +100,6 @@ describe('react-performance-track', () => { name: '\u200bcookies [Prefetchable]', properties: [], }, - // TODO: The error message makes this seem like it shouldn't pop up here. - { - name: '\u200bcookies', - properties: [ - [ - 'rejected with', - 'During prerendering, `cookies()` rejects when the prerender is complete. ' + - 'Typically these errors are handled by React but if you move `cookies()` to a different context by using `setTimeout`, `after`, or similar functions you may observe this error and you should handle it in that context. ' + - 'This occurred at route "/cookies".', - ], - ], - }, ]) ) }) @@ -145,18 +133,6 @@ describe('react-performance-track', () => { name: '\u200bheaders [Prefetchable]', properties: [], }, - // TODO: The error message makes this seem like it shouldn't pop up here. - { - name: '\u200bheaders', - properties: [ - [ - 'rejected with', - 'During prerendering, `headers()` rejects when the prerender is complete. ' + - 'Typically these errors are handled by React but if you move `headers()` to a different context by using `setTimeout`, `after`, or similar functions you may observe this error and you should handle it in that context. ' + - 'This occurred at route "/headers".', - ], - ], - }, ]) ) }) diff --git a/test/e2e/app-dir/cache-components-errors/cache-components-errors.test.ts b/test/e2e/app-dir/cache-components-errors/cache-components-errors.test.ts index 5465b70fbeaf3..3977aae738427 100644 --- a/test/e2e/app-dir/cache-components-errors/cache-components-errors.test.ts +++ b/test/e2e/app-dir/cache-components-errors/cache-components-errors.test.ts @@ -88,12 +88,27 @@ describe('Cache Components Errors', () => { await expect(browser).toDisplayCollapsedRedbox(` { - "description": "Route "/dynamic-metadata-static-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", + "description": "Data that blocks navigation was accessed inside generateMetadata() in an otherwise prerenderable page + + When Document metadata is the only part of a page that cannot be prerendered Next.js expects you to either make it prerenderable or make some other part of the page non-prerenderable to avoid unintentional partially dynamic pages. Uncached data such as fetch(...), cached data with a low expire time, or connection() are all examples of data that only resolve on navigation. + + To fix this: + + Move the asynchronous await into a Cache Component ("use cache"). This allows Next.js to statically prerender generateMetadata() as part of the HTML document, so it's instantly visible to the user. + + or + + add connection() inside a somewhere in a Page or Layout. This tells Next.js that the page is intended to have some non-prerenderable parts. + + Learn more: https://nextjs.org/docs/messages/next-prerender-dynamic-metadata", "environmentLabel": "Server", - "label": "Console Error", - "source": null, + "label": "Ambiguous Metadata", + "source": "app/dynamic-metadata-static-route/page.tsx (2:9) @ Module.generateMetadata + > 2 | await new Promise((r) => setTimeout(r, 0)) + | ^", "stack": [ - "LogSafely ", + "Module.generateMetadata app/dynamic-metadata-static-route/page.tsx (2:9)", + "ReportValidation ", ], } `) @@ -139,30 +154,28 @@ describe('Cache Components Errors', () => { await expect(browser).toDisplayCollapsedRedbox(` { - "description": "Uncached data was accessed outside of + "description": "Data that blocks navigation was accessed outside of - This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. + This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. Uncached data such as fetch(...), cached data with a low expire time, or connection() are all examples of data that only resolve on navigation. To fix this, you can either: - Wrap the component in a boundary. This allows Next.js to stream its contents to the user as soon as it's ready, without blocking the rest of the app. + Provide a fallback UI using around this component. This allows Next.js to stream its contents to the user as soon as it's ready, without blocking the rest of the app. or Move the asynchronous await into a Cache Component ("use cache"). This allows Next.js to statically prerender the component as part of the HTML document, so it's instantly visible to the user. - Note that request-specific information — such as params, cookies, and headers — is not available during static prerendering, so must be wrapped in . - Learn more: https://nextjs.org/docs/messages/blocking-route", "environmentLabel": "Server", "label": "Blocking Route", - "source": "app/dynamic-metadata-error-route/page.tsx (20:16) @ Dynamic - > 20 | async function Dynamic() { - | ^", + "source": "app/dynamic-metadata-error-route/page.tsx (21:9) @ Dynamic + > 21 | await new Promise((r) => setTimeout(r)) + | ^", "stack": [ - "Dynamic app/dynamic-metadata-error-route/page.tsx (20:16)", + "Dynamic app/dynamic-metadata-error-route/page.tsx (21:9)", "Page app/dynamic-metadata-error-route/page.tsx (15:7)", - "LogSafely ", + "ReportValidation ", ], } `) @@ -266,12 +279,27 @@ describe('Cache Components Errors', () => { await expect(browser).toDisplayCollapsedRedbox(` { - "description": "Route "/dynamic-metadata-static-with-suspense" 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", + "description": "Data that blocks navigation was accessed inside generateMetadata() in an otherwise prerenderable page + + When Document metadata is the only part of a page that cannot be prerendered Next.js expects you to either make it prerenderable or make some other part of the page non-prerenderable to avoid unintentional partially dynamic pages. Uncached data such as fetch(...), cached data with a low expire time, or connection() are all examples of data that only resolve on navigation. + + To fix this: + + Move the asynchronous await into a Cache Component ("use cache"). This allows Next.js to statically prerender generateMetadata() as part of the HTML document, so it's instantly visible to the user. + + or + + add connection() inside a somewhere in a Page or Layout. This tells Next.js that the page is intended to have some non-prerenderable parts. + + Learn more: https://nextjs.org/docs/messages/next-prerender-dynamic-metadata", "environmentLabel": "Server", - "label": "Console Error", - "source": null, + "label": "Ambiguous Metadata", + "source": "app/dynamic-metadata-static-with-suspense/page.tsx (2:9) @ Module.generateMetadata + > 2 | await new Promise((r) => setTimeout(r, 0)) + | ^", "stack": [ - "LogSafely ", + "Module.generateMetadata app/dynamic-metadata-static-with-suspense/page.tsx (2:9)", + "ReportValidation ", ], } `) @@ -342,12 +370,27 @@ describe('Cache Components Errors', () => { await expect(browser).toDisplayCollapsedRedbox(` { - "description": "Route "/dynamic-viewport-static-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", + "description": "Data that blocks navigation was accessed inside generateViewport() + + Viewport metadata needs to be available on page load so accessing data that waits for a user navigation while producing it prevents Next.js from prerendering an initial UI. Uncached data such as fetch(...), cached data with a low expire time, or connection() are all examples of data that only resolve on navigation. + + To fix this: + + Move the asynchronous await into a Cache Component ("use cache"). This allows Next.js to statically prerender generateViewport() as part of the HTML document, so it's instantly visible to the user. + + or + + Put a around your document .This indicate to Next.js that you are opting into allowing blocking navigations for any page. + + Learn more: https://nextjs.org/docs/messages/next-prerender-dynamic-viewport", "environmentLabel": "Server", - "label": "Console Error", - "source": null, + "label": "Blocking Route", + "source": "app/dynamic-viewport-static-route/page.tsx (2:9) @ Module.generateViewport + > 2 | await new Promise((r) => setTimeout(r, 0)) + | ^", "stack": [ - "LogSafely ", + "Module.generateViewport app/dynamic-viewport-static-route/page.tsx (2:9)", + "ReportValidation ", ], } `) @@ -367,12 +410,12 @@ describe('Cache Components Errors', () => { if (isDebugPrerender) { expect(output).toMatchInlineSnapshot(` - "Route "/dynamic-viewport-static-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 - Error occurred prerendering page "/dynamic-viewport-static-route". Read more: https://nextjs.org/docs/messages/prerender-error + "Route "/dynamic-viewport-static-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 + Error occurred prerendering page "/dynamic-viewport-static-route". Read more: https://nextjs.org/docs/messages/prerender-error - > Export encountered errors on following paths: - /dynamic-viewport-static-route/page: /dynamic-viewport-static-route" - `) + > Export encountered errors on following paths: + /dynamic-viewport-static-route/page: /dynamic-viewport-static-route" + `) } else { expect(output).toMatchInlineSnapshot(` "Route "/dynamic-viewport-static-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 @@ -393,12 +436,27 @@ describe('Cache Components Errors', () => { await expect(browser).toDisplayCollapsedRedbox(` { - "description": "Route "/dynamic-viewport-dynamic-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", + "description": "Data that blocks navigation was accessed inside generateViewport() + + Viewport metadata needs to be available on page load so accessing data that waits for a user navigation while producing it prevents Next.js from prerendering an initial UI. Uncached data such as fetch(...), cached data with a low expire time, or connection() are all examples of data that only resolve on navigation. + + To fix this: + + Move the asynchronous await into a Cache Component ("use cache"). This allows Next.js to statically prerender generateViewport() as part of the HTML document, so it's instantly visible to the user. + + or + + Put a around your document .This indicate to Next.js that you are opting into allowing blocking navigations for any page. + + Learn more: https://nextjs.org/docs/messages/next-prerender-dynamic-viewport", "environmentLabel": "Server", - "label": "Console Error", - "source": null, + "label": "Blocking Route", + "source": "app/dynamic-viewport-dynamic-route/page.tsx (4:9) @ Module.generateViewport + > 4 | await new Promise((r) => setTimeout(r, 0)) + | ^", "stack": [ - "LogSafely ", + "Module.generateViewport app/dynamic-viewport-dynamic-route/page.tsx (4:9)", + "ReportValidation ", ], } `) @@ -418,12 +476,12 @@ describe('Cache Components Errors', () => { if (isDebugPrerender) { expect(output).toMatchInlineSnapshot(` - "Route "/dynamic-viewport-dynamic-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 - Error occurred prerendering page "/dynamic-viewport-dynamic-route". Read more: https://nextjs.org/docs/messages/prerender-error + "Route "/dynamic-viewport-dynamic-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 + Error occurred prerendering page "/dynamic-viewport-dynamic-route". Read more: https://nextjs.org/docs/messages/prerender-error - > Export encountered errors on following paths: - /dynamic-viewport-dynamic-route/page: /dynamic-viewport-dynamic-route" - `) + > Export encountered errors on following paths: + /dynamic-viewport-dynamic-route/page: /dynamic-viewport-dynamic-route" + `) } else { expect(output).toMatchInlineSnapshot(` "Route "/dynamic-viewport-dynamic-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 @@ -462,65 +520,61 @@ describe('Cache Components Errors', () => { const browser = await next.browser(pathname) await expect(browser).toDisplayCollapsedRedbox(` - [ - { - "description": "Uncached data was accessed outside of - - This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. - - To fix this, you can either: + [ + { + "description": "Data that blocks navigation was accessed outside of - Wrap the component in a boundary. This allows Next.js to stream its contents to the user as soon as it's ready, without blocking the rest of the app. + This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. Uncached data such as fetch(...), cached data with a low expire time, or connection() are all examples of data that only resolve on navigation. - or + To fix this, you can either: - Move the asynchronous await into a Cache Component ("use cache"). This allows Next.js to statically prerender the component as part of the HTML document, so it's instantly visible to the user. + Provide a fallback UI using around this component. This allows Next.js to stream its contents to the user as soon as it's ready, without blocking the rest of the app. - Note that request-specific information — such as params, cookies, and headers — is not available during static prerendering, so must be wrapped in . + or - Learn more: https://nextjs.org/docs/messages/blocking-route", - "environmentLabel": "Server", - "label": "Blocking Route", - "source": "app/dynamic-root/page.tsx (59:26) @ fetchRandom - > 59 | const response = await fetch( - | ^", - "stack": [ - "fetchRandom app/dynamic-root/page.tsx (59:26)", - "FetchingComponent app/dynamic-root/page.tsx (45:56)", - "Page app/dynamic-root/page.tsx (22:9)", - "LogSafely ", - ], - }, - { - "description": "Uncached data was accessed outside of + Move the asynchronous await into a Cache Component ("use cache"). This allows Next.js to statically prerender the component as part of the HTML document, so it's instantly visible to the user. - This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. + Learn more: https://nextjs.org/docs/messages/blocking-route", + "environmentLabel": "Server", + "label": "Blocking Route", + "source": "app/dynamic-root/page.tsx (63:26) @ fetchRandom + > 63 | const response = await fetch( + | ^", + "stack": [ + "fetchRandom app/dynamic-root/page.tsx (63:26)", + "FetchingComponent app/dynamic-root/page.tsx (46:50)", + "Page app/dynamic-root/page.tsx (23:9)", + "ReportValidation ", + ], + }, + { + "description": "Data that blocks navigation was accessed outside of - To fix this, you can either: + This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. Uncached data such as fetch(...), cached data with a low expire time, or connection() are all examples of data that only resolve on navigation. - Wrap the component in a boundary. This allows Next.js to stream its contents to the user as soon as it's ready, without blocking the rest of the app. + To fix this, you can either: - or + Provide a fallback UI using around this component. This allows Next.js to stream its contents to the user as soon as it's ready, without blocking the rest of the app. - Move the asynchronous await into a Cache Component ("use cache"). This allows Next.js to statically prerender the component as part of the HTML document, so it's instantly visible to the user. + or - Note that request-specific information — such as params, cookies, and headers — is not available during static prerendering, so must be wrapped in . + Move the asynchronous await into a Cache Component ("use cache"). This allows Next.js to statically prerender the component as part of the HTML document, so it's instantly visible to the user. - Learn more: https://nextjs.org/docs/messages/blocking-route", - "environmentLabel": "Server", - "label": "Blocking Route", - "source": "app/dynamic-root/page.tsx (59:26) @ fetchRandom - > 59 | const response = await fetch( - | ^", - "stack": [ - "fetchRandom app/dynamic-root/page.tsx (59:26)", - "FetchingComponent app/dynamic-root/page.tsx (45:56)", - "Page app/dynamic-root/page.tsx (27:7)", - "LogSafely ", - ], - }, - ] - `) + Learn more: https://nextjs.org/docs/messages/blocking-route", + "environmentLabel": "Server", + "label": "Blocking Route", + "source": "app/dynamic-root/page.tsx (63:26) @ fetchRandom + > 63 | const response = await fetch( + | ^", + "stack": [ + "fetchRandom app/dynamic-root/page.tsx (63:26)", + "FetchingComponent app/dynamic-root/page.tsx (46:50)", + "Page app/dynamic-root/page.tsx (28:7)", + "ReportValidation ", + ], + }, + ] + `) }) } else { it('should error the build if cache components happens in the root (outside a Suspense)', async () => { @@ -726,7 +780,7 @@ describe('Cache Components Errors', () => { "stack": [ "RandomReadingComponent app/sync-random-with-fallback/page.tsx (37:23)", "Page app/sync-random-with-fallback/page.tsx (18:11)", - "LogSafely ", + "ReportValidation ", ], } `) @@ -818,7 +872,7 @@ describe('Cache Components Errors', () => { "getRandomNumber app/sync-random-without-fallback/page.tsx (32:15)", "RandomReadingComponent app/sync-random-without-fallback/page.tsx (40:18)", "Page app/sync-random-without-fallback/page.tsx (18:11)", - "LogSafely ", + "ReportValidation ", ], } `) @@ -1659,7 +1713,7 @@ describe('Cache Components Errors', () => { "stack": [ "SyncIO app/sync-attribution/guarded-async-unguarded-clientsync/client.tsx (5:16)", "Page app/sync-attribution/guarded-async-unguarded-clientsync/page.tsx (22:9)", - "LogSafely ", + "ReportValidation ", ], } `) @@ -1740,34 +1794,34 @@ describe('Cache Components Errors', () => { const browser = await next.browser(pathname) await expect(browser).toDisplayCollapsedRedbox(` - { - "description": "Uncached data was accessed outside of + { + "description": "Runtime data was accessed outside of - This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. + This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation.cookies(), headers(), and searchParams, are examples of Runtime data that can only come from a user request. - To fix this, you can either: + To fix this: - Wrap the component in a boundary. This allows Next.js to stream its contents to the user as soon as it's ready, without blocking the rest of the app. + Provide a fallback UI using around this component. - or + or - Move the asynchronous await into a Cache Component ("use cache"). This allows Next.js to statically prerender the component as part of the HTML document, so it's instantly visible to the user. + Move the Runtime data access into a deeper component wrapped in . - Note that request-specific information — such as params, cookies, and headers — is not available during static prerendering, so must be wrapped in . + In either case this allows Next.js to stream its contents to the user when they request the page, while still providing an initial UI that is prerendered and prefetchable for instant navigations. - Learn more: https://nextjs.org/docs/messages/blocking-route", - "environmentLabel": "Server", - "label": "Blocking Route", - "source": "app/sync-attribution/unguarded-async-guarded-clientsync/page.tsx (34:18) @ RequestData - > 34 | ;(await cookies()).get('foo') - | ^", - "stack": [ - "RequestData app/sync-attribution/unguarded-async-guarded-clientsync/page.tsx (34:18)", - "Page app/sync-attribution/unguarded-async-guarded-clientsync/page.tsx (27:9)", - "LogSafely ", - ], - } - `) + Learn more: https://nextjs.org/docs/messages/blocking-route", + "environmentLabel": "Server", + "label": "Blocking Route", + "source": "app/sync-attribution/unguarded-async-guarded-clientsync/page.tsx (34:18) @ RequestData + > 34 | ;(await cookies()).get('foo') + | ^", + "stack": [ + "RequestData app/sync-attribution/unguarded-async-guarded-clientsync/page.tsx (34:18)", + "Page app/sync-attribution/unguarded-async-guarded-clientsync/page.tsx (27:9)", + "ReportValidation ", + ], + } + `) }) } else { it('should error the build with a reason related dynamic data', async () => { @@ -1905,7 +1959,7 @@ describe('Cache Components Errors', () => { "stack": [ "SyncIO app/sync-attribution/unguarded-async-unguarded-clientsync/client.tsx (5:16)", "Page app/sync-attribution/unguarded-async-unguarded-clientsync/page.tsx (22:9)", - "LogSafely ", + "ReportValidation ", ], } `) @@ -2280,34 +2334,9 @@ describe('Cache Components Errors', () => { it('should show a redbox error', async () => { const browser = await next.browser('/use-cache-low-expire') - await expect(browser).toDisplayCollapsedRedbox(` - { - "description": "Uncached data was accessed outside of - - This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. - - To fix this, you can either: - - Wrap the component in a boundary. This allows Next.js to stream its contents to the user as soon as it's ready, without blocking the rest of the app. - - or - - Move the asynchronous await into a Cache Component ("use cache"). This allows Next.js to statically prerender the component as part of the HTML document, so it's instantly visible to the user. - - Note that request-specific information — such as params, cookies, and headers — is not available during static prerendering, so must be wrapped in . - - Learn more: https://nextjs.org/docs/messages/blocking-route", - "environmentLabel": "Server", - "label": "Blocking Route", - "source": "app/use-cache-low-expire/page.tsx (3:16) @ Page - > 3 | export default async function Page() { - | ^", - "stack": [ - "Page app/use-cache-low-expire/page.tsx (3:16)", - "LogSafely ", - ], - } - `) + await expect(browser).toDisplayCollapsedRedbox( + `"Redbox did not open."` + ) }) } else { it('should error the build', async () => { @@ -2403,34 +2432,9 @@ describe('Cache Components Errors', () => { it('should show a redbox error', async () => { const browser = await next.browser('/use-cache-revalidate-0') - await expect(browser).toDisplayCollapsedRedbox(` - { - "description": "Uncached data was accessed outside of - - This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. - - To fix this, you can either: - - Wrap the component in a boundary. This allows Next.js to stream its contents to the user as soon as it's ready, without blocking the rest of the app. - - or - - Move the asynchronous await into a Cache Component ("use cache"). This allows Next.js to statically prerender the component as part of the HTML document, so it's instantly visible to the user. - - Note that request-specific information — such as params, cookies, and headers — is not available during static prerendering, so must be wrapped in . - - Learn more: https://nextjs.org/docs/messages/blocking-route", - "environmentLabel": "Server", - "label": "Blocking Route", - "source": "app/use-cache-revalidate-0/page.tsx (3:16) @ Page - > 3 | export default async function Page() { - | ^", - "stack": [ - "Page app/use-cache-revalidate-0/page.tsx (3:16)", - "LogSafely ", - ], - } - `) + await expect(browser).toDisplayCollapsedRedbox( + `"Redbox did not open."` + ) }) } else { it('should error the build', async () => { @@ -2527,9 +2531,32 @@ describe('Cache Components Errors', () => { it('should show a redbox error', async () => { const browser = await next.browser('/use-cache-params/foo') - await expect(browser).toDisplayCollapsedRedbox( - `"Redbox did not open."` - ) + await expect(browser).toDisplayCollapsedRedbox(` + { + "description": "Runtime data was accessed outside of + + This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation.cookies(), headers(), and searchParams, are examples of Runtime data that can only come from a user request. + + To fix this: + + Provide a fallback UI using around this component. + + or + + Move the Runtime data access into a deeper component wrapped in . + + In either case this allows Next.js to stream its contents to the user when they request the page, while still providing an initial UI that is prerendered and prefetchable for instant navigations. + + Learn more: https://nextjs.org/docs/messages/blocking-route", + "environmentLabel": "Server", + "label": "Blocking Route", + "source": null, + "stack": [ + "Page [Prerender] ", + "ReportValidation ", + ], + } + `) }) } else { it('should error the build', async () => { @@ -2861,19 +2888,19 @@ describe('Cache Components Errors', () => { await expect(browser).toDisplayCollapsedRedbox(` { - "description": "Uncached data was accessed outside of + "description": "Runtime data was accessed outside of - This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. + This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation.cookies(), headers(), and searchParams, are examples of Runtime data that can only come from a user request. - To fix this, you can either: + To fix this: - Wrap the component in a boundary. This allows Next.js to stream its contents to the user as soon as it's ready, without blocking the rest of the app. + Provide a fallback UI using around this component. or - Move the asynchronous await into a Cache Component ("use cache"). This allows Next.js to statically prerender the component as part of the HTML document, so it's instantly visible to the user. + Move the Runtime data access into a deeper component wrapped in . - Note that request-specific information — such as params, cookies, and headers — is not available during static prerendering, so must be wrapped in . + In either case this allows Next.js to stream its contents to the user when they request the page, while still providing an initial UI that is prerendered and prefetchable for instant navigations. Learn more: https://nextjs.org/docs/messages/blocking-route", "environmentLabel": "Server", @@ -2884,7 +2911,7 @@ describe('Cache Components Errors', () => { "stack": [ "Private app/use-cache-private-without-suspense/page.tsx (15:1)", "Page app/use-cache-private-without-suspense/page.tsx (10:7)", - "LogSafely ", + "ReportValidation ", ], } `) @@ -3044,7 +3071,7 @@ describe('Cache Components Errors', () => { "stack": [ "DateReadingComponent app/sync-io-current-time/date/page.tsx (19:16)", "Page app/sync-io-current-time/date/page.tsx (11:9)", - "LogSafely ", + "ReportValidation ", ], } `) @@ -3129,7 +3156,7 @@ describe('Cache Components Errors', () => { "stack": [ "DateReadingComponent app/sync-io-current-time/date-now/page.tsx (19:21)", "Page app/sync-io-current-time/date-now/page.tsx (11:9)", - "LogSafely ", + "ReportValidation ", ], } `) @@ -3214,7 +3241,7 @@ describe('Cache Components Errors', () => { "stack": [ "DateReadingComponent app/sync-io-current-time/new-date/page.tsx (19:16)", "Page app/sync-io-current-time/new-date/page.tsx (11:9)", - "LogSafely ", + "ReportValidation ", ], } `) @@ -3299,7 +3326,7 @@ describe('Cache Components Errors', () => { "stack": [ "SyncIOComponent app/sync-io-random/math-random/page.tsx (19:21)", "Page app/sync-io-random/math-random/page.tsx (11:9)", - "LogSafely ", + "ReportValidation ", ], } `) @@ -3384,7 +3411,7 @@ describe('Cache Components Errors', () => { "stack": [ "SyncIOComponent app/sync-io-web-crypto/get-random-value/page.tsx (20:10)", "Page app/sync-io-web-crypto/get-random-value/page.tsx (11:9)", - "LogSafely ", + "ReportValidation ", ], } `) @@ -3471,7 +3498,7 @@ describe('Cache Components Errors', () => { "stack": [ "SyncIOComponent app/sync-io-web-crypto/random-uuid/page.tsx (19:23)", "Page app/sync-io-web-crypto/random-uuid/page.tsx (11:9)", - "LogSafely ", + "ReportValidation ", ], } `) @@ -3563,20 +3590,20 @@ describe('Cache Components Errors', () => { `) } else { await expect(browser).toDisplayCollapsedRedbox(` - { - "description": "Route "/sync-io-node-crypto/generate-key-pair-sync" used \`require('node:crypto').generateKeyPairSync(...)\` 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", - "environmentLabel": "Server", - "label": "Console Error", - "source": "app/sync-io-node-crypto/generate-key-pair-sync/page.tsx (20:17) @ SyncIOComponent - > 20 | const first = crypto.generateKeyPairSync('rsa', keyGenOptions) - | ^", - "stack": [ - "SyncIOComponent app/sync-io-node-crypto/generate-key-pair-sync/page.tsx (20:17)", - "Page app/sync-io-node-crypto/generate-key-pair-sync/page.tsx (12:9)", - "LogSafely ", - ], - } - `) + { + "description": "Route "/sync-io-node-crypto/generate-key-pair-sync" used \`require('node:crypto').generateKeyPairSync(...)\` 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", + "environmentLabel": "Server", + "label": "Console Error", + "source": "app/sync-io-node-crypto/generate-key-pair-sync/page.tsx (20:17) @ SyncIOComponent + > 20 | const first = crypto.generateKeyPairSync('rsa', keyGenOptions) + | ^", + "stack": [ + "SyncIOComponent app/sync-io-node-crypto/generate-key-pair-sync/page.tsx (20:17)", + "Page app/sync-io-node-crypto/generate-key-pair-sync/page.tsx (12:9)", + "ReportValidation ", + ], + } + `) } }) } else { @@ -3687,20 +3714,20 @@ describe('Cache Components Errors', () => { `) } else { await expect(browser).toDisplayCollapsedRedbox(` - { - "description": "Route "/sync-io-node-crypto/generate-key-sync" used \`require('node:crypto').generateKeySync(...)\` 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", - "environmentLabel": "Server", - "label": "Console Error", - "source": "app/sync-io-node-crypto/generate-key-sync/page.tsx (20:17) @ SyncIOComponent - > 20 | const first = crypto - | ^", - "stack": [ - "SyncIOComponent app/sync-io-node-crypto/generate-key-sync/page.tsx (20:17)", - "Page app/sync-io-node-crypto/generate-key-sync/page.tsx (12:9)", - "LogSafely ", - ], - } - `) + { + "description": "Route "/sync-io-node-crypto/generate-key-sync" used \`require('node:crypto').generateKeySync(...)\` 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", + "environmentLabel": "Server", + "label": "Console Error", + "source": "app/sync-io-node-crypto/generate-key-sync/page.tsx (20:17) @ SyncIOComponent + > 20 | const first = crypto + | ^", + "stack": [ + "SyncIOComponent app/sync-io-node-crypto/generate-key-sync/page.tsx (20:17)", + "Page app/sync-io-node-crypto/generate-key-sync/page.tsx (12:9)", + "ReportValidation ", + ], + } + `) } }) } else { @@ -3811,20 +3838,20 @@ describe('Cache Components Errors', () => { `) } else { await expect(browser).toDisplayCollapsedRedbox(` - { - "description": "Route "/sync-io-node-crypto/generate-prime-sync" used \`require('node:crypto').generatePrimeSync(...)\` 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", - "environmentLabel": "Server", - "label": "Console Error", - "source": "app/sync-io-node-crypto/generate-prime-sync/page.tsx (20:32) @ SyncIOComponent - > 20 | const first = new Uint8Array(crypto.generatePrimeSync(128)) - | ^", - "stack": [ - "SyncIOComponent app/sync-io-node-crypto/generate-prime-sync/page.tsx (20:32)", - "Page app/sync-io-node-crypto/generate-prime-sync/page.tsx (12:9)", - "LogSafely ", - ], - } - `) + { + "description": "Route "/sync-io-node-crypto/generate-prime-sync" used \`require('node:crypto').generatePrimeSync(...)\` 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", + "environmentLabel": "Server", + "label": "Console Error", + "source": "app/sync-io-node-crypto/generate-prime-sync/page.tsx (20:32) @ SyncIOComponent + > 20 | const first = new Uint8Array(crypto.generatePrimeSync(128)) + | ^", + "stack": [ + "SyncIOComponent app/sync-io-node-crypto/generate-prime-sync/page.tsx (20:32)", + "Page app/sync-io-node-crypto/generate-prime-sync/page.tsx (12:9)", + "ReportValidation ", + ], + } + `) } }) } else { @@ -3935,20 +3962,20 @@ describe('Cache Components Errors', () => { `) } else { await expect(browser).toDisplayCollapsedRedbox(` - { - "description": "Route "/sync-io-node-crypto/get-random-values" used \`crypto.getRandomValues()\` 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", - "environmentLabel": "Server", - "label": "Console Error", - "source": "app/sync-io-node-crypto/get-random-values/page.tsx (21:3) @ SyncIOComponent - > 21 | crypto.getRandomValues(first) - | ^", - "stack": [ - "SyncIOComponent app/sync-io-node-crypto/get-random-values/page.tsx (21:3)", - "Page app/sync-io-node-crypto/get-random-values/page.tsx (12:9)", - "LogSafely ", - ], - } - `) + { + "description": "Route "/sync-io-node-crypto/get-random-values" used \`crypto.getRandomValues()\` 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", + "environmentLabel": "Server", + "label": "Console Error", + "source": "app/sync-io-node-crypto/get-random-values/page.tsx (21:3) @ SyncIOComponent + > 21 | crypto.getRandomValues(first) + | ^", + "stack": [ + "SyncIOComponent app/sync-io-node-crypto/get-random-values/page.tsx (21:3)", + "Page app/sync-io-node-crypto/get-random-values/page.tsx (12:9)", + "ReportValidation ", + ], + } + `) } }) } else { @@ -4059,20 +4086,20 @@ describe('Cache Components Errors', () => { `) } else { await expect(browser).toDisplayCollapsedRedbox(` - { - "description": "Route "/sync-io-node-crypto/random-bytes" used \`require('node:crypto').randomBytes(size)\` 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", - "environmentLabel": "Server", - "label": "Console Error", - "source": "app/sync-io-node-crypto/random-bytes/page.tsx (20:17) @ SyncIOComponent - > 20 | const first = crypto.randomBytes(8) - | ^", - "stack": [ - "SyncIOComponent app/sync-io-node-crypto/random-bytes/page.tsx (20:17)", - "Page app/sync-io-node-crypto/random-bytes/page.tsx (12:9)", - "LogSafely ", - ], - } - `) + { + "description": "Route "/sync-io-node-crypto/random-bytes" used \`require('node:crypto').randomBytes(size)\` 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", + "environmentLabel": "Server", + "label": "Console Error", + "source": "app/sync-io-node-crypto/random-bytes/page.tsx (20:17) @ SyncIOComponent + > 20 | const first = crypto.randomBytes(8) + | ^", + "stack": [ + "SyncIOComponent app/sync-io-node-crypto/random-bytes/page.tsx (20:17)", + "Page app/sync-io-node-crypto/random-bytes/page.tsx (12:9)", + "ReportValidation ", + ], + } + `) } }) } else { @@ -4183,20 +4210,20 @@ describe('Cache Components Errors', () => { `) } else { await expect(browser).toDisplayCollapsedRedbox(` - { - "description": "Route "/sync-io-node-crypto/random-fill-sync" used \`require('node:crypto').randomFillSync(...)\` 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", - "environmentLabel": "Server", - "label": "Console Error", - "source": "app/sync-io-node-crypto/random-fill-sync/page.tsx (21:3) @ SyncIOComponent - > 21 | crypto.randomFillSync(first, 4, 8) - | ^", - "stack": [ - "SyncIOComponent app/sync-io-node-crypto/random-fill-sync/page.tsx (21:3)", - "Page app/sync-io-node-crypto/random-fill-sync/page.tsx (12:9)", - "LogSafely ", - ], - } - `) + { + "description": "Route "/sync-io-node-crypto/random-fill-sync" used \`require('node:crypto').randomFillSync(...)\` 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", + "environmentLabel": "Server", + "label": "Console Error", + "source": "app/sync-io-node-crypto/random-fill-sync/page.tsx (21:3) @ SyncIOComponent + > 21 | crypto.randomFillSync(first, 4, 8) + | ^", + "stack": [ + "SyncIOComponent app/sync-io-node-crypto/random-fill-sync/page.tsx (21:3)", + "Page app/sync-io-node-crypto/random-fill-sync/page.tsx (12:9)", + "ReportValidation ", + ], + } + `) } }) } else { @@ -4307,20 +4334,20 @@ describe('Cache Components Errors', () => { `) } else { await expect(browser).toDisplayCollapsedRedbox(` - { - "description": "Route "/sync-io-node-crypto/random-int-between" used \`require('node:crypto').randomInt(min, max)\` 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", - "environmentLabel": "Server", - "label": "Console Error", - "source": "app/sync-io-node-crypto/random-int-between/page.tsx (20:17) @ SyncIOComponent - > 20 | const first = crypto.randomInt(128, 256) - | ^", - "stack": [ - "SyncIOComponent app/sync-io-node-crypto/random-int-between/page.tsx (20:17)", - "Page app/sync-io-node-crypto/random-int-between/page.tsx (12:9)", - "LogSafely ", - ], - } - `) + { + "description": "Route "/sync-io-node-crypto/random-int-between" used \`require('node:crypto').randomInt(min, max)\` 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", + "environmentLabel": "Server", + "label": "Console Error", + "source": "app/sync-io-node-crypto/random-int-between/page.tsx (20:17) @ SyncIOComponent + > 20 | const first = crypto.randomInt(128, 256) + | ^", + "stack": [ + "SyncIOComponent app/sync-io-node-crypto/random-int-between/page.tsx (20:17)", + "Page app/sync-io-node-crypto/random-int-between/page.tsx (12:9)", + "ReportValidation ", + ], + } + `) } }) } else { @@ -4431,20 +4458,20 @@ describe('Cache Components Errors', () => { `) } else { await expect(browser).toDisplayCollapsedRedbox(` - { - "description": "Route "/sync-io-node-crypto/random-int-up-to" used \`require('node:crypto').randomInt(min, max)\` 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", - "environmentLabel": "Server", - "label": "Console Error", - "source": "app/sync-io-node-crypto/random-int-up-to/page.tsx (20:17) @ SyncIOComponent - > 20 | const first = crypto.randomInt(128) - | ^", - "stack": [ - "SyncIOComponent app/sync-io-node-crypto/random-int-up-to/page.tsx (20:17)", - "Page app/sync-io-node-crypto/random-int-up-to/page.tsx (12:9)", - "LogSafely ", - ], - } - `) + { + "description": "Route "/sync-io-node-crypto/random-int-up-to" used \`require('node:crypto').randomInt(min, max)\` 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", + "environmentLabel": "Server", + "label": "Console Error", + "source": "app/sync-io-node-crypto/random-int-up-to/page.tsx (20:17) @ SyncIOComponent + > 20 | const first = crypto.randomInt(128) + | ^", + "stack": [ + "SyncIOComponent app/sync-io-node-crypto/random-int-up-to/page.tsx (20:17)", + "Page app/sync-io-node-crypto/random-int-up-to/page.tsx (12:9)", + "ReportValidation ", + ], + } + `) } }) } else { @@ -4555,20 +4582,20 @@ describe('Cache Components Errors', () => { `) } else { await expect(browser).toDisplayCollapsedRedbox(` - { - "description": "Route "/sync-io-node-crypto/random-uuid" used \`require('node:crypto').randomUUID()\` 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", - "environmentLabel": "Server", - "label": "Console Error", - "source": "app/sync-io-node-crypto/random-uuid/page.tsx (20:17) @ SyncIOComponent - > 20 | const first = crypto.randomUUID() - | ^", - "stack": [ - "SyncIOComponent app/sync-io-node-crypto/random-uuid/page.tsx (20:17)", - "Page app/sync-io-node-crypto/random-uuid/page.tsx (12:9)", - "LogSafely ", - ], - } - `) + { + "description": "Route "/sync-io-node-crypto/random-uuid" used \`require('node:crypto').randomUUID()\` 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", + "environmentLabel": "Server", + "label": "Console Error", + "source": "app/sync-io-node-crypto/random-uuid/page.tsx (20:17) @ SyncIOComponent + > 20 | const first = crypto.randomUUID() + | ^", + "stack": [ + "SyncIOComponent app/sync-io-node-crypto/random-uuid/page.tsx (20:17)", + "Page app/sync-io-node-crypto/random-uuid/page.tsx (12:9)", + "ReportValidation ", + ], + } + `) } }) } else { diff --git a/test/e2e/app-dir/cache-components-errors/fixtures/default/app/dynamic-root/page.tsx b/test/e2e/app-dir/cache-components-errors/fixtures/default/app/dynamic-root/page.tsx index 1eef58a72fd87..667809bc21a1f 100644 --- a/test/e2e/app-dir/cache-components-errors/fixtures/default/app/dynamic-root/page.tsx +++ b/test/e2e/app-dir/cache-components-errors/fixtures/default/app/dynamic-root/page.tsx @@ -1,6 +1,7 @@ import { Suspense } from 'react' import { IndirectionOne, IndirectionTwo } from './indirection' +import { cookies } from 'next/headers' export default async function Page() { return ( @@ -56,8 +57,15 @@ const fetchRandomCached = async (entropy: string) => { } const fetchRandom = async (entropy: string) => { + // Hide uncached I/O behind a runtime API call, to ensure we still get the + // correct owner stack for the error. + await cookies() const response = await fetch( 'https://next-data-api-endpoint.vercel.app/api/random?b=' + entropy ) + // The error should point at the fetch above, and not at the following fetch. + await fetch( + 'https://next-data-api-endpoint.vercel.app/api/random?b=' + entropy + 'x' + ) return response.text() }