Skip to content

Fix dev shell validation for novel parameter values - #98460

Draft
gnoff wants to merge 1 commit into
canaryfrom
codex/fix-dev-param-validation
Draft

Fix dev shell validation for novel parameter values#98460
gnoff wants to merge 1 commit into
canaryfrom
codex/fix-dev-param-validation

Conversation

@gnoff

@gnoff gnoff commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Fix Cache Components development validation so it checks the most-specific parameter shape produced by generateStaticParams, regardless of the literal parameter values in the request.

This is a standalone fix for existing behavior, with no experimental matching API or feature flag. It forms the bottom of the parameter-matching stack. #97393 builds on this separation to let explicit fallback directives choose an earlier validation boundary, so this fix can land without committing to the new API.

Why

The dev server currently chooses fallback parameters by matching the requested URL against the generated paths. That is appropriate for staging the foreground response, but that same selection also reaches background static-shell validation. A novel value therefore causes validation to check a more generic shell than the build requires.

For /[top]/items/[bottom], suppose generateStaticParams returns:

return [{ top: 't1' }]

The build validates /t1/items/[bottom]: top is available, while bottom remains dynamic. A dev request for /t2/items/b2 should exercise the same shape using its actual top: 't2' value. It should not require the shell to work with both top and bottom unknown.

Generated example Dev request Unknown parameters during static-shell validation
{ top: 't1', bottom: 'b1' } /t2/items/b2 None
{ top: 't1' } /t2/items/b2 bottom
No examples /t2/items/b2 top, bottom

This restores the value-independent validation semantics that were lost when #95066 made foreground staging value-sensitive. It does not revert that foreground staging change.

Changes

  • Select the smallest fallback-parameter set across all generated paths for validation, separately from the per-URL set used by the foreground render.
  • Pass that validation shape through dev-only request metadata.
  • When the two shapes differ, construct a separate validation context and request store. Do not reuse Flight chunks produced with the foreground parameter shape; use the existing background validation render instead.
  • Require an explicit render context at every dev payload call site, rather than defaulting to a captured foreground context or introducing a separate validation wrapper.
  • Keep production rendering, build output, cache-miss routing, and the foreground dev response unchanged.
  • Strengthen the existing no-API fixture to wait for background validation before asserting success. Restore the novel-top error expectation to the page that reads the genuinely dynamic bottom, rather than the layout that reads top.
  • Limit this fixture to static-shell validation so automatic navigation-boundary insights do not obscure the behavior under test.

Verification

  • Reproduced two failing regression cases against the unchanged baseline, then passed the suite with the fix.
  • CI=1 pnpm build-all (uses the installed native package)
  • pnpm --filter=next types
  • ESLint and Prettier for the changed files
  • pnpm test-dev-webpack test/development/app-dir/cache-components-dev-fallback-validation/cache-components-dev-fallback-validation.test.ts
  • pnpm test-dev-turbo test/development/app-dir/cache-components-dev-fallback-validation/cache-components-dev-fallback-validation.test.ts
  • pnpm test-dev-webpack test/development/app-dir/cache-components-dev-warmup -t 'mixed static and fallback params resolve in the correct phase' (24 checks across eight configurations; also passed against the unchanged baseline)

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Failing test suites

Commit: 36e32f2 | About building and testing Next.js

pnpm test-dev test/development/app-dir/instant-navs-devtools/instant-navs-devtools.test.ts (job)

  • instant-nav-panel > history traversals > re-arms the capture when navigating back from a captured SPA navigation (DD)
Expand output

● instant-nav-panel › history traversals › re-arms the capture when navigating back from a captured SPA navigation

locator.waitFor: Timeout 60000ms exceeded.
Call log:
  - waiting for locator('[data-testid="dynamic-skeleton"]') to be visible

  278 |     await browser
  279 |       .locator('[data-testid="dynamic-skeleton"]')
> 280 |       .waitFor({ state: 'visible' })
      |        ^
  281 |     await browser
  282 |       .locator('[data-testid="param-skeleton"]')
  283 |       .waitFor({ state: 'visible' })

  at waitFor (development/app-dir/instant-navs-devtools/instant-navs-devtools.test.ts:280:8)
  at Object.expectTargetPageSpaShell (development/app-dir/instant-navs-devtools/instant-navs-devtools.test.ts:834:13)

gnoff added a commit that referenced this pull request Sep 9, 2026
## Summary

Add experimental parameter-matching APIs behind `experimental.paramMatching` so Cache Components can separate build-time prerender selection from runtime cache-miss behavior.

- `generateStaticParams` continues to select concrete parameter combinations to prerender during the build.
- `experimental_paramMatching` and `experimental_generateParamMatching` configure each visible route parameter as `not-found`, `blocking`, `fallback`, or permanently `dynamic`.
- Parameters without an explicit directive retain the existing static-shell heuristics.

## Stack

Builds on #98460, which restores value-independent dev shell validation without introducing an API. This layer adds explicit policy boundaries on top of that baseline; the shared render/validation separation is owned by the base PR.

