#3406 Index route is overwritten by wildcard route
Bug confirmed on wrangler 4.60.0. Root index route (/) incorrectly handled by catch-all [[fallback]].js due to segment-count sorting in compareRoutes().
Keep open - valid routing priority bug needs fix in filepath-routing.ts
Analysis Report
Issue Review: cloudflare/workers-sdk#3406
Summary
Pages Functions routing incorrectly prioritizes catch-all wildcard routes ([[fallback]].js) over the root index route (index.js), causing / to be handled by the wrong function.
Findings
- Created: 2023-06-05
- Updated: 2025-10-30
- Version: Wrangler 3.0.1 -> 4.60.0
- Component: Pages Functions (filepath-routing)
- Labels: bug, pages
- Comments: 0
Key Evidence
Issue is well-documented: The reporter correctly identified the root cause in the code - the
compareRoutesfunction sorts routes by segment count, but doesn't account for the root index route having 0 segments.No fix found:
- No PRs reference issue #3406
- No changelog entries mention this issue
- The relevant code in
filepath-routing.tsappears unchanged
Bug reproduced on wrangler 4.60.0:
File structure: functions/index.js functions/about.js functions/[name].js functions/[[fallback]].js Results: GET / -> [[fallback]].js (BUG - should be index.js) GET /about -> about.js (correct) GET /something -> [name].js (correct) GET /something/else -> [[fallback]].js (correct)Root cause confirmed: The sorting algorithm puts routes with more segments first. Since
index.jsbecomes/(0 segments) and[[fallback]].jsbecomes/:fallback*(1 segment), the catch-all comes first and matches/before the index route.Note: The issue author stated
/maps to[name].js, but testing shows it actually maps to[[fallback]].js. The[name].jspattern (:name) requires at least one character and won't match/, but the catch-all[[fallback]].jspattern (:fallback*) matches zero or more segments and incorrectly captures/.
Recommendation
Status: KEEP OPEN
Reasoning: The bug is confirmed to still exist in wrangler 4.60.0. The issue has a clear root cause analysis with code references, and can be reproduced with a minimal example. This is a legitimate routing priority bug that affects users with both index routes and catch-all routes.
Action: Keep open. This is a valid bug that needs engineering attention. A fix would need to modify the compareRoutes function in packages/wrangler/src/pages/functions/filepath-routing.ts to ensure the root index route takes precedence over catch-all wildcards.
Suggested Comment
This issue has been verified as still occurring on wrangler 4.60.0. Testing confirms that
GET /incorrectly routes to[[fallback]].jsinstead ofindex.jswhen both files exist.Note: The original report mentioned it maps to
[name].js, but testing shows it actually maps to[[fallback]].js- the[name].jspattern requires at least one character, while[[fallback]].js(catch-all) matches zero or more path segments.The root cause is in
compareRoutes()- routes are sorted by segment count (more segments first), so/:fallback*(1 segment) comes before/(0 segments). Since catch-all patterns match empty paths, the wildcard intercepts requests to/before the index route.
Solution Recommendation
Root Cause Analysis
The compareRoutes() function in filepath-routing.ts:118-165 sorts routes by segment count first:
// sort routes with fewer segments after those with more segments
if (segmentsA.length !== segmentsB.length) {
return segmentsB.length - segmentsA.length;
}
The problem:
index.js→ routePath/→ 0 segments[[fallback]].js→ routePath/:fallback*→ 1 segment
Since routes with more segments come first, the catch-all (/:fallback*) is evaluated before the index route (/), and since * matches zero-or-more characters, it captures requests to /.
Proposed Solution
Add special handling in compareRoutes() to ensure the root index route (/) always takes precedence over catch-all wildcards at the root level.
export function compareRoutes(
{ routePath: routePathA, method: methodA }: RouteConfig,
{ routePath: routePathB, method: methodB }: RouteConfig
) {
function parseRoutePath(routePath: UrlPath): string[] {
return routePath.slice(1).split("/").filter(Boolean);
}
const segmentsA = parseRoutePath(routePathA);
const segmentsB = parseRoutePath(routePathB);
// NEW: Root index route (/) should come before root-level catch-all wildcards
const isRootIndexA = segmentsA.length === 0;
const isRootIndexB = segmentsB.length === 0;
const isRootCatchAllA = segmentsA.length === 1 && segmentsA[0].includes("*");
const isRootCatchAllB = segmentsB.length === 1 && segmentsB[0].includes("*");
// Root index takes precedence over root-level catch-all
if (isRootIndexA && isRootCatchAllB) return -1;
if (isRootCatchAllA && isRootIndexB) return 1;
// ... rest of existing logic unchanged
}
Alternative (More Comprehensive) Solution
A more robust fix would handle this at any nesting level, not just the root. The principle: an exact route should always beat a catch-all sibling.
export function compareRoutes(
{ routePath: routePathA, method: methodA }: RouteConfig,
{ routePath: routePathB, method: methodB }: RouteConfig
) {
function parseRoutePath(routePath: UrlPath): string[] {
return routePath.slice(1).split("/").filter(Boolean);
}
const segmentsA = parseRoutePath(routePathA);
const segmentsB = parseRoutePath(routePathB);
// NEW: Check if one route is a "parent" of a catch-all route
// e.g., "/" vs "/:fallback*" or "/foo" vs "/foo/:rest*"
const aIsPrefixOfB = routePathB.startsWith(routePathA.replace(/\/$/, "") + "/") ||
(routePathA === "/" && segmentsB.length > 0);
const bIsPrefixOfA = routePathA.startsWith(routePathB.replace(/\/$/, "") + "/") ||
(routePathB === "/" && segmentsA.length > 0);
// If A is a prefix of B and B ends with a catch-all, A should come first
if (aIsPrefixOfB && segmentsB.some(s => s.includes("*"))) return -1;
if (bIsPrefixOfA && segmentsA.some(s => s.includes("*"))) return 1;
// ... rest of existing logic unchanged
}
Implementation Difficulty: EASY
| Factor | Assessment |
|---|---|
| Lines of code | ~5-10 lines added |
| Risk | Low - isolated change to sorting logic |
| Testing | Easy to unit test with existing test infrastructure |
| Scope | Single file (filepath-routing.ts) |
| Breaking changes | None - this fixes incorrect behavior |
Testing Recommendations
Add test cases to verify:
GET /→index.js(not[[fallback]].js)GET /foo→[name].js(not[[fallback]].js)GET /foo/bar→[[fallback]].js(correct catch-all behavior)- Nested scenarios:
/api/with bothapi/index.jsandapi/[[...rest]].js
Files to Modify
packages/wrangler/src/pages/functions/filepath-routing.ts- Add the fixpackages/wrangler/src/pages/functions/filepath-routing.test.ts- Add test cases
Notes & Feedback (0)
No notes yet.