Skip to content

Commit

Permalink
[dynamicIO] refine error messaging for sync API access (#71467)
Browse files Browse the repository at this point in the history
Temporarily removing the dev warnings. I've moved these to be errors in
the build side. In a follow up PR I will use prerendering during dev to
trigger these errors and report them as dev errors as well.

This also updates how we track sync dynamic errors. The main goal is to
have a stack trace of the sync function and separately report all the
component stacks that might contain this function call. It's not
fool-proof b/c the sync api may be called in a way that is not observed
by SSR. The whole debugging story here will improve dramatically with
owner stacks but that is currently experimental only in React.

The plan for future updates is to add a prospective render for SSR so we
can flush out module scope issues like calling Math.random while lazy
initializing a module. This is asserted for RSC in this change (see new
tests) but won't wouldn't pass for SSR b/c we only do single pass
prerendering at the moment.

After that I will add in the prerender-while-render in dev and utilize
the prerender errors to power dev as well.
  • Loading branch information
gnoff authored Oct 18, 2024
1 parent 5c404e9 commit 1f46214
Show file tree
Hide file tree
Showing 23 changed files with 630 additions and 222 deletions.
97 changes: 97 additions & 0 deletions errors/next-prerender-sync-headers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
title: Cannot access Request information synchronously with `cookies()`, `headers()`, or `draftMode()`
---

#### Why This Error Occurred

In Next.js 15 global functions that provide access to Request Header information such as `cookies()`, `headers()`, and `draftMode()` are each now `async` and return a `Promise` rather than the associated object directly.

To support migrating to the async APIs Next.js 15 still supports accessing properties of the underlying object directly on the returned Promise. However when `dynamicIO` is enabled it is an error to do so.

#### Possible Ways to Fix It

If you haven't already followed the Next.js 15 upgrade guide which includes running a codemod to auto-convert to the necessary async form of these APIs start there.

[Next 15 Upgrade Guide](https://nextjs.org/docs/app/building-your-application/upgrading/version-15#async-request-apis-breaking-change)

If you have already run the codemod on your project look for instances of the string `@next-codemod-error` to see where the codemod was unable to convert to the async form. You will have to refactor your code manually here to make it compatible with the new result type.

Before:

```jsx filename=".../token-utils.js"
// This function is sync and the codemod won't make it async
// because it doesn't know about every callsite that uses it.
export function getToken() {
// @next-codemod-error ...
return cookies().get('token')
}
```

```jsx filename="app/page.js"
import { getToken } from '.../token-utils'

export default function Page() {
const token = getToken();
validateToken(token)
return ...
}
```

After:

```jsx filename=".../token-utils.js"
export async function Page() {
return (await cookies()).get(token)
}
```

```jsx filename="app/page.js"
import { getToken } from '.../token-utils'

export default async function Page() {
const token = await getToken();
validateToken(token)
return ...
}
```

If you do not find instances of this string then it is possible the synchronous use of Request Header data is inside a 3rd party library. You should identify which library is using this function and see if it has published a Next 15 compatible version that adheres to the new Promise return type.

as a last resort you can add `await connection()` before calling the 3rd party function which uses this API. This will inform Next.js that everything after this await should be excluded from prerendering. This will continue to work until we remove support for synchronously access Request data which is expected to happen in the next major version.

Before:

```jsx filename="app/page.js"
import { getDataFrom3rdParty } from '3rdparty'

export default function Page() {
// Imagine this function access Request data synchronously
// on the inside even if it has an async external interface
const token = await getDataFrom3rdParty();
return ...
}
```

After:

```jsx filename="app/page.js"
import { connection } from 'next/server'

export default async function Page() {
await connection()
// Everything from here down will be excluded from prerendering
const token = await getDataFrom3rdParty();
validateToken(token)
return ...
}
```

Note that with `await connection()` and `dynamicIO` it is still required that there is a Suspense boundary somewhere above the component that uses `await connection()`. If you do not have any Suspense boundary parent you will need to add one where is appropriate to describe a fallback UI.

### Useful Links

- [`headers` function](https://nextjs.org/docs/app/api-reference/functions/headers)
- [`cookies` function](https://nextjs.org/docs/app/api-reference/functions/cookies)
- [`draftMode` function](https://nextjs.org/docs/app/api-reference/functions/draft-mode)
- [`connection` function](https://nextjs.org/docs/app/api-reference/functions/connection)
- [`Suspense` React API](https://react.dev/reference/react/Suspense)
59 changes: 59 additions & 0 deletions errors/next-prerender-sync-params.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
title: Cannot access Request information synchronously with Page or Layout or Route `params` or Page `searchParams`
---

#### Why This Error Occurred

In Next.js 15 `params` passed into Page and Layout components and `searchParams` passed into Page components are now Promises.

To support migrating to the async `params` and `searchParams` Next.js 15 still supports accessing param and searchParam values directly on the Promise object. However when `dynamicIO` is enabled it is an error to do so.

#### Possible Ways to Fix It

If you haven't already followed the Next.js 15 upgrade guide which includes running a codemod to auto-convert to the necessary async form of `params` and `searchParams` start there.

[Next 15 Upgrade Guide](https://nextjs.org/docs/app/building-your-application/upgrading/version-15#async-request-apis-breaking-change)

If you have already run the codemod on your project look for instances of the string `@next-codemod-error` to see where the codemod was unable to convert to the async form. You will have to refactor your code manually here to make it compatible with the new result type.

Before:

```jsx filename=".../some-file.js"
// This component ends up being the Page component even though it is defined outside of
// page.js due to how it is reexported in page.js
export default function ComponentThatWillBeExportedAsPage({ params, searchParams }) {
const { slug } = params;
const { page } = searchParams
return <RenderList slug={slug} page={page}>
}
```

```jsx filename="app/page.js"
// the codemod cannot find the actual Page component so the Page may still have remaining
// synchronous access to params and searchParams

// @next-codemod-error
export * from '.../some-file'
```

After:

```jsx filename=".../some-file.js"
// This component ends up being the Page component even though it is defined outside of
// page.js due to how it is reexported in page.js
export default async function ComponentThatWillBeExportedAsPage({ params, searchParams }) {
const { slug } = await params;
const { page } = await searchParams
return <RenderList slug={slug} page={page}>
}
```

```jsx filename="app/page.js"
export * from '.../some-file'
```

It is unexpected that you would run the codemod and not successfully convert all instances of `params` and `searchParams` to async or have a marker string to help you locate unconverted cases. If you do find yourself in this situation please report this to [Next.js on Github](https://github.com/vercel/next.js/issues).

- [`page.js` file convention](https://nextjs.org/docs/app/api-reference/file-conventions/page)
- [`layout.js` file convention](https://nextjs.org/docs/app/api-reference/file-conventions/layout)
- [`route.js` file convention](https://nextjs.org/docs/app/api-reference/file-conventions/route)
39 changes: 39 additions & 0 deletions errors/next-prerender-sync-request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
title: Cannot access Request information synchronously in route.js without awaiting connection first
---

#### Why This Error Occurred

When `dyanmicIO` is enabled Next.js treats most access to Request information as explicitly dynamic. However routes have a Request object and you can read values like the current pathname from that object synchronously. If you need to access Request information in a route handler you should first call `await connection()` to inform Next.js that everything after this point is explicitly dynamic.

This is only needed in `GET` handlers because `GET` is the only handler Next.js will attempt to prerender.

#### Possible Ways to Fix It

Look for access of the Request object in your route.js and add `await connection()` before you access any properties of the Request.

Before:

```jsx filename="app/.../route.js"
export default function GET(request) {
const requestHeaders = request.headers
return ...
}
```

After:

```jsx filename="app/.../route.js"
import { connection } from 'next/server'

export default async function GET(request) {
await connection()
const requestHeaders = request.headers
return ...
}
```

It is unexpected that you would run the codemod and not successfully convert all instances of `params` and `searchParams` to async or have a marker string to help you locate unconverted cases. If you do find yourself in this situation please report this to [Next.js on Github](https://github.com/vercel/next.js/issues).

- [`connection` function](https://nextjs.org/docs/app/api-reference/functions/connection)
- [`route.js` file convention](https://nextjs.org/docs/app/api-reference/file-conventions/route)
6 changes: 2 additions & 4 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2208,10 +2208,9 @@ async function prerenderToStream(

const componentStack: string | undefined = (errorInfo as any)
.componentStack
if (typeof componentStack === 'string' && err instanceof Error) {
if (typeof componentStack === 'string') {
trackAllowedDynamicAccess(
workStore.route,
err,
componentStack,
dynamicTracking
)
Expand Down Expand Up @@ -2543,10 +2542,9 @@ async function prerenderToStream(

const componentStack: string | undefined = (errorInfo as any)
.componentStack
if (typeof componentStack === 'string' && err instanceof Error) {
if (typeof componentStack === 'string') {
trackAllowedDynamicAccess(
workStore.route,
err,
componentStack,
dynamicTracking
)
Expand Down
Loading

0 comments on commit 1f46214

Please sign in to comment.