@@ -11,6 +11,7 @@ import (
1111
1212 "github.com/gofiber/utils/v2"
1313 utilsstrings "github.com/gofiber/utils/v2/strings"
14+ "github.com/valyala/bytebufferpool"
1415 "github.com/valyala/fasthttp"
1516)
1617
@@ -58,11 +59,122 @@ type Route struct {
5859 routeParser routeParser // Parameter parser
5960
6061 // Data for routing
61- use bool // USE matches path prefixes
62- mount bool // Indicated a mounted app on a specific route
63- star bool // Path equals '*'
64- root bool // Path equals '/'
65- autoHead bool // Automatically generated HEAD route
62+ use bool // USE matches path prefixes
63+ mount bool // Indicated a mounted app on a specific route
64+ star bool // Path equals '*'
65+ root bool // Path equals '/'
66+ autoHead bool // Automatically generated HEAD route
67+ caseSensitive bool // Whether parameter matching is case-sensitive
68+ }
69+
70+ var (
71+ defaultGreedyParameterKeys = []string {"*" , "+" }
72+ preferredWildcardGreedyParameters = []string {"*" , "+" }
73+ preferredPlusGreedyParameters = []string {"+" , "*" }
74+ )
75+
76+ // URL generates a URL from the route path and parameters.
77+ // This method fills in the route parameters with the provided values.
78+ // Parameter matching respects the app's CaseSensitive configuration:
79+ // case-insensitive by default, case-sensitive when CaseSensitive is true.
80+ //
81+ // Example:
82+ //
83+ // app.Get("/user/:name/:id", handler).Name("user")
84+ // url, err := app.GetRoute("user").URL(Map{"name": "john", "id": "123"})
85+ // // Returns: "/user/john/123"
86+ //
87+ //nolint:gocritic // hugeParam: app.GetRoute returns a value, so URL must be callable on that value directly.
88+ func (r Route ) URL (params Map ) (string , error ) {
89+ if r .Path == "" {
90+ return "" , ErrNotFound
91+ }
92+
93+ return buildRouteURL (& r , params )
94+ }
95+
96+ // buildRouteURL generates a URL from route segments and parameters.
97+ // This shared helper is used by both Route.URL() and DefaultRes.getLocationFromRoute()
98+ // to ensure consistent URL generation behavior across APIs.
99+ //
100+ // Parameter resolution uses a deterministic three-step lookup:
101+ // 1. Exact key match on segment.ParamName
102+ // 2. Case-insensitive fallback picking the lexicographically-smallest matching key (when !caseSensitive)
103+ // 3. Greedy parameter fallback for wildcard (*) and plus (+) parameters
104+ func buildRouteURL (route * Route , params Map ) (string , error ) {
105+ if len (route .routeParser .segs ) == 0 {
106+ return route .Path , nil
107+ }
108+
109+ buf := bytebufferpool .Get ()
110+ defer bytebufferpool .Put (buf )
111+
112+ for _ , segment := range route .routeParser .segs {
113+ if ! segment .IsParam {
114+ _ , err := buf .WriteString (segment .Const )
115+ if err != nil {
116+ return "" , fmt .Errorf ("failed to write string: %w" , err )
117+ }
118+ continue
119+ }
120+
121+ var (
122+ val any
123+ found bool
124+ )
125+
126+ // Prefer an exact parameter name match
127+ if val , found = params [segment .ParamName ]; ! found && ! route .caseSensitive {
128+ // Fall back to a case-insensitive match using a deterministic winner
129+ var matchedKey string
130+ foundMatch := false
131+ for key := range params {
132+ if utils .EqualFold (key , segment .ParamName ) && (! foundMatch || key < matchedKey ) {
133+ matchedKey = key
134+ foundMatch = true
135+ }
136+ }
137+ if foundMatch {
138+ val = params [matchedKey ]
139+ found = true
140+ }
141+ }
142+
143+ // For greedy parameters, fall back to generic greedy keys
144+ if ! found && segment .IsGreedy {
145+ for _ , greedyKey := range preferredGreedyParameters (segment .ParamName ) {
146+ if val , found = params [greedyKey ]; found {
147+ break
148+ }
149+ }
150+ }
151+
152+ if found {
153+ _ , err := buf .WriteString (utils .ToString (val ))
154+ if err != nil {
155+ return "" , fmt .Errorf ("failed to write string: %w" , err )
156+ }
157+ }
158+ }
159+
160+ return buf .String (), nil
161+ }
162+
163+ // preferredGreedyParameters returns the generic greedy fallback lookup order
164+ // for a route parameter name.
165+ // Parameter names starting with '+' prefer '+' before '*', names starting with
166+ // '*' prefer '*' before '+', and all other names fall back to the default order.
167+ func preferredGreedyParameters (paramName string ) []string {
168+ if paramName != "" {
169+ switch paramName [0 ] {
170+ case plusParam :
171+ return preferredPlusGreedyParameters
172+ case wildcardParam :
173+ return defaultGreedyParameterKeys
174+ }
175+ }
176+
177+ return defaultGreedyParameterKeys
66178}
67179
68180func (r * Route ) match (detectionPath , path string , params * [maxParams ]string ) bool {
@@ -369,18 +481,20 @@ func (app *App) addPrefixToRoute(prefix string, route *Route) *Route {
369481 route .routeParser = parseRoute (prettyPath , app .customConstraints ... )
370482 route .root = false
371483 route .star = false
484+ route .caseSensitive = app .config .CaseSensitive
372485
373486 return route
374487}
375488
376489func (* App ) copyRoute (route * Route ) * Route {
377490 return & Route {
378491 // Router booleans
379- use : route .use ,
380- mount : route .mount ,
381- star : route .star ,
382- root : route .root ,
383- autoHead : route .autoHead ,
492+ use : route .use ,
493+ mount : route .mount ,
494+ star : route .star ,
495+ root : route .root ,
496+ autoHead : route .autoHead ,
497+ caseSensitive : route .caseSensitive ,
384498
385499 // Path data
386500 path : route .path ,
@@ -554,10 +668,11 @@ func (app *App) register(methods []string, pathRaw string, group *Group, handler
554668 isRoot := pathClean == "/"
555669
556670 route := Route {
557- use : isUse ,
558- mount : isMount ,
559- star : isStar ,
560- root : isRoot ,
671+ use : isUse ,
672+ mount : isMount ,
673+ star : isStar ,
674+ root : isRoot ,
675+ caseSensitive : app .config .CaseSensitive ,
561676
562677 path : pathClean ,
563678 routeParser : parsedPretty ,
0 commit comments