You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Given I declare two separate routes with the same path
When, I run go_router_builder
Then, I want to know about my mistake at build time so I can fix it. Otherwise, I run into unexpected behavior at runtime.
Current behavior
First declared wins, but not always. I tested all three cases against go_router 17.3.0.
Duplicate leaf paths: the first wins, the second is dead. Configuration._getLocRouteMatches walks the root routes in declaration order and returns the first non-empty result (configuration.dart:395-405). Children do the same and break on the first hit (match.dart:264-277). Two details leaves under /home, and /home/details renders the first.
Paths differing only in parameter name: same, first wins. /meal/:id before /meal/:mealId, navigating to /meal/7 renders the first and populates id with 7. The second builder never runs, so mealId is never read. A failed attempt doesn't leak parameters either, since each route stages them in a temp map and merges only once it's part of the result (match.dart:216-225).
Duplicate paths with different children: the second is reachable. This is the one that breaks the simple story. Matching backtracks. A route that matches a prefix but whose children can't complete the URL returns empty (match.dart:278-282), and the sibling loop keeps going. So with:
/home
details -> [a]
details -> [b]
/home/details/b reaches child b through the second details. I confirmed it renders. /home/details on its own still resolves to the first.
Proposal
Detect the conflict in go_router_builder and report it at build time. Two routes conflict when they compete for the same URL namespace and their paths normalize to the same pattern, so meal/:id and meal/:mealId count as one pattern.
Resolve namespaces per level of the route tree. Shell routes and StatefulShellRoute branches own no path, so the routes inside them compete with the routes around them. Relative routes own their path like any other route. Separate annotations compete, since $appRoutes collects them into one list. A library includes its part files, which covers an app's whole route table given it has to live in one library today ([go_router_builder] Define routes in separate files #122258).
Report rather than fail, by default. The third case above works at runtime, because matching backtracks, so a hard error would fail the build of an app that runs correctly. Same for one route class declared twice with different children, which is a way to group children by feature area. Every route still generates and the author decides what to do.
Make the severity configurable with a duplicate_route_paths builder option, accepting warning (the default), error, and ignore. Teams whose duplicates are always mistakes get a hard failure, and teams using the grouping pattern set ignore. Package-wide, with no per-route escape hatch.
Use case
As a developer
go_router_builderCurrent behavior
First declared wins, but not always. I tested all three cases against
go_router17.3.0.Duplicate leaf paths: the first wins, the second is dead.
Configuration._getLocRouteMatcheswalks the root routes in declaration order and returns the first non-empty result (configuration.dart:395-405). Children do the same and break on the first hit (match.dart:264-277). Twodetailsleaves under/home, and/home/detailsrenders the first.Paths differing only in parameter name: same, first wins.
/meal/:idbefore/meal/:mealId, navigating to/meal/7renders the first and populatesidwith 7. The second builder never runs, somealIdis never read. A failed attempt doesn't leak parameters either, since each route stages them in a temp map and merges only once it's part of the result (match.dart:216-225).Duplicate paths with different children: the second is reachable. This is the one that breaks the simple story. Matching backtracks. A route that matches a prefix but whose children can't complete the URL returns empty (
match.dart:278-282), and the sibling loop keeps going. So with:/home/details/breaches childbthrough the seconddetails. I confirmed it renders./home/detailson its own still resolves to the first.Proposal
go_router_builderand report it at build time. Two routes conflict when they compete for the same URL namespace and their paths normalize to the same pattern, someal/:idandmeal/:mealIdcount as one pattern.StatefulShellRoutebranches own no path, so the routes inside them compete with the routes around them. Relative routes own their path like any other route. Separate annotations compete, since$appRoutescollects them into one list. A library includes itspartfiles, which covers an app's whole route table given it has to live in one library today ([go_router_builder] Define routes in separate files #122258).duplicate_route_pathsbuilder option, acceptingwarning(the default),error, andignore. Teams whose duplicates are always mistakes get a hard failure, and teams using the grouping pattern setignore. Package-wide, with no per-route escape hatch.go_router_builder's own example app registeredDoubleRoutetwice since [go_router_builder] Adds support for enhanced enums. packages#2395 (2022) andDoubleExtensionRoutetwice since [go_router_builder] Support extension types packages#9458 (2025). Both duplicates were unreachable, and removing them changed nothing but the dead entries in the generated route list.