## Example

For `/[lang]/catalog/[top]/items/[bottom]`:

```ts
// app/[lang]/layout.tsx
export const experimental_paramMatching = {
  lang: 'not-found',
} as const

// app/[lang]/catalog/[top]/items/[bottom]/page.tsx
export const experimental_paramMatching = {
  top: 'blocking',
  bottom: 'fallback',
} as const

export function generateStaticParams() {
  return [{ lang: 'en', top: 't1', bottom: 'b1' }]
}
```

This makes an unknown `lang` a 404, blocks while generating a novel `top`, immediately serves fallback UI for a novel `bottom`, and retains `/en/catalog/t1/items/b1` as a build-time prerender.

## Composition and validation

- A layout or page may configure only parameters visible at or above its own segment.
- Static and generated fragments run independently and are merged from ancestors toward the page using descendant-wins assignment.
- Next.js validates the merged explicit policy exactly as written; it never rewrites neighboring inherited directives to make an override coherent.
- Parameters must progress in `not-found -> blocking -> fallback -> dynamic` order.
- Parallel branches that contribute to the same URL matcher must agree on shared parameter policy and produce a coherent combined policy.
- A parameter configured as `dynamic` cannot participate in `generateStaticParams` output.
- After explicit policy is merged and validated, unconfigured holes are filled using existing build-time shell inference.

## Static-shell behavior

An explicit `blocking` or `fallback` directive does not require `generateStaticParams` to provide an example for that parameter. The build validates the most-specific generic shell reachable from the available examples:

- Explicit fallback shells must be non-empty and are retained for immediate use on a miss.
- Generic blocking shells may be rendered only for validation and are never registered as concrete cache outputs.
- A more-specific generated shell supersedes an unnecessary generic blocking validation.
- `instant = false` opts out of the static-shell requirement.

Development uses the same specificity model while anonymizing literal parameter values: it considers whether each parameter is present, not whether that value appeared during the build. Ordinary development requests remain dynamic renders rather than entering the production prerender path.

## Deployment and coverage

Closed `not-found` prefixes reuse the existing fallback-false adapter contract, so the initial implementation works with the current Vercel routing layer without a proxy change.

The fixture matrix covers inherited and overridden policy, inferred holes, no-example blocking and fallback behavior, catch-all and optional catch-all routes, parallel slots, foreground cache behavior, development validation specificity, export constraints, type generation, and invalid configurations.

The optional local matcher diagnostic remains isolated in the following commit so it can be removed before release.

## Verification

- `pnpm build-all`
- `pnpm --filter=next types`
- `pnpm jest packages/next/src/build/static-paths/app.test.ts --runInBand`
- `pnpm test-dev-turbo test/e2e/app-dir/generate-prerender-matching/generate-prerender-matching.test.ts`
- `pnpm test-start-turbo test/e2e/app-dir/generate-prerender-matching/generate-prerender-matching.test.ts`
- `pnpm test-start-webpack test/e2e/app-dir/generate-prerender-matching/generate-prerender-matching.test.ts`
@gnoff
gnoff added this pull request to stack #98461 September 9, 2026 22:02
## Summary

Fix Cache Components development validation so it checks the most-specific parameter shape produced by `generateStaticParams`, regardless of the literal parameter values in the request.

This is a standalone fix for existing behavior, with no experimental matching API or feature flag. It forms the bottom of the parameter-matching stack. #97393 builds on this separation to let explicit fallback directives choose an earlier validation boundary, so this fix can land without committing to the new API.

## Why

The dev server currently chooses fallback parameters by matching the requested URL against the generated paths. That is appropriate for staging the foreground response, but that same selection also reaches background static-shell validation. A novel value therefore causes validation to check a more generic shell than the build requires.

For `/[top]/items/[bottom]`, suppose `generateStaticParams` returns:

```ts
return [{ top: 't1' }]
```

The build validates `/t1/items/[bottom]`: `top` is available, while `bottom` remains dynamic. A dev request for `/t2/items/b2` should exercise the same shape using its actual `top: 't2'` value. It should not require the shell to work with both `top` and `bottom` unknown.

| Generated example | Dev request | Unknown parameters during static-shell validation |
| --- | --- | --- |
| `{ top: 't1', bottom: 'b1' }` | `/t2/items/b2` | None |
| `{ top: 't1' }` | `/t2/items/b2` | `bottom` |
| No examples | `/t2/items/b2` | `top`, `bottom` |

This restores the value-independent validation semantics that were lost when #95066 made foreground staging value-sensitive. It does not revert that foreground staging change.

## Changes

- Select the smallest fallback-parameter set across all generated paths for validation, separately from the per-URL set used by the foreground render.
- Pass that validation shape through dev-only request metadata.
- When the two shapes differ, construct a separate validation context and request store. Do not reuse Flight chunks produced with the foreground parameter shape; use the existing background validation render instead.
- Require an explicit render context at every dev payload call site, rather than defaulting to a captured foreground context or introducing a separate validation wrapper.
- Keep production rendering, build output, cache-miss routing, and the foreground dev response unchanged.
- Strengthen the existing no-API fixture to wait for background validation before asserting success. Restore the novel-`top` error expectation to the page that reads the genuinely dynamic `bottom`, rather than the layout that reads `top`.
- Limit this fixture to static-shell validation so automatic navigation-boundary insights do not obscure the behavior under test.

