Files
Hendrik LiebauandGitHub 662c6d575f Warn for non-deterministic "use cache" args during final prerender (#92820)
When a `"use cache"` function receives arguments that differ between the
cache warming phase and the final prerender, the cache key changes and
the Resume Data Cache (RDC) entry from the prospective prerender is
missed.

This can happen for various reasons, for example when concurrent async
operations push results into a shared array in non-deterministic order,
and that array is then passed as an argument to a cached function.

Without a `cacheSignal` to keep the render alive, the final prerender
aborts the cache entry generation, producing an incomplete RSC stream
that causes "Connection closed" errors.

This change detects that scenario (an RDC miss during the final
prerender where `cacheSignal` is `null`) and returns a hanging promise
instead of generating a broken cache entry. A warning is logged to help
developers identify the non-deterministic arguments. By making the
cached function a dynamic hole rather than erroring, the prerender can
still complete and produce at least a partial shell if there is a
Suspense boundary above. This affects both on-demand prerendering and
runtime prefetching.

To avoid false positives, cache keys that were intentionally skipped
during the prospective prerender (e.g. because the cached function
accessed fallback params) are tracked in a `dynamicCacheKeys` set on the
RDC. During the final prerender, a known dynamic key is returned as a
hanging promise early without logging a warning. This also serves as a
performance optimization, since it avoids trying to regenerate the
entry. This set is intentionally not serialized, as cache misses for
dynamic keys should generate fresh entries during the resume at request
time.
2026-04-15 21:59:58 +00:00

53 lines
1.6 KiB
TypeScript

import { nextTestSetup } from 'e2e-utils'
describe('use-cache-non-deterministic-args', () => {
const { next } = nextTestSetup({
files: __dirname,
})
it('should warn about non-deterministic cache args during on-demand prerender', async () => {
const outputIndex = next.cliOutput.length
await next.browser('/unknown')
const output = next.cliOutput.slice(outputIndex)
expect(output).not.toContain('Connection closed')
expect(output).toContain(
'Unexpected cache miss after cache warming phase during prerendering'
)
})
it('should warn about non-deterministic cache args during runtime prefetch', async () => {
const outputIndex = next.cliOutput.length
let onRuntimePrefetchDone: () => void
const runtimePrefetchDone = new Promise<void>((resolve) => {
onRuntimePrefetchDone = resolve
})
// Visit /with-runtime-prefetch to render the Link. The link being visible
// in the viewport triggers a runtime prefetch for the /known route.
await next.browser('/with-runtime-prefetch', {
beforePageLoad(page) {
page.on('response', (res) => {
const url = new URL(res.url())
const request = res.request()
if (
url.pathname === '/known' &&
request.headers()['next-router-prefetch'] === '2'
) {
onRuntimePrefetchDone()
}
})
},
})
await runtimePrefetchDone
const output = next.cliOutput.slice(outputIndex)
expect(output).not.toContain('Connection closed')
expect(output).toContain(
'Unexpected cache miss after cache warming phase during prerendering'
)
})
})