Skip to content

Commit df0eca4

Browse files
authored
Merge pull request #4195 from gofiber/claude/fix-getroute-path-param
2 parents 4acf465 + f817947 commit df0eca4

6 files changed

Lines changed: 347 additions & 45 deletions

File tree

docs/api/app.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,8 @@ func main() {
535535

536536
This method retrieves a route by its name.
537537

538+
The returned `Route` can be inspected or used to generate a URL directly with `route.URL(params)`.
539+
538540
```go title="Signature"
539541
func (app *App) GetRoute(name string) Route
540542
```
@@ -554,12 +556,17 @@ func main() {
554556
app := fiber.New()
555557

556558
app.Get("/", handler).Name("index")
559+
app.Get("/user/:name/:id", handler).Name("user")
557560

558561
route := app.GetRoute("index")
559562

560563
data, _ := json.MarshalIndent(route, "", " ")
561564
fmt.Println(string(data))
562565

566+
userRoute := app.GetRoute("user")
567+
location, _ := userRoute.URL(fiber.Map{"name": "john", "id": 1})
568+
fmt.Println(location) // /user/john/1
569+
563570
log.Fatal(app.Listen(":3000"))
564571
}
565572
```

docs/whats_new.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,8 @@ app.RouteChain("/api").RouteChain("/user/:id?")
415415

416416
You can find more information about `app.RouteChain` and `app.Route` in the API documentation ([RouteChain](./api/app#routechain), [Route](./api/app#route)).
417417

418+
Named routes retrieved with `app.GetRoute(name)` also support `route.URL(params)` for generating relative URLs directly from the route definition, including parameter substitution for named, wildcard (`*`), and plus (`+`) segments.
419+
418420
### Domain routing
419421

420422
`Domain` creates a router scoped to a specific hostname pattern. Routes registered through the returned `Router` only match requests whose hostname (from `c.Hostname()`) matches the pattern. When `TrustProxy` is enabled and the proxy is trusted (as defined by [`TrustProxyConfig`](./api/app#trustproxyconfig)), the hostname may be derived from the `X-Forwarded-Host` header. Be sure to configure `TrustProxyConfig` to restrict which proxies are trusted and prevent header spoofing when enabling `TrustProxy`.

path.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,6 @@ const (
124124
var (
125125
// slash has a special role, unlike the other parameters it must not be interpreted as a parameter
126126
routeDelimiter = []byte{slashDelimiter, '-', '.'}
127-
// list of greedy parameters
128-
greedyParameters = []byte{wildcardParam, plusParam}
129127
// list of chars for the parameter recognizing
130128
parameterStartChars = [256]bool{
131129
wildcardParam: true,

res.go

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package fiber
22

33
import (
44
"bufio"
5-
"bytes"
65
"fmt"
76
"html/template"
87
"io"
@@ -646,38 +645,14 @@ func (r *DefaultRes) ViewBind(vars Map) error {
646645
return r.c.ViewBind(vars)
647646
}
648647

649-
// getLocationFromRoute get URL location from route using parameters
648+
// getLocationFromRoute gets the URL location from a route using parameters.
649+
// Nil receivers and missing routes return ErrNotFound to match Route.URL semantics.
650650
func (r *DefaultRes) getLocationFromRoute(route *Route, params Map) (string, error) {
651-
if route == nil || route.Path == "" {
651+
if r == nil || route == nil || route.Path == "" {
652652
return "", ErrNotFound
653653
}
654654

655-
app := r.c.app
656-
buf := bytebufferpool.Get()
657-
for _, segment := range route.routeParser.segs {
658-
if !segment.IsParam {
659-
_, err := buf.WriteString(segment.Const)
660-
if err != nil {
661-
return "", fmt.Errorf("failed to write string: %w", err)
662-
}
663-
continue
664-
}
665-
666-
for key, val := range params {
667-
isSame := key == segment.ParamName || (!app.config.CaseSensitive && utils.EqualFold(key, segment.ParamName))
668-
isGreedy := segment.IsGreedy && len(key) == 1 && bytes.IndexByte(greedyParameters, key[0]) >= 0
669-
if isSame || isGreedy {
670-
_, err := buf.WriteString(utils.ToString(val))
671-
if err != nil {
672-
return "", fmt.Errorf("failed to write string: %w", err)
673-
}
674-
}
675-
}
676-
}
677-
location := buf.String()
678-
// release buffer
679-
bytebufferpool.Put(buf)
680-
return location, nil
655+
return buildRouteURL(route, params)
681656
}
682657

683658
// GetRouteURL generates URLs to named routes, with parameters. URLs are relative, for example: "/user/1831"

router.go

Lines changed: 129 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -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

68180
func (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

376489
func (*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

Comments
 (0)