## Verification

- Reproduced two failing regression cases against the unchanged baseline, then passed the suite with the fix.
- `CI=1 pnpm build-all` (uses the installed native package)
- `pnpm --filter=next types`
- ESLint and Prettier for the changed files
- `pnpm test-dev-webpack test/development/app-dir/cache-components-dev-fallback-validation/cache-components-dev-fallback-validation.test.ts`
- `pnpm test-dev-turbo test/development/app-dir/cache-components-dev-fallback-validation/cache-components-dev-fallback-validation.test.ts`
- `pnpm test-dev-webpack test/development/app-dir/cache-components-dev-warmup -t 'mixed static and fallback params resolve in the correct phase'` (24 checks across eight configurations; also passed against the unchanged baseline)

<!-- NEXT_JS_LLM -->
gnoff added a commit that referenced this pull request Sep 9, 2026
## Summary

Add experimental parameter-matching APIs behind `experimental.paramMatching` so Cache Components can separate build-time prerender selection from runtime cache-miss behavior.

- `generateStaticParams` continues to select concrete parameter combinations to prerender during the build.
- `experimental_paramMatching` and `experimental_generateParamMatching` configure each visible route parameter as `not-found`, `blocking`, `fallback`, or permanently `dynamic`.
- Parameters without an explicit directive retain the existing static-shell heuristics.

## Stack

Builds on #98460, which restores value-independent dev shell validation without introducing an API. This layer adds explicit policy boundaries on top of that baseline; the shared render/validation separation is owned by the base PR.

## Example

For `/[lang]/catalog/[top]/items/[bottom]`:

```ts
// app/[lang]/layout.tsx
export const experimental_paramMatching = {
  lang: 'not-found',
} as const

// app/[lang]/catalog/[top]/items/[bottom]/page.tsx
export const experimental_paramMatching = {
  top: 'blocking',
  bottom: 'fallback',
} as const

export function generateStaticParams() {
  return [{ lang: 'en', top: 't1', bottom: 'b1' }]
}
```

This makes an unknown `lang` a 404, blocks while generating a novel `top`, immediately serves fallback UI for a novel `bottom`, and retains `/en/catalog/t1/items/b1` as a build-time prerender.

## Composition and validation

- A layout or page may configure only parameters visible at or above its own segment.
- Static and generated fragments run independently and are merged from ancestors toward the page using descendant-wins assignment.
- Next.js validates the merged explicit policy exactly as written; it never rewrites neighboring inherited directives to make an override coherent.
- Parameters must progress in `not-found -> blocking -> fallback -> dynamic` order.
- Parallel branches that contribute to the same URL matcher must agree on shared parameter policy and produce a coherent combined policy.
- A parameter configured as `dynamic` cannot participate in `generateStaticParams` output.
- After explicit policy is merged and validated, unconfigured holes are filled using existing build-time shell inference.

## Static-shell behavior

An explicit `blocking` or `fallback` directive does not require `generateStaticParams` to provide an example for that parameter. The build validates the most-specific generic shell reachable from the available examples:

- Explicit fallback shells must be non-empty and are retained for immediate use on a miss.
- Generic blocking shells may be rendered only for validation and are never registered as concrete cache outputs.
- A more-specific generated shell supersedes an unnecessary generic blocking validation.
- `instant = false` opts out of the static-shell requirement.

Development uses the same specificity model while anonymizing literal parameter values: it considers whether each parameter is present, not whether that value appeared during the build. Ordinary development requests remain dynamic renders rather than entering the production prerender path.

## Deployment and coverage

Closed `not-found` prefixes reuse the existing fallback-false adapter contract, so the initial implementation works with the current Vercel routing layer without a proxy change.

The fixture matrix covers inherited and overridden policy, inferred holes, no-example blocking and fallback behavior, catch-all and optional catch-all routes, parallel slots, foreground cache behavior, development validation specificity, export constraints, type generation, and invalid configurations.

The optional local matcher diagnostic remains isolated in the following commit so it can be removed before release.

## Verification

- `pnpm build-all`
- `pnpm --filter=next types`
- `pnpm jest packages/next/src/build/static-paths/app.test.ts --runInBand`
- `pnpm test-dev-turbo test/e2e/app-dir/generate-prerender-matching/generate-prerender-matching.test.ts`
- `pnpm test-start-turbo test/e2e/app-dir/generate-prerender-matching/generate-prerender-matching.test.ts`
- `pnpm test-start-webpack test/e2e/app-dir/generate-prerender-matching/generate-prerender-matching.test.ts`
@gnoff
gnoff force-pushed the codex/fix-dev-param-validation branch from 12fa6f5 to 36e32f2 Compare September 9, 2026 23:16
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