Files
Janka UrygaandGitHub 46681d90f0 [PPF] Sync IO is only allowed in the dynamic stage (#95384)
This PR makes Sync IO behavior more restrictive when
`partialPrefetching` is on (either globally or for a page).

- Previously, we allowed `await cookies(); Date.now()` in in segments
that won't ever be runtime prefetched. This was achieved by separating
segments into "early" and "late" stages and only erroring in the "early"
ones.
- After this PR, it will always be an error to do sync IO anywhere other
than after `io()`/`connection()`/uncached IO (i.e. outside the Dynamic
stage).

We're doing this mainly because with `partialPrefetching`, `cookies()`
resolves in App Shells, and sync IO causes a severe deopt in any
prerender. `allow-runtime` also opts a route into `partialPrefetching`,
which means that even the segments above the runtime prefetch boundary
will need an App Shell and can't tolerate Sync IO.

This PR removes all early/late stage separation, because we no longer
need to vary the Sync IO behavior on the segment. Instead, we now pick
whether a stageController should use
- `SyncIOMode.AllowedInRuntimeOrDynamic` (legacy)
- `SyncIOMode.AllowedInDynamic` (`partialPrefetching`)
- `SyncIOMode.Untracked` if it needs to opt out

I've removed the existing tests that asserted that sync IO is allowed in
segments above the `allow-runtime` boundary. Instead, we check that sync
io either errors (if partialPrefetching is on) or is allowed (if
partialPrefetching is off)

Closes NAR-855
2026-07-10 15:36:41 +00:00

51 lines
1.7 KiB
TypeScript

import { Instant } from 'next'
import { cookies } from 'next/headers'
import { Fragment, Suspense } from 'react'
export const instant: Instant = {
level: 'experimental-error',
unstable_samples: [{ cookies: [{ name: 'testCookie', value: 'testValue' }] }],
}
// cookies() is passed as a promise input to a public "use cache" function.
// The cache doesn't read the cookies in its body — they're only part of the cache key.
// After the cache resolves, Date.now() is sync IO that should error.
//
// This test validates that the cache input encoding resolves in the
// Runtime stage. Date.now() should either error or be allowed
// depending on if partialPrefetching is enabled or not.
async function cachedFn(cookiePromise: Promise<string>) {
'use cache'
// Intentionally not reading the cookie promise in the body.
// It's only used as part of the cache key via input encoding.
return 'cached result'
}
export default async function Page() {
return (
<main>
<p>
Page with sync IO after cache with cookie input:{' '}
<SuspenseIfNotPartialPrefetching>
<SyncIOAfterCache />
</SuspenseIfNotPartialPrefetching>
</p>
</main>
)
}
// Before partialPrefetching, cookies() is not allowed in shells,
// so it would fail instant validation unless wrapped in suspense.
// We're not interested in checking if cookies blocks,
// only in wheter the Sync IO after triggers an error.
const SuspenseIfNotPartialPrefetching = process.env.__NEXT_PARTIAL_PREFETCHING
? Fragment
: Suspense
async function SyncIOAfterCache() {
const cookiePromise = cookies().then((c) => c.get('testCookie')?.value ?? '')
await cachedFn(cookiePromise)
return Date.now()
}