[PR #987] [MERGED] fix(routing): omit empty optional catch-all params #1004

Closed
opened 2026-05-06 13:11:31 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/987
Author: @NathanDrake2406
Created: 4/30/2026
Status: Merged
Merged: 4/30/2026
Merged by: @james-elicx

Base: mainHead: nathan/fix-optional-catchall-params


📝 Commits (1)

  • f5c01d3 fix(routing): omit empty optional catch-all params

📊 Changes

5 files changed (+69 additions, -19 deletions)

View changed files

📝 packages/vinext/src/routing/route-trie.ts (+11 -6)
📝 packages/vinext/src/server/app-rsc-route-matching.ts (+11 -4)
📝 tests/app-rsc-route-matching.test.ts (+24 -3)
📝 tests/route-trie.test.ts (+20 -3)
📝 tests/routing.test.ts (+3 -3)

📄 Description

What this changes

Optional catch-all routes now omit the param key when the route matches with zero remaining segments. For example, /shop matching /shop/[[...path]] now produces {} instead of { path: [] }, while /shop/a/b still produces { path: ["a", "b"] }.

Why

Next.js treats the zero-segment optional catch-all case as a missing capture, not as an empty captured array. The relevant source path is:

  • route-regex.ts emits an optional repeated capture for optional catch-all params.
  • route-matcher.ts only writes a param when the regex capture is defined.
  • get-dynamic-param.test.ts covers the downstream app-router interpretation of an absent optional catch-all param as a null tree segment.

vinext's route trie instead materialized the zero-segment optional catch-all as [], which made app/pages route params observably diverge from Next.js.

Approach

The fix keeps app shape in codegen and behavior in normal modules:

  • Update the shared route trie so the zero-segment optional catch-all branch returns an empty params object while non-empty optional catch-all matches still assign the remaining segment array.
  • Apply the same absent-key semantics to standalone app RSC pattern matching used outside the route trie.
  • Add regression coverage at the trie boundary, the app RSC matcher boundary, and the pages/app route fixture boundary, including hyphenated param names.

Validation

  • vp test run tests/route-trie.test.ts tests/app-rsc-route-matching.test.ts tests/routing.test.ts
  • vp test run tests/pages-router.test.ts tests/app-router.test.ts -t "optional catch-all"
  • vp check packages/vinext/src/routing/route-trie.ts packages/vinext/src/server/app-rsc-route-matching.ts tests/route-trie.test.ts tests/app-rsc-route-matching.test.ts tests/routing.test.ts

Risks / follow-ups

This is a compatibility change for the zero-segment optional catch-all params shape. Code that depended on vinext's previous [] value for missing optional catch-all params will need to use the same params.slug ?? [] pattern that works in Next.js.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/987 **Author:** [@NathanDrake2406](https://github.com/NathanDrake2406) **Created:** 4/30/2026 **Status:** ✅ Merged **Merged:** 4/30/2026 **Merged by:** [@james-elicx](https://github.com/james-elicx) **Base:** `main` ← **Head:** `nathan/fix-optional-catchall-params` --- ### 📝 Commits (1) - [`f5c01d3`](https://github.com/cloudflare/vinext/commit/f5c01d3bb149323a8ab90fa85266e335c15d49d6) fix(routing): omit empty optional catch-all params ### 📊 Changes **5 files changed** (+69 additions, -19 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/routing/route-trie.ts` (+11 -6) 📝 `packages/vinext/src/server/app-rsc-route-matching.ts` (+11 -4) 📝 `tests/app-rsc-route-matching.test.ts` (+24 -3) 📝 `tests/route-trie.test.ts` (+20 -3) 📝 `tests/routing.test.ts` (+3 -3) </details> ### 📄 Description ## What this changes Optional catch-all routes now omit the param key when the route matches with zero remaining segments. For example, `/shop` matching `/shop/[[...path]]` now produces `{}` instead of `{ path: [] }`, while `/shop/a/b` still produces `{ path: ["a", "b"] }`. ## Why Next.js treats the zero-segment optional catch-all case as a missing capture, not as an empty captured array. The relevant source path is: - [`route-regex.ts`](https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/route-regex.ts#L120) emits an optional repeated capture for optional catch-all params. - [`route-matcher.ts`](https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/route-matcher.ts#L33-L42) only writes a param when the regex capture is defined. - [`get-dynamic-param.test.ts`](https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/get-dynamic-param.test.ts#L159-L167) covers the downstream app-router interpretation of an absent optional catch-all param as a `null` tree segment. vinext's route trie instead materialized the zero-segment optional catch-all as `[]`, which made app/pages route params observably diverge from Next.js. ## Approach The fix keeps app shape in codegen and behavior in normal modules: - Update the shared route trie so the zero-segment optional catch-all branch returns an empty params object while non-empty optional catch-all matches still assign the remaining segment array. - Apply the same absent-key semantics to standalone app RSC pattern matching used outside the route trie. - Add regression coverage at the trie boundary, the app RSC matcher boundary, and the pages/app route fixture boundary, including hyphenated param names. ## Validation - `vp test run tests/route-trie.test.ts tests/app-rsc-route-matching.test.ts tests/routing.test.ts` - `vp test run tests/pages-router.test.ts tests/app-router.test.ts -t "optional catch-all"` - `vp check packages/vinext/src/routing/route-trie.ts packages/vinext/src/server/app-rsc-route-matching.ts tests/route-trie.test.ts tests/app-rsc-route-matching.test.ts tests/routing.test.ts` ## Risks / follow-ups This is a compatibility change for the zero-segment optional catch-all params shape. Code that depended on vinext's previous `[]` value for missing optional catch-all params will need to use the same `params.slug ?? []` pattern that works in Next.js. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:11:31 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#1004
No description provided.