Files
Janka UrygaandGitHub 1660d3966a [CC] Fix hanging dynamic promise when abandoning render (#86690)
Fixes #86662

The bug was introduced in #85747, where we added
`RenderStage.Abandoned`. We forgot to update the logic that rejects
promises on abort, so if a render was abandoned (i.e. restarted due to a
cache miss), then promises waiting for the dynamic stage would hang
forever.

This is not generally observable unless the promise is persisted/cached
in userspace. Userspace caches are problematic in Cache Components and
we discourage their usage, but the test app included here should still
work as expected (i.e. without hanging forever while rendering).
2025-12-02 15:11:15 +01:00

72 lines
1.9 KiB
TypeScript

// This repros a bug from https://github.com/vercel/next.js/issues/86662.
// The bug from occurs if we have:
// - a cache (which requires warming = a restart in dev)
// - a dynamic function (blocked until the dynamic stage, like an uncached fetch)
// that dedupes concurrent calls. note that that's not quite the same as caching it,
// because it gets dropped after finishing.
import { Suspense } from 'react'
export default async function Page() {
return (
<main>
<Suspense fallback={<div>Loading...</div>}>
<div id="random">
<DynamicRandom />
</div>
</Suspense>
</main>
)
}
async function DynamicRandom() {
const [, random] = await Promise.all([
// ensure that there's a restart to warm this cache.
cached(),
// This fetch going to be blocked on the dynamic stage.
// That promise should be rejected when restarting.
// If it's not rejected (like it wasn't before the fix),
// it'll remain hanging, and we'll never render anything.
fetchDynamicRandomDeduped(),
])
return random
}
function dedupeConcurrent<T>(func: () => Promise<T>): () => Promise<T> {
let pending: Promise<T> | null = null
return () => {
if (pending !== null) {
console.log('dedupe :: re-using pending promise')
return pending
}
console.log('dedupe :: starting')
const promise = func()
pending = promise
const clearPending = () => {
console.log('dedupe :: finished')
pending = null
}
promise.then(clearPending, clearPending)
return promise
}
}
const fetchDynamicRandomDeduped = dedupeConcurrent(async () => {
const res = await fetch(
'https://next-data-api-endpoint.vercel.app/api/random'
)
if (!res.ok) {
throw new Error(`request failed with status ${res.status}`)
}
const text = await res.text()
return text
})
async function cached() {
'use cache'
await new Promise((resolve) => setTimeout(resolve))
}