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 -->