Skip to content

fix(qwik-router): backtrack from static prefixes and rank matches by specificity - #8899

Open
blakeley wants to merge 1 commit into
QwikDev:mainfrom
blakeley:fix/router-matcher-backtracking
Open

fix(qwik-router): backtrack from static prefixes and rank matches by specificity#8899
blakeley wants to merge 1 commit into
QwikDev:mainfrom
blakeley:fix/router-matcher-backtracking

Conversation

@blakeley

@blakeley blakeley commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Fixes #8893

Two symptoms

  1. A static prefix dead-ends the match. The matcher walks the route trie greedily: at each segment it takes the first child that matches (exact → _M groups → _W_A) and never reconsiders. So with routes/(marketing)/pricing/index.tsx and routes/(app)/[a]/[b]/[c]/index.tsx, the URL /pricing/x/y matches pricing on the first segment, dead-ends under it, and 404s — even though [a]/[b]/[c] matches the whole URL.

  2. No specificity ranking across groups. When two branches both match a URL, the winner is whichever the trie happens to be walked into first, so it depends on group ordering. [x]/static.xml in one group and [a]/[b] in another both match /foo/static.xml, and which one wins is an accident of _M order rather than the more specific route.

Approach

matchRouteTree now resolves the best complete match up front, then replays it:

  • findChildAll returns every candidate child for a segment (in the same priority order findChild already walks) rather than just the first.
  • findMatchChain does a DFS over those candidates, only accepting a chain that consumes the whole pathname and lands on a node that actually has a route — so a static prefix that dead-ends no longer hides a dynamic sibling.
  • compareChains ranks complete chains lexicographically by per-segment kind (static < dynamic < catch-all), so the most specific match wins regardless of group order.

When no chain matches, findMatchChain returns undefined and the segment loop runs exactly as before — the greedy walk, the rest-wildcard fallback, and the 404/boundary recovery are untouched, so unmatched URLs keep producing the same routeParts, params, and error boundaries they do today.

One existing rule is preserved deliberately: an exact route subtree still owns its unmatched descendants, so a dead end under a static route 404s rather than being swallowed by a sibling [...rest] (covered by the existing exact child dead end does not fall back to sibling _M catchall test).

Repro

Minimal reproduction: https://github.com/blakeley/router-matcher-no-backtracking

Tests

Two cases added to packages/qwik-router/src/runtime/src/routing.unit.ts, both of which fail on main and pass with this change:

  • a static single-segment route in one group and [a]/[b]/[c] in another — /pricing/x/y reaches the dynamic route instead of 404ing
  • [x]/static.xml beats [a]/[b] for /foo/static.xml in both group orders

The rest of the existing matcher suite (the static route itself still winning, plain dynamic matches, catch-all fallback, and an exact dead end not falling through to a sibling catch-all) is unchanged and still green, which is what pins the behaviour this change must not alter.

This restores the documented Qwik 1 routing semantics, where static pages and dynamic route families can share a URL prefix and the most specific route wins.

@blakeley
blakeley requested a review from a team as a code owner August 1, 2026 00:02
@changeset-bot

changeset-bot Bot commented Aug 1, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 96c8091

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@qwik.dev/router Patch
eslint-plugin-qwik Patch
@qwik.dev/core Patch
create-qwik Patch
@qwik.dev/react Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Aug 1, 2026

Copy link
Copy Markdown

Open in StackBlitz

@qwik.dev/core

npm i https://pkg.pr.new/QwikDev/qwik/@qwik.dev/core@8899

@qwik.dev/router

npm i https://pkg.pr.new/QwikDev/qwik/@qwik.dev/router@8899

eslint-plugin-qwik

npm i https://pkg.pr.new/QwikDev/qwik/eslint-plugin-qwik@8899

create-qwik

npm i https://pkg.pr.new/QwikDev/qwik/create-qwik@8899

@qwik.dev/optimizer

npm i https://pkg.pr.new/QwikDev/qwik/@qwik.dev/optimizer@8899

@qwik.dev/devtools

npm i https://pkg.pr.new/QwikDev/qwik/@qwik.dev/devtools@8899

commit: 96c8091

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.

[🐞] v2: route matcher doesn't backtrack — a static route prefix 404s deeper dynamic routes; no specificity ranking across groups

1 participant