[GH-ISSUE #1099] App Router: client-side dynamic param parsing should canonicalize URL pathname parts to match server encoding #235

Open
opened 2026-05-06 12:38:25 +02:00 by BreizhHardware · 0 comments

Originally created by @github-actions[bot] on GitHub (May 6, 2026).
Original GitHub issue: https://github.com/cloudflare/vinext/issues/1099

Tracking Next.js fix vercel/next.js#93491 (commit 47bcfa0).

Summary

When deriving segment cache keys for dynamic params on the client, Next.js's parseDynamicParamFromURLPart was applying encodeURIComponent to pathname parts that come from URL.pathname.split('/'). Those parts are already in the percent-encoded form the URL parser produces, so the call double-encoded — turning %2F into %252F and so on. The server-side equivalent (get-dynamic-param.ts) starts from a decoded param value and applies encodeURIComponent once, so the two encodings only matched for inputs without any percent sequences.

The fix introduces a canonicalizeURLPart helper that decodes first and re-encodes:

function canonicalizeURLPart(part: string): string {
  try {
    return encodeURIComponent(decodeURIComponent(part))
  } catch {
    return part
  }
}

The decode step also handles characters like ,, :, and + that the URL parser leaves untouched but encodeURIComponent percent-encodes; simply dropping encodeURIComponent would silently mismatch on those.

Impact in Next.js

When client and server param encodings disagreed:

  • writeDynamicDataIntoNavigationTask returned didReceiveUnknownParallelRoute
  • finishNavigationTask fell through to dispatchRetryDueToTreeMismatch
  • That retry called invalidateRouteCacheEntries, bumping the route cache version and invalidating every entry
  • The next prefetch missed the cache and fired a fresh next-router-segment-prefetch request — visible regression on encoded URLs (e.g. /foo%2Fbar) during back navigation after a <Link> re-mount

Relevance to vinext

vinext implements App Router client navigation and its own route param plumbing. Audit our equivalent of parseDynamicParamFromURLPart (or wherever we derive params from URL.pathname.split('/') parts on the client) and confirm:

  1. We don't double-encode parts that already came out of the URL parser
  2. Client and server produce identical canonical forms for params containing %xx sequences (especially %2F), ,, :, +
  3. Catchall, optional catchall, dynamic, and intercepted variants are all handled

Test case to port

The PR adds test/e2e/app-dir/segment-cache/encoded-slash-params/ exercising prefetch → click → back against /foo and /foo%2Fbar. With the bug, the encoded variant fires a route-tree request on the back-nav <Link> reveal while the unencoded variant does not. Port an equivalent fixture + assertion once we audit the param parsing path.

References

Originally created by @github-actions[bot] on GitHub (May 6, 2026). Original GitHub issue: https://github.com/cloudflare/vinext/issues/1099 Tracking Next.js fix [vercel/next.js#93491](https://github.com/vercel/next.js/pull/93491) (commit [47bcfa0](https://github.com/vercel/next.js/commit/47bcfa0956679c2a5fea0b941b76bb2d69878d9c)). ## Summary When deriving segment cache keys for dynamic params on the client, Next.js's `parseDynamicParamFromURLPart` was applying `encodeURIComponent` to pathname parts that come from `URL.pathname.split('/')`. Those parts are already in the percent-encoded form the URL parser produces, so the call double-encoded — turning `%2F` into `%252F` and so on. The server-side equivalent (`get-dynamic-param.ts`) starts from a decoded param value and applies `encodeURIComponent` once, so the two encodings only matched for inputs without any percent sequences. The fix introduces a `canonicalizeURLPart` helper that decodes first and re-encodes: ```ts function canonicalizeURLPart(part: string): string { try { return encodeURIComponent(decodeURIComponent(part)) } catch { return part } } ``` The decode step also handles characters like `,`, `:`, and `+` that the URL parser leaves untouched but `encodeURIComponent` percent-encodes; simply dropping `encodeURIComponent` would silently mismatch on those. ## Impact in Next.js When client and server param encodings disagreed: - `writeDynamicDataIntoNavigationTask` returned `didReceiveUnknownParallelRoute` - `finishNavigationTask` fell through to `dispatchRetryDueToTreeMismatch` - That retry called `invalidateRouteCacheEntries`, bumping the route cache version and invalidating every entry - The next prefetch missed the cache and fired a fresh `next-router-segment-prefetch` request — visible regression on encoded URLs (e.g. `/foo%2Fbar`) during back navigation after a `<Link>` re-mount ## Relevance to vinext vinext implements App Router client navigation and its own route param plumbing. Audit our equivalent of `parseDynamicParamFromURLPart` (or wherever we derive params from `URL.pathname.split('/')` parts on the client) and confirm: 1. We don't double-encode parts that already came out of the URL parser 2. Client and server produce identical canonical forms for params containing `%xx` sequences (especially `%2F`), `,`, `:`, `+` 3. Catchall, optional catchall, dynamic, and intercepted variants are all handled ## Test case to port The PR adds `test/e2e/app-dir/segment-cache/encoded-slash-params/` exercising prefetch → click → back against `/foo` and `/foo%2Fbar`. With the bug, the encoded variant fires a route-tree request on the back-nav `<Link>` reveal while the unencoded variant does not. Port an equivalent fixture + assertion once we audit the param parsing path. ## References - Next.js PR: https://github.com/vercel/next.js/pull/93491 - Commit: https://github.com/vercel/next.js/commit/47bcfa0956679c2a5fea0b941b76bb2d69878d9c - Files changed: `packages/next/src/client/route-params.ts` - Test fixture: `test/e2e/app-dir/segment-cache/encoded-slash-params/`
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#235
No description provided.