Skip to content

Preserve closed-route admission across cache misses - #98964

Draft
gnoff wants to merge 2 commits into
codex/root-param-shell-cachefrom
codex/closed-route-revalidation
Draft

gnoff wants to merge 2 commits into
codex/root-param-shell-cachefrom
codex/closed-route-revalidation

Conversation

@gnoff

@gnoff gnoff commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Summary

The latest review follow-up (Server Action exemption and expanded fixtures) is verified locally but has not yet been pushed. The verification below distinguishes those local results from the published head.

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 published PR head, and passes with the local explicit action exemption. It also passes in development with the exemption.
  • Final local production runs: all 49 tests across seven suites pass in both Webpack and fresh-native Turbopack, including the existing closed-parameter navigation suite.
  • 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.

@github-actions

github-actions Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Tests Passed

Commit: 78df9b6

gnoff added a commit that referenced this pull request Sep 20, 2026
Bring in the standalone closed-route revalidation fix from #98964 and use the same pre-cache admission boundary for the API-specific development matching result. Preserve the separate normal development render path and the selected specialized matcher policy in production.

Extend the routing fixture to revalidate build-seeded and runtime-generated outputs beneath closed prefixes, invalidate a shared dynamic-tail shell without capturing either request value, and prove invalidating unlisted URLs never admits them. These API-only checks remain in this layer rather than the unflagged prerequisite.

The new lifecycle checks and existing runtime-policy suite pass with fresh-native Turbopack and Webpack with Partial Prefetching disabled. The two previously identified percent-encoding failures remain unchanged; fully closed revalidation and expected routing-miss logging now pass.
@gnoff
gnoff added this pull request to stack #98967 September 20, 2026 20:01
Determine whether a URL is admitted from the build manifest before looking up its ISR entry. A build-generated /closed/known must remain valid after revalidatePath expires its content or an external cache loses the entry; an unlisted URL must be rejected without a cache lookup or page render.

Regenerate admitted fallback-false paths with a blocking render instead of treating their missing content as an unknown route. Cache contents no longer authorize otherwise closed paths during on-demand revalidation. This uses existing manifest metadata and does not change the adapter contract.

Add real production fixtures covering repeated invalidation, catch-all and optional catch-all paths, time-based ISR, build- and runtime-rendered notFound results becoming available again, open routes first generated at runtime, and empty custom caches. Document, navigation, and bot requests verify that unlisted paths do not consult the route cache.
These suites exercise dynamicParams = false, which is unsupported with Cache Components. Exclude them from the CC test matrix rather than overriding that job's cached-navigation defaults in the fixtures.

Verified with the CI test runner's selection mode: neither suite is selected by the CC manifest, while both remain selected in the normal production run. Runtime code and test assertions are unchanged.
@gnoff
gnoff force-pushed the codex/closed-route-revalidation branch from e6e5ae6 to 78df9b6 Compare September 21, 2026 06:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant