@@ -606,6 +606,12 @@ func (r *Router) propagateRoute(pathPrefix, namePrefix string, rt *core.Route) {
606606 r .nameIndex [newRoute .Name ] = newRoute
607607 }
608608
609+ // Also track in directRoutes and dispatchers for findMatchingRoute
610+ dispatcherKey := newRoute .Method + ":" + newRoute .ChiPattern
611+ if len (newRoute .QueryMatchers ) == 0 {
612+ r .directRoutes [dispatcherKey ] = newRoute
613+ }
614+
609615 // Propagate further up if this router also has parents
610616 for _ , p := range r .parents {
611617 p .router .propagateRoute (p .pathPrefix , p .namePrefix , newRoute )
@@ -700,36 +706,78 @@ func (r *Router) SubRouter(prefix string) *Router {
700706// findMatchingRoute manually matches a request against registered routes
701707// This is used as a fallback when Chi's RouteContext isn't available (e.g., in global middleware)
702708func (r * Router ) findMatchingRoute (method , path string ) * core.Route {
709+ type candidate struct {
710+ route * core.Route
711+ pattern string
712+ }
713+ var matches []candidate
714+
703715 // Check all direct routes
704716 for key , route := range r .directRoutes {
705717 if strings .HasPrefix (key , method + ":" ) {
706718 pattern := strings .TrimPrefix (key , method + ":" )
707719 if r .matchPattern (pattern , path ) {
708- return route
720+ matches = append ( matches , candidate { route : route , pattern : pattern })
709721 }
710722 }
711723 }
712724
713- // Check dispatcher routes (return fallback route for query-multiplexed)
725+ // Check dispatcher routes
714726 for key , disp := range r .dispatchers {
715727 if strings .HasPrefix (key , method + ":" ) {
716728 pattern := strings .TrimPrefix (key , method + ":" )
717729 if r .matchPattern (pattern , path ) {
718- // Return fallback route (no query matchers)
730+ // For dispatchers, we want the fallback route for name/action resolution in middleware
731+ // when we don't know the query params yet.
732+ var fallback * core.Route
719733 for _ , rt := range disp .Routes {
720734 if len (rt .QueryMatchers ) == 0 {
721- return rt
735+ fallback = rt
736+ break
722737 }
723738 }
724- // If no fallback, return first route
725- if len (disp .Routes ) > 0 {
726- return disp .Routes [0 ]
739+ if fallback == nil && len (disp .Routes ) > 0 {
740+ fallback = disp .Routes [0 ]
741+ }
742+ if fallback != nil {
743+ matches = append (matches , candidate {route : fallback , pattern : pattern })
744+ }
745+ }
746+ }
747+ }
748+
749+ if len (matches ) == 0 {
750+ return nil
751+ }
752+
753+ // If multiple matches, prioritize literal matches (no {param} or *)
754+ if len (matches ) > 1 {
755+ bestIdx := 0
756+ bestScore := - 1
757+
758+ for i , match := range matches {
759+ score := 0
760+ if ! strings .ContainsAny (match .pattern , "{}*" ) {
761+ score = 100 // Exact literal match
762+ } else {
763+ // Count literal parts (non-parameters)
764+ parts := strings .Split (match .pattern , "/" )
765+ for _ , p := range parts {
766+ if p != "" && ! strings .ContainsAny (p , "{}*" ) {
767+ score ++
768+ }
727769 }
728770 }
771+
772+ if score > bestScore {
773+ bestScore = score
774+ bestIdx = i
775+ }
729776 }
777+ return matches [bestIdx ].route
730778 }
731779
732- return nil
780+ return matches [ 0 ]. route
733781}
734782
735783// matchPattern checks if a Chi pattern matches a path
0 commit comments