mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
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.
53 lines
1.6 KiB
TypeScript
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'
|
|
)
|
|
})
|
|
})
|