next.js
851b6cb0 - Preserve closed-route admission across cache misses (#98964)

Commit
30 days ago
Preserve closed-route admission across cache misses (#98964) ## Summary Keep route admission independent of ISR cache contents. With Cache Components disabled, a route using `dynamicParams = false` and `generateStaticParams()` can currently return 200 for a build-generated URL, then incorrectly return 404 after `revalidatePath` expires its cache entry. An empty external cache causes the same failure on the first request. For example, if the build generated `/products/known`, that URL remains allowed after invalidation; `/products/unlisted` remains disallowed regardless of cache state. If a later build removes `/products/known`, it must immediately stop matching, even if an external cache still holds that URL's old response. The request handler now rejects unlisted closed URLs before looking up their ISR entries. This prevents both serving an old cache hit and generating a new response for a URL the current build does not admit. An admitted build path whose matched fallback is `false` regenerates using a blocking render when necessary. The two decisions stay separate: the manifest determines whether the URL may match, while the response generator chooses how to produce an admitted response. We do not share mutable fallback-mode state between those stages. ### Preserved exceptions and adapter behavior - **Server Actions:** forwarding can invoke a worker at `/products/[slug]`, not a concrete page URL. Action calls retain their existing exemption from page admission checks. A retained action must still work after the user navigates away from its closed route. - **Draft mode:** an unpublished URL can be previewed without admitting it publicly or replacing its public cache entry. - **Minimal mode:** the platform router remains responsible for admission before invoking the renderer. - **Adapters:** rendering a routing 404 now happens outside the response cache. The previous placement could successfully send a 404 and then throw `cache entry required but not generated` because it returned no cache entry for a non-null cache key. The new adapter fixture reproduces this on the parent runtime. This uses the existing prerender manifest and does not change its format or the adapter contract. The most specific matched dynamic route continues to determine the policy for open URLs, so specialized matchers can admit an open suffix without admitting an unknown closed prefix. ## Regression coverage Real fixtures cover: - Repeated invalidation, ordinary and optional catch-all paths, time-based ISR, concurrent regeneration, and serving a stale response while regenerating in the background. - Empty external caches and an external cache retained across two real builds. The latter verifies that the removed path's old entry still exists but is never consulted. - Document, HEAD, navigation, prefetch, bot, rewritten, and overlapping-route requests. - Direct and forwarded Server Actions, draft previews, and direct minimal-mode renderer invocations. - Adapter-specific 404 handling and regeneration of admitted paths. - The distinction between routing rejection and content that calls `notFound()`: an admitted URL can transition from 200 to 404 and back, including when its build render returned 404. These legacy `dynamicParams` fixtures are excluded from the Cache Components matrix because that configuration does not support the API. ## Stack This is an unflagged correctness fix, placed after #98950 and before the parameter-matching API in #97393. It can land without committing to that API. The API's mixed-prefix and dynamic-tail lifecycle coverage belongs in the API layer. ## Verification - Before the original fix, the two initial regression fixtures failed 11 checks while the open-route and time-based controls passed. - Against the parent runtime, the new adapter fixture reproduces the cache-entry invariant on document, navigation, prefetch, and bot requests, plus the invalidation regression. - The retained Server Action regression passes on the parent runtime, fails on the earlier PR revision without the exemption, and passes with the explicit action exemption. It also passes in development with the exemption. - Local production runs before replaying the follow-up onto the rebased stack: all 49 tests across seven suites pass in both Webpack and fresh-native Turbopack, including the existing closed-parameter navigation suite. The follow-up patch applied unchanged; CI will verify the rebased head. - Development Turbopack request-mode coverage: 10 passed; the public-cache reuse assertion is production-only. - Repository TypeScript, Next.js declaration generation, changed-file formatting/ESLint, and `git diff --check` pass. No live deployment verification. A separate `revalidate-if-generated` hang with an always-empty custom cache reproduces on both this PR and its parent; its diagnostic is retained for a separate follow-up, not included or silently skipped here. <!-- NEXT_JS_LLM -->
Author
Parents
Loading