From daa7dc90c7d51a9bfad8308e23eafd7c76f12ff2 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Tue, 7 Apr 2026 05:05:39 +0000 Subject: [PATCH 01/18] Initial plan From db31cdf22a56370230e1d4eacedd21b298558eb4 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Tue, 7 Apr 2026 05:19:10 +0000 Subject: [PATCH 02/18] Add URL() method to Route for generating URLs with parameters This implements the feature requested in the issue to allow generating URLs from named routes with parameter substitution. Users can now do: route := app.GetRoute("a-name") url, err := route.URL(Map{"name": "name-val"}) // Returns: "/a/name-val" The implementation: - Added Route.URL() method that generates URLs by filling in route parameters - Uses case-insensitive parameter matching (matching Fiber's default behavior) - Supports all parameter types: named params, wildcards, and greedy params - Added comprehensive test coverage with 11 test cases - All quality checks pass (format, lint, test) Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/7256ea7c-dddf-4096-bb79-2fa6c3eefe39 Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- ctx_interface_gen.go | 3 + res_interface_gen.go | 3 + router.go | 46 +++++++++++++++ router_test.go | 133 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 185 insertions(+) diff --git a/ctx_interface_gen.go b/ctx_interface_gen.go index 51b47f34f78..7a1eb3b6c7f 100644 --- a/ctx_interface_gen.go +++ b/ctx_interface_gen.go @@ -485,5 +485,8 @@ type Ctx interface { // or when blocking access to sensitive endpoints. Drop() error // End immediately flushes the current response and closes the underlying connection. + // + // Note: End does not work when using streaming (e.g. fasthttp's HijackConn or SendStream), + // because in streaming mode the connection is managed asynchronously and ctx.Conn() may return nil. End() error } diff --git a/res_interface_gen.go b/res_interface_gen.go index d6150f055e1..239ddd16350 100644 --- a/res_interface_gen.go +++ b/res_interface_gen.go @@ -159,5 +159,8 @@ type Res interface { // or when blocking access to sensitive endpoints. Drop() error // End immediately flushes the current response and closes the underlying connection. + // + // Note: End does not work when using streaming (e.g. fasthttp's HijackConn or SendStream), + // because in streaming mode the connection is managed asynchronously and ctx.Conn() may return nil. End() error } diff --git a/router.go b/router.go index dd4b7de4e17..cb30dcf815b 100644 --- a/router.go +++ b/router.go @@ -5,12 +5,14 @@ package fiber import ( + "bytes" "fmt" "slices" "sync/atomic" "github.com/gofiber/utils/v2" utilsstrings "github.com/gofiber/utils/v2/strings" + "github.com/valyala/bytebufferpool" "github.com/valyala/fasthttp" ) @@ -65,6 +67,50 @@ type Route struct { autoHead bool // Automatically generated HEAD route } +// URL generates a URL from the route path and parameters. +// This method fills in the route parameters with the provided values. +// Parameter matching is case-insensitive by default to match Fiber's default behavior. +// +// Example: +// +// app.Get("/user/:name/:id", handler).Name("user") +// route := app.GetRoute("user") +// url, err := route.URL(Map{"name": "john", "id": "123"}) +// // Returns: "/user/john/123" +func (r *Route) URL(params Map) (string, error) { + if r.Path == "" { + return "", ErrNotFound + } + + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + for _, segment := range r.routeParser.segs { + if !segment.IsParam { + _, err := buf.WriteString(segment.Const) + if err != nil { + return "", fmt.Errorf("failed to write string: %w", err) + } + continue + } + + for key, val := range params { + // Use case-insensitive matching to support both CaseSensitive and non-CaseSensitive configs + isSame := key == segment.ParamName || utils.EqualFold(key, segment.ParamName) + isGreedy := segment.IsGreedy && len(key) == 1 && bytes.IndexByte(greedyParameters, key[0]) >= 0 + if isSame || isGreedy { + _, err := buf.WriteString(utils.ToString(val)) + if err != nil { + return "", fmt.Errorf("failed to write string: %w", err) + } + break + } + } + } + + return buf.String(), nil +} + func (r *Route) match(detectionPath, path string, params *[maxParams]string) bool { // root detectionPath check if r.root && len(detectionPath) == 1 && detectionPath[0] == '/' { diff --git a/router_test.go b/router_test.go index 02aa85bc7e0..cb8581043e2 100644 --- a/router_test.go +++ b/router_test.go @@ -2360,3 +2360,136 @@ func Benchmark_Router_GitHub_API_Parallel(b *testing.B) { require.True(b, match) } } + +// go test -run Test_Route_URL +func Test_Route_URL(t *testing.T) { + t.Parallel() + + t.Run("simple parameter", func(t *testing.T) { + t.Parallel() + app := New() + app.Get("/user/:name", emptyHandler).Name("User") + + route := app.GetRoute("User") + url, err := route.URL(Map{"name": "fiber"}) + require.NoError(t, err) + require.Equal(t, "/user/fiber", url) + }) + + t.Run("multiple parameters", func(t *testing.T) { + t.Parallel() + app := New() + app.Get("/user/:name/:id", emptyHandler).Name("UserID") + + route := app.GetRoute("UserID") + url, err := route.URL(Map{"name": "john", "id": "123"}) + require.NoError(t, err) + require.Equal(t, "/user/john/123", url) + }) + + t.Run("wildcard parameters", func(t *testing.T) { + t.Parallel() + app := New() + app.Get("/:phone/*/send/*", emptyHandler).Name("SendSms") + + route := app.GetRoute("SendSms") + url, err := route.URL(Map{ + "phone": "23456789", + "*1": "sms", + "*2": "test-msg", + }) + require.NoError(t, err) + require.Equal(t, "/23456789/sms/send/test-msg", url) + }) + + t.Run("single wildcard parameter", func(t *testing.T) { + t.Parallel() + app := New() + app.Get("/:phone/*/send", emptyHandler).Name("SendSms") + + route := app.GetRoute("SendSms") + url, err := route.URL(Map{ + "phone": "23456789", + "*": "sms", + }) + require.NoError(t, err) + require.Equal(t, "/23456789/sms/send", url) + }) + + t.Run("case sensitivity", func(t *testing.T) { + t.Parallel() + app := New() + app.Get("/user/:name", emptyHandler).Name("User") + + route := app.GetRoute("User") + // Case-insensitive by default (matching Fiber's default behavior) + url, err := route.URL(Map{"Name": "fiber"}) + require.NoError(t, err) + require.Equal(t, "/user/fiber", url) + }) + + t.Run("empty route", func(t *testing.T) { + t.Parallel() + route := Route{} + url, err := route.URL(Map{"name": "fiber"}) + require.Error(t, err) + require.Equal(t, ErrNotFound, err) + require.Empty(t, url) + }) + + t.Run("no parameters", func(t *testing.T) { + t.Parallel() + app := New() + app.Get("/static/path", emptyHandler).Name("Static") + + route := app.GetRoute("Static") + url, err := route.URL(Map{}) + require.NoError(t, err) + require.Equal(t, "/static/path", url) + }) + + t.Run("missing parameter value", func(t *testing.T) { + t.Parallel() + app := New() + app.Get("/user/:name/:id", emptyHandler).Name("UserID") + + route := app.GetRoute("UserID") + url, err := route.URL(Map{"name": "john"}) + require.NoError(t, err) + // Missing id parameter results in empty placeholder + require.Equal(t, "/user/john/", url) + }) + + t.Run("extra parameters ignored", func(t *testing.T) { + t.Parallel() + app := New() + app.Get("/user/:name", emptyHandler).Name("User") + + route := app.GetRoute("User") + url, err := route.URL(Map{"name": "fiber", "extra": "ignored"}) + require.NoError(t, err) + require.Equal(t, "/user/fiber", url) + }) + + t.Run("numeric parameter values", func(t *testing.T) { + t.Parallel() + app := New() + app.Get("/item/:id", emptyHandler).Name("Item") + + route := app.GetRoute("Item") + url, err := route.URL(Map{"id": 42}) + require.NoError(t, err) + require.Equal(t, "/item/42", url) + }) + + t.Run("complex path with mixed content", func(t *testing.T) { + t.Parallel() + app := New() + app.Get("/api/v1/users/:userId/posts/:postId/comments", emptyHandler).Name("Comments") + + route := app.GetRoute("Comments") + url, err := route.URL(Map{"userId": "user123", "postId": "post456"}) + require.NoError(t, err) + require.Equal(t, "/api/v1/users/user123/posts/post456/comments", url) + }) +} From 26faa1d8e1be3f1bd7c04d769b918ad29e1251ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 05:37:34 +0000 Subject: [PATCH 03/18] fix: address review feedback on Route.URL() - deterministic lookup, case sensitivity, revert generated files Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/b7a53bed-4d39-487b-8e01-9ecfe5d38b64 Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- ctx_interface_gen.go | 3 -- res_interface_gen.go | 3 -- router.go | 73 +++++++++++++++++++++++++++++--------------- router_test.go | 18 ++++++++++- 4 files changed, 65 insertions(+), 32 deletions(-) diff --git a/ctx_interface_gen.go b/ctx_interface_gen.go index 7a1eb3b6c7f..51b47f34f78 100644 --- a/ctx_interface_gen.go +++ b/ctx_interface_gen.go @@ -485,8 +485,5 @@ type Ctx interface { // or when blocking access to sensitive endpoints. Drop() error // End immediately flushes the current response and closes the underlying connection. - // - // Note: End does not work when using streaming (e.g. fasthttp's HijackConn or SendStream), - // because in streaming mode the connection is managed asynchronously and ctx.Conn() may return nil. End() error } diff --git a/res_interface_gen.go b/res_interface_gen.go index 239ddd16350..d6150f055e1 100644 --- a/res_interface_gen.go +++ b/res_interface_gen.go @@ -159,8 +159,5 @@ type Res interface { // or when blocking access to sensitive endpoints. Drop() error // End immediately flushes the current response and closes the underlying connection. - // - // Note: End does not work when using streaming (e.g. fasthttp's HijackConn or SendStream), - // because in streaming mode the connection is managed asynchronously and ctx.Conn() may return nil. End() error } diff --git a/router.go b/router.go index cb30dcf815b..484dfb9fc07 100644 --- a/router.go +++ b/router.go @@ -5,7 +5,6 @@ package fiber import ( - "bytes" "fmt" "slices" "sync/atomic" @@ -60,16 +59,18 @@ type Route struct { routeParser routeParser // Parameter parser // Data for routing - use bool // USE matches path prefixes - mount bool // Indicated a mounted app on a specific route - star bool // Path equals '*' - root bool // Path equals '/' - autoHead bool // Automatically generated HEAD route + use bool // USE matches path prefixes + mount bool // Indicated a mounted app on a specific route + star bool // Path equals '*' + root bool // Path equals '/' + autoHead bool // Automatically generated HEAD route + caseSensitive bool // Whether parameter matching is case-sensitive } // URL generates a URL from the route path and parameters. // This method fills in the route parameters with the provided values. -// Parameter matching is case-insensitive by default to match Fiber's default behavior. +// Parameter matching respects the app's CaseSensitive configuration: +// case-insensitive by default, case-sensitive when CaseSensitive is true. // // Example: // @@ -94,16 +95,36 @@ func (r *Route) URL(params Map) (string, error) { continue } - for key, val := range params { - // Use case-insensitive matching to support both CaseSensitive and non-CaseSensitive configs - isSame := key == segment.ParamName || utils.EqualFold(key, segment.ParamName) - isGreedy := segment.IsGreedy && len(key) == 1 && bytes.IndexByte(greedyParameters, key[0]) >= 0 - if isSame || isGreedy { - _, err := buf.WriteString(utils.ToString(val)) - if err != nil { - return "", fmt.Errorf("failed to write string: %w", err) + var ( + val any + found bool + ) + + // Prefer an exact parameter name match + if val, found = params[segment.ParamName]; !found && !r.caseSensitive { + // Fall back to a case-insensitive match + for key := range params { + if utils.EqualFold(key, segment.ParamName) { + val = params[key] + found = true + break } - break + } + } + + // For greedy parameters, fall back to generic greedy keys + if !found && segment.IsGreedy { + for _, greedyParam := range greedyParameters { + if val, found = params[string(greedyParam)]; found { + break + } + } + } + + if found { + _, err := buf.WriteString(utils.ToString(val)) + if err != nil { + return "", fmt.Errorf("failed to write string: %w", err) } } } @@ -422,11 +443,12 @@ func (app *App) addPrefixToRoute(prefix string, route *Route) *Route { func (*App) copyRoute(route *Route) *Route { return &Route{ // Router booleans - use: route.use, - mount: route.mount, - star: route.star, - root: route.root, - autoHead: route.autoHead, + use: route.use, + mount: route.mount, + star: route.star, + root: route.root, + autoHead: route.autoHead, + caseSensitive: route.caseSensitive, // Path data path: route.path, @@ -600,10 +622,11 @@ func (app *App) register(methods []string, pathRaw string, group *Group, handler isRoot := pathClean == "/" route := Route{ - use: isUse, - mount: isMount, - star: isStar, - root: isRoot, + use: isUse, + mount: isMount, + star: isStar, + root: isRoot, + caseSensitive: app.config.CaseSensitive, path: pathClean, routeParser: parsedPretty, diff --git a/router_test.go b/router_test.go index cb8581043e2..0095bbb974a 100644 --- a/router_test.go +++ b/router_test.go @@ -2416,7 +2416,7 @@ func Test_Route_URL(t *testing.T) { require.Equal(t, "/23456789/sms/send", url) }) - t.Run("case sensitivity", func(t *testing.T) { + t.Run("case insensitive default", func(t *testing.T) { t.Parallel() app := New() app.Get("/user/:name", emptyHandler).Name("User") @@ -2428,6 +2428,22 @@ func Test_Route_URL(t *testing.T) { require.Equal(t, "/user/fiber", url) }) + t.Run("case sensitive", func(t *testing.T) { + t.Parallel() + app := New(Config{CaseSensitive: true}) + app.Get("/user/:name", emptyHandler).Name("User") + + route := app.GetRoute("User") + // Exact case match succeeds + url, err := route.URL(Map{"name": "fiber"}) + require.NoError(t, err) + require.Equal(t, "/user/fiber", url) + // Different case does not match when CaseSensitive is true + url, err = route.URL(Map{"Name": "fiber"}) + require.NoError(t, err) + require.Equal(t, "/user/", url) + }) + t.Run("empty route", func(t *testing.T) { t.Parallel() route := Route{} From 6b37d8d5cf9edaccc70870d32a79d3b2d0eafab4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:39:39 +0000 Subject: [PATCH 04/18] fix: nil guard, deterministic case-insensitive fallback, mounted route caseSensitive Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/234974ca-1106-44ec-9ea0-af2206caff3f Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- router.go | 14 +++++++++----- router_test.go | 9 +++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/router.go b/router.go index 484dfb9fc07..104fd374642 100644 --- a/router.go +++ b/router.go @@ -79,7 +79,7 @@ type Route struct { // url, err := route.URL(Map{"name": "john", "id": "123"}) // // Returns: "/user/john/123" func (r *Route) URL(params Map) (string, error) { - if r.Path == "" { + if r == nil || r.Path == "" { return "", ErrNotFound } @@ -102,14 +102,17 @@ func (r *Route) URL(params Map) (string, error) { // Prefer an exact parameter name match if val, found = params[segment.ParamName]; !found && !r.caseSensitive { - // Fall back to a case-insensitive match + // Fall back to a case-insensitive match using a deterministic winner + var matchedKey string for key := range params { - if utils.EqualFold(key, segment.ParamName) { - val = params[key] + if utils.EqualFold(key, segment.ParamName) && (!found || key < matchedKey) { + matchedKey = key found = true - break } } + if found { + val = params[matchedKey] + } } // For greedy parameters, fall back to generic greedy keys @@ -436,6 +439,7 @@ func (app *App) addPrefixToRoute(prefix string, route *Route) *Route { route.routeParser = parseRoute(prettyPath, app.customConstraints...) route.root = false route.star = false + route.caseSensitive = app.config.CaseSensitive return route } diff --git a/router_test.go b/router_test.go index 0095bbb974a..37617648691 100644 --- a/router_test.go +++ b/router_test.go @@ -2453,6 +2453,15 @@ func Test_Route_URL(t *testing.T) { require.Empty(t, url) }) + t.Run("nil route", func(t *testing.T) { + t.Parallel() + var route *Route + url, err := route.URL(Map{"name": "fiber"}) + require.Error(t, err) + require.Equal(t, ErrNotFound, err) + require.Empty(t, url) + }) + t.Run("no parameters", func(t *testing.T) { t.Parallel() app := New() From 434c6f2b5de24f0bc38123eeed60a82a0a2943fe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 14:02:55 +0000 Subject: [PATCH 05/18] refactor: extract shared buildRouteURL helper, add deterministic fallback test Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/dd1306ff-05bd-4c60-a07f-d95d8af9f60f Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- res.go | 28 +--------------------------- router.go | 16 ++++++++++++++-- router_test.go | 5 +++++ 3 files changed, 20 insertions(+), 29 deletions(-) diff --git a/res.go b/res.go index 98ccc2ac1e2..687032401ba 100644 --- a/res.go +++ b/res.go @@ -2,7 +2,6 @@ package fiber import ( "bufio" - "bytes" "fmt" "html/template" "io" @@ -652,32 +651,7 @@ func (r *DefaultRes) getLocationFromRoute(route *Route, params Map) (string, err return "", ErrNotFound } - app := r.c.app - buf := bytebufferpool.Get() - for _, segment := range route.routeParser.segs { - if !segment.IsParam { - _, err := buf.WriteString(segment.Const) - if err != nil { - return "", fmt.Errorf("failed to write string: %w", err) - } - continue - } - - for key, val := range params { - isSame := key == segment.ParamName || (!app.config.CaseSensitive && utils.EqualFold(key, segment.ParamName)) - isGreedy := segment.IsGreedy && len(key) == 1 && bytes.IndexByte(greedyParameters, key[0]) >= 0 - if isSame || isGreedy { - _, err := buf.WriteString(utils.ToString(val)) - if err != nil { - return "", fmt.Errorf("failed to write string: %w", err) - } - } - } - } - location := buf.String() - // release buffer - bytebufferpool.Put(buf) - return location, nil + return buildRouteURL(route.routeParser.segs, params, r.c.app.config.CaseSensitive) } // GetRouteURL generates URLs to named routes, with parameters. URLs are relative, for example: "/user/1831" diff --git a/router.go b/router.go index 104fd374642..cbb53b766ae 100644 --- a/router.go +++ b/router.go @@ -83,10 +83,22 @@ func (r *Route) URL(params Map) (string, error) { return "", ErrNotFound } + return buildRouteURL(r.routeParser.segs, params, r.caseSensitive) +} + +// buildRouteURL generates a URL from route segments and parameters. +// This shared helper is used by both Route.URL() and DefaultRes.getLocationFromRoute() +// to ensure consistent URL generation behavior across APIs. +// +// Parameter resolution uses a deterministic three-step lookup: +// 1. Exact key match on segment.ParamName +// 2. Case-insensitive fallback picking the lexicographically-smallest matching key (when !caseSensitive) +// 3. Greedy parameter fallback for wildcard (*) and plus (+) parameters +func buildRouteURL(segs []*routeSegment, params Map, caseSensitive bool) (string, error) { buf := bytebufferpool.Get() defer bytebufferpool.Put(buf) - for _, segment := range r.routeParser.segs { + for _, segment := range segs { if !segment.IsParam { _, err := buf.WriteString(segment.Const) if err != nil { @@ -101,7 +113,7 @@ func (r *Route) URL(params Map) (string, error) { ) // Prefer an exact parameter name match - if val, found = params[segment.ParamName]; !found && !r.caseSensitive { + if val, found = params[segment.ParamName]; !found && !caseSensitive { // Fall back to a case-insensitive match using a deterministic winner var matchedKey string for key := range params { diff --git a/router_test.go b/router_test.go index 37617648691..90c098fbd4a 100644 --- a/router_test.go +++ b/router_test.go @@ -2426,6 +2426,11 @@ func Test_Route_URL(t *testing.T) { url, err := route.URL(Map{"Name": "fiber"}) require.NoError(t, err) require.Equal(t, "/user/fiber", url) + + // When multiple keys case-fold to the same param name, prefer the exact match. + url, err = route.URL(Map{"name": "exact", "Name": "fallback"}) + require.NoError(t, err) + require.Equal(t, "/user/exact", url) }) t.Run("case sensitive", func(t *testing.T) { From 9fef9782ac2cdc9283854fe497ae55e65f73fef8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 14:46:46 +0000 Subject: [PATCH 06/18] fix: refine Route.URL greedy fallback and docs Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/143982c2-4d96-4f13-8af7-06e714872490 Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- docs/api/app.md | 7 +++++++ docs/whats_new.md | 2 ++ res.go | 4 ++-- router.go | 27 ++++++++++++++++++++++----- router_test.go | 22 ++++++++++++++++++++++ 5 files changed, 55 insertions(+), 7 deletions(-) diff --git a/docs/api/app.md b/docs/api/app.md index 67a3ae7da9c..a10283a8f35 100644 --- a/docs/api/app.md +++ b/docs/api/app.md @@ -535,6 +535,8 @@ func main() { This method retrieves a route by its name. +The returned `Route` can be inspected or used to generate a URL directly with `route.URL(params)`. + ```go title="Signature" func (app *App) GetRoute(name string) Route ``` @@ -554,12 +556,17 @@ func main() { app := fiber.New() app.Get("/", handler).Name("index") + app.Get("/user/:name/:id", handler).Name("user") route := app.GetRoute("index") data, _ := json.MarshalIndent(route, "", " ") fmt.Println(string(data)) + userRoute := app.GetRoute("user") + location, _ := userRoute.URL(fiber.Map{"name": "john", "id": 1}) + fmt.Println(location) // /user/john/1 + log.Fatal(app.Listen(":3000")) } ``` diff --git a/docs/whats_new.md b/docs/whats_new.md index 20c7781d092..97df46f5bd1 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -415,6 +415,8 @@ app.RouteChain("/api").RouteChain("/user/:id?") You can find more information about `app.RouteChain` and `app.Route` in the API documentation ([RouteChain](./api/app#routechain), [Route](./api/app#route)). +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. + ### Domain routing `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`. diff --git a/res.go b/res.go index 687032401ba..90f4ca0fa9b 100644 --- a/res.go +++ b/res.go @@ -646,12 +646,12 @@ func (r *DefaultRes) ViewBind(vars Map) error { } // getLocationFromRoute get URL location from route using parameters -func (r *DefaultRes) getLocationFromRoute(route *Route, params Map) (string, error) { +func (*DefaultRes) getLocationFromRoute(route *Route, params Map) (string, error) { if route == nil || route.Path == "" { return "", ErrNotFound } - return buildRouteURL(route.routeParser.segs, params, r.c.app.config.CaseSensitive) + return buildRouteURL(route, params) } // GetRouteURL generates URLs to named routes, with parameters. URLs are relative, for example: "/user/1831" diff --git a/router.go b/router.go index cbb53b766ae..f609339d750 100644 --- a/router.go +++ b/router.go @@ -83,7 +83,7 @@ func (r *Route) URL(params Map) (string, error) { return "", ErrNotFound } - return buildRouteURL(r.routeParser.segs, params, r.caseSensitive) + return buildRouteURL(r, params) } // buildRouteURL generates a URL from route segments and parameters. @@ -94,11 +94,15 @@ func (r *Route) URL(params Map) (string, error) { // 1. Exact key match on segment.ParamName // 2. Case-insensitive fallback picking the lexicographically-smallest matching key (when !caseSensitive) // 3. Greedy parameter fallback for wildcard (*) and plus (+) parameters -func buildRouteURL(segs []*routeSegment, params Map, caseSensitive bool) (string, error) { +func buildRouteURL(route *Route, params Map) (string, error) { + if len(route.routeParser.segs) == 0 { + return route.Path, nil + } + buf := bytebufferpool.Get() defer bytebufferpool.Put(buf) - for _, segment := range segs { + for _, segment := range route.routeParser.segs { if !segment.IsParam { _, err := buf.WriteString(segment.Const) if err != nil { @@ -113,7 +117,7 @@ func buildRouteURL(segs []*routeSegment, params Map, caseSensitive bool) (string ) // Prefer an exact parameter name match - if val, found = params[segment.ParamName]; !found && !caseSensitive { + if val, found = params[segment.ParamName]; !found && !route.caseSensitive { // Fall back to a case-insensitive match using a deterministic winner var matchedKey string for key := range params { @@ -129,7 +133,7 @@ func buildRouteURL(segs []*routeSegment, params Map, caseSensitive bool) (string // For greedy parameters, fall back to generic greedy keys if !found && segment.IsGreedy { - for _, greedyParam := range greedyParameters { + for _, greedyParam := range preferredGreedyParameters(segment.ParamName) { if val, found = params[string(greedyParam)]; found { break } @@ -147,6 +151,19 @@ func buildRouteURL(segs []*routeSegment, params Map, caseSensitive bool) (string return buf.String(), nil } +func preferredGreedyParameters(paramName string) []byte { + if paramName != "" { + switch paramName[0] { + case plusParam: + return []byte{plusParam, wildcardParam} + case wildcardParam: + return []byte{wildcardParam, plusParam} + } + } + + return greedyParameters +} + func (r *Route) match(detectionPath, path string, params *[maxParams]string) bool { // root detectionPath check if r.root && len(detectionPath) == 1 && detectionPath[0] == '/' { diff --git a/router_test.go b/router_test.go index 90c098fbd4a..bad74ca7c53 100644 --- a/router_test.go +++ b/router_test.go @@ -2416,6 +2416,20 @@ func Test_Route_URL(t *testing.T) { require.Equal(t, "/23456789/sms/send", url) }) + t.Run("plus parameters prefer plus fallback", func(t *testing.T) { + t.Parallel() + app := New() + app.Get("/user/+", emptyHandler).Name("UserGreedy") + + route := app.GetRoute("UserGreedy") + url, err := route.URL(Map{ + "*": "wildcard", + "+": "plus", + }) + require.NoError(t, err) + require.Equal(t, "/user/plus", url) + }) + t.Run("case insensitive default", func(t *testing.T) { t.Parallel() app := New() @@ -2458,6 +2472,14 @@ func Test_Route_URL(t *testing.T) { require.Empty(t, url) }) + t.Run("route without parsed segments returns path", func(t *testing.T) { + t.Parallel() + route := Route{Path: "/error"} + url, err := route.URL(Map{"name": "fiber"}) + require.NoError(t, err) + require.Equal(t, "/error", url) + }) + t.Run("nil route", func(t *testing.T) { t.Parallel() var route *Route From be744961d78588fc370d38b630d0407d8ea4e079 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 14:49:29 +0000 Subject: [PATCH 07/18] test: cover default greedy fallback path Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/143982c2-4d96-4f13-8af7-06e714872490 Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- res.go | 4 ++-- router_test.go | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/res.go b/res.go index 90f4ca0fa9b..cd727eb58f2 100644 --- a/res.go +++ b/res.go @@ -646,8 +646,8 @@ func (r *DefaultRes) ViewBind(vars Map) error { } // getLocationFromRoute get URL location from route using parameters -func (*DefaultRes) getLocationFromRoute(route *Route, params Map) (string, error) { - if route == nil || route.Path == "" { +func (r *DefaultRes) getLocationFromRoute(route *Route, params Map) (string, error) { + if r == nil || route == nil || route.Path == "" { return "", ErrNotFound } diff --git a/router_test.go b/router_test.go index bad74ca7c53..79c4af76741 100644 --- a/router_test.go +++ b/router_test.go @@ -2430,6 +2430,12 @@ func Test_Route_URL(t *testing.T) { require.Equal(t, "/user/plus", url) }) + t.Run("preferred greedy parameters default fallback", func(t *testing.T) { + t.Parallel() + require.Equal(t, greedyParameters, preferredGreedyParameters("")) + require.Equal(t, greedyParameters, preferredGreedyParameters("name")) + }) + t.Run("case insensitive default", func(t *testing.T) { t.Parallel() app := New() From 6d6740e020fd6715abc529a750452a2a8d900a4c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 14:51:28 +0000 Subject: [PATCH 08/18] perf: avoid greedy fallback slice allocations Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/143982c2-4d96-4f13-8af7-06e714872490 Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- res.go | 3 ++- router.go | 9 +++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/res.go b/res.go index cd727eb58f2..60540945dba 100644 --- a/res.go +++ b/res.go @@ -645,7 +645,8 @@ func (r *DefaultRes) ViewBind(vars Map) error { return r.c.ViewBind(vars) } -// getLocationFromRoute get URL location from route using parameters +// getLocationFromRoute get URL location from route using parameters. +// Nil receivers and missing routes return ErrNotFound to match Route.URL semantics. func (r *DefaultRes) getLocationFromRoute(route *Route, params Map) (string, error) { if r == nil || route == nil || route.Path == "" { return "", ErrNotFound diff --git a/router.go b/router.go index f609339d750..039587bb490 100644 --- a/router.go +++ b/router.go @@ -67,6 +67,11 @@ type Route struct { caseSensitive bool // Whether parameter matching is case-sensitive } +var ( + preferredWildcardGreedyParameters = []byte{wildcardParam, plusParam} + preferredPlusGreedyParameters = []byte{plusParam, wildcardParam} +) + // URL generates a URL from the route path and parameters. // This method fills in the route parameters with the provided values. // Parameter matching respects the app's CaseSensitive configuration: @@ -155,9 +160,9 @@ func preferredGreedyParameters(paramName string) []byte { if paramName != "" { switch paramName[0] { case plusParam: - return []byte{plusParam, wildcardParam} + return preferredPlusGreedyParameters case wildcardParam: - return []byte{wildcardParam, plusParam} + return preferredWildcardGreedyParameters } } From 6e56f9910fd27285550bfd4aeacab44462b5fe0f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 14:53:31 +0000 Subject: [PATCH 09/18] docs: clarify greedy fallback helper behavior Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/143982c2-4d96-4f13-8af7-06e714872490 Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- router.go | 2 ++ router_test.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/router.go b/router.go index 039587bb490..10f6e148c26 100644 --- a/router.go +++ b/router.go @@ -156,6 +156,8 @@ func buildRouteURL(route *Route, params Map) (string, error) { return buf.String(), nil } +// preferredGreedyParameters returns the generic greedy fallback lookup order +// for a route parameter name, preferring the matching greedy token type first. func preferredGreedyParameters(paramName string) []byte { if paramName != "" { switch paramName[0] { diff --git a/router_test.go b/router_test.go index 79c4af76741..25972a33e2d 100644 --- a/router_test.go +++ b/router_test.go @@ -2432,6 +2432,8 @@ func Test_Route_URL(t *testing.T) { t.Run("preferred greedy parameters default fallback", func(t *testing.T) { t.Parallel() + require.Equal(t, preferredPlusGreedyParameters, preferredGreedyParameters("+1")) + require.Equal(t, preferredWildcardGreedyParameters, preferredGreedyParameters("*1")) require.Equal(t, greedyParameters, preferredGreedyParameters("")) require.Equal(t, greedyParameters, preferredGreedyParameters("name")) }) From 2e241ed8c44ddec9117ea37e3a7d6c1b9731c13b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 14:55:40 +0000 Subject: [PATCH 10/18] docs: explain Route.URL fallback edge cases Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/143982c2-4d96-4f13-8af7-06e714872490 Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- router.go | 4 +++- router_test.go | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/router.go b/router.go index 10f6e148c26..d86beaa592e 100644 --- a/router.go +++ b/router.go @@ -157,7 +157,9 @@ func buildRouteURL(route *Route, params Map) (string, error) { } // preferredGreedyParameters returns the generic greedy fallback lookup order -// for a route parameter name, preferring the matching greedy token type first. +// for a route parameter name. +// Parameter names starting with '+' prefer '+' before '*', names starting with +// '*' prefer '*' before '+', and all other names fall back to the default order. func preferredGreedyParameters(paramName string) []byte { if paramName != "" { switch paramName[0] { diff --git a/router_test.go b/router_test.go index 25972a33e2d..840d60b3a60 100644 --- a/router_test.go +++ b/router_test.go @@ -2482,6 +2482,9 @@ func Test_Route_URL(t *testing.T) { t.Run("route without parsed segments returns path", func(t *testing.T) { t.Parallel() + // This covers fallback/manual Route values whose Path is set but whose + // routeParser was never populated, such as synthetic routes outside the + // normal registration pipeline. route := Route{Path: "/error"} url, err := route.URL(Map{"name": "fiber"}) require.NoError(t, err) From a7f3f1f3e06072d45e4db18df882b8d391ec5426 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 11:25:22 +0000 Subject: [PATCH 11/18] perf: precompute greedy fallback map keys Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/ed85235e-587d-4c64-9cd9-faf015448a5a Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- path.go | 2 -- router.go | 13 +++++++------ router_test.go | 4 ++-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/path.go b/path.go index 4c37868558d..4aea2c3132a 100644 --- a/path.go +++ b/path.go @@ -124,8 +124,6 @@ const ( var ( // slash has a special role, unlike the other parameters it must not be interpreted as a parameter routeDelimiter = []byte{slashDelimiter, '-', '.'} - // list of greedy parameters - greedyParameters = []byte{wildcardParam, plusParam} // list of chars for the parameter recognizing parameterStartChars = [256]bool{ wildcardParam: true, diff --git a/router.go b/router.go index d86beaa592e..d656841aa2d 100644 --- a/router.go +++ b/router.go @@ -68,8 +68,9 @@ type Route struct { } var ( - preferredWildcardGreedyParameters = []byte{wildcardParam, plusParam} - preferredPlusGreedyParameters = []byte{plusParam, wildcardParam} + greedyParameterKeys = []string{"*", "+"} + preferredWildcardGreedyParameters = []string{"*", "+"} + preferredPlusGreedyParameters = []string{"+", "*"} ) // URL generates a URL from the route path and parameters. @@ -138,8 +139,8 @@ func buildRouteURL(route *Route, params Map) (string, error) { // For greedy parameters, fall back to generic greedy keys if !found && segment.IsGreedy { - for _, greedyParam := range preferredGreedyParameters(segment.ParamName) { - if val, found = params[string(greedyParam)]; found { + for _, greedyKey := range preferredGreedyParameters(segment.ParamName) { + if val, found = params[greedyKey]; found { break } } @@ -160,7 +161,7 @@ func buildRouteURL(route *Route, params Map) (string, error) { // for a route parameter name. // Parameter names starting with '+' prefer '+' before '*', names starting with // '*' prefer '*' before '+', and all other names fall back to the default order. -func preferredGreedyParameters(paramName string) []byte { +func preferredGreedyParameters(paramName string) []string { if paramName != "" { switch paramName[0] { case plusParam: @@ -170,7 +171,7 @@ func preferredGreedyParameters(paramName string) []byte { } } - return greedyParameters + return greedyParameterKeys } func (r *Route) match(detectionPath, path string, params *[maxParams]string) bool { diff --git a/router_test.go b/router_test.go index 840d60b3a60..9ed67fd11d3 100644 --- a/router_test.go +++ b/router_test.go @@ -2434,8 +2434,8 @@ func Test_Route_URL(t *testing.T) { t.Parallel() require.Equal(t, preferredPlusGreedyParameters, preferredGreedyParameters("+1")) require.Equal(t, preferredWildcardGreedyParameters, preferredGreedyParameters("*1")) - require.Equal(t, greedyParameters, preferredGreedyParameters("")) - require.Equal(t, greedyParameters, preferredGreedyParameters("name")) + require.Equal(t, greedyParameterKeys, preferredGreedyParameters("")) + require.Equal(t, greedyParameterKeys, preferredGreedyParameters("name")) }) t.Run("case insensitive default", func(t *testing.T) { From 9b47da0b2cf7e03e49ac3d3ed8fe625be06dba60 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 11:27:31 +0000 Subject: [PATCH 12/18] refactor: rename default greedy fallback keys Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/ed85235e-587d-4c64-9cd9-faf015448a5a Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- router.go | 4 ++-- router_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/router.go b/router.go index d656841aa2d..31a4d043b89 100644 --- a/router.go +++ b/router.go @@ -68,7 +68,7 @@ type Route struct { } var ( - greedyParameterKeys = []string{"*", "+"} + defaultGreedyParameterKeys = []string{"*", "+"} preferredWildcardGreedyParameters = []string{"*", "+"} preferredPlusGreedyParameters = []string{"+", "*"} ) @@ -171,7 +171,7 @@ func preferredGreedyParameters(paramName string) []string { } } - return greedyParameterKeys + return defaultGreedyParameterKeys } func (r *Route) match(detectionPath, path string, params *[maxParams]string) bool { diff --git a/router_test.go b/router_test.go index 9ed67fd11d3..6d1544d4e28 100644 --- a/router_test.go +++ b/router_test.go @@ -2434,8 +2434,8 @@ func Test_Route_URL(t *testing.T) { t.Parallel() require.Equal(t, preferredPlusGreedyParameters, preferredGreedyParameters("+1")) require.Equal(t, preferredWildcardGreedyParameters, preferredGreedyParameters("*1")) - require.Equal(t, greedyParameterKeys, preferredGreedyParameters("")) - require.Equal(t, greedyParameterKeys, preferredGreedyParameters("name")) + require.Equal(t, defaultGreedyParameterKeys, preferredGreedyParameters("")) + require.Equal(t, defaultGreedyParameterKeys, preferredGreedyParameters("name")) }) t.Run("case insensitive default", func(t *testing.T) { From 389a0f6b5f8de461904b391ced2c3168246cde3a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 13:36:52 +0000 Subject: [PATCH 13/18] =?UTF-8?q?=F0=9F=90=9B=20fix:=20make=20Route.URL=20?= =?UTF-8?q?callable=20on=20GetRoute=20values?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/9ce9d7e0-82b2-41df-81e3-33ad7c2fc7c1 Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- router.go | 11 ++++++----- router_test.go | 13 +++++++------ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/router.go b/router.go index 31a4d043b89..a94518c4c31 100644 --- a/router.go +++ b/router.go @@ -81,15 +81,16 @@ var ( // Example: // // app.Get("/user/:name/:id", handler).Name("user") -// route := app.GetRoute("user") -// url, err := route.URL(Map{"name": "john", "id": "123"}) +// url, err := app.GetRoute("user").URL(Map{"name": "john", "id": "123"}) // // Returns: "/user/john/123" -func (r *Route) URL(params Map) (string, error) { - if r == nil || r.Path == "" { +// +//nolint:gocritic // App.GetRoute returns a value, so URL must be callable on that value directly. +func (r Route) URL(params Map) (string, error) { + if r.Path == "" { return "", ErrNotFound } - return buildRouteURL(r, params) + return buildRouteURL(&r, params) } // buildRouteURL generates a URL from route segments and parameters. diff --git a/router_test.go b/router_test.go index 6d1544d4e28..4e646d71968 100644 --- a/router_test.go +++ b/router_test.go @@ -2491,13 +2491,14 @@ func Test_Route_URL(t *testing.T) { require.Equal(t, "/error", url) }) - t.Run("nil route", func(t *testing.T) { + t.Run("GetRoute direct call", func(t *testing.T) { t.Parallel() - var route *Route - url, err := route.URL(Map{"name": "fiber"}) - require.Error(t, err) - require.Equal(t, ErrNotFound, err) - require.Empty(t, url) + app := New() + app.Get("/user/:name", emptyHandler).Name("User") + + url, err := app.GetRoute("User").URL(Map{"name": "fiber"}) + require.NoError(t, err) + require.Equal(t, "/user/fiber", url) }) t.Run("no parameters", func(t *testing.T) { From 3384a5bc654d121d48d77dbdf09400a35ade04a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 13:39:18 +0000 Subject: [PATCH 14/18] =?UTF-8?q?=F0=9F=A7=B9=20chore:=20clarify=20Route.U?= =?UTF-8?q?RL=20nolint=20rationale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/9ce9d7e0-82b2-41df-81e3-33ad7c2fc7c1 Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- router.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/router.go b/router.go index a94518c4c31..776bb897c04 100644 --- a/router.go +++ b/router.go @@ -84,7 +84,7 @@ var ( // url, err := app.GetRoute("user").URL(Map{"name": "john", "id": "123"}) // // Returns: "/user/john/123" // -//nolint:gocritic // App.GetRoute returns a value, so URL must be callable on that value directly. +//nolint:gocritic // hugeParam: App.GetRoute returns a value, so URL must be callable on that value directly. func (r Route) URL(params Map) (string, error) { if r.Path == "" { return "", ErrNotFound From afc8c17acb21e01f9e225131b2258937047cdff8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 13:41:26 +0000 Subject: [PATCH 15/18] =?UTF-8?q?=F0=9F=A7=B9=20chore:=20fix=20Route.URL?= =?UTF-8?q?=20comment=20casing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/9ce9d7e0-82b2-41df-81e3-33ad7c2fc7c1 Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- router.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/router.go b/router.go index 776bb897c04..44fc901295d 100644 --- a/router.go +++ b/router.go @@ -84,7 +84,7 @@ var ( // url, err := app.GetRoute("user").URL(Map{"name": "john", "id": "123"}) // // Returns: "/user/john/123" // -//nolint:gocritic // hugeParam: App.GetRoute returns a value, so URL must be callable on that value directly. +//nolint:gocritic // hugeParam: app.GetRoute returns a value, so URL must be callable on that value directly. func (r Route) URL(params Map) (string, error) { if r.Path == "" { return "", ErrNotFound From 55afb076c129f9175f5deb502242757dca199cd3 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Sat, 11 Apr 2026 14:17:07 -0400 Subject: [PATCH 16/18] Update res.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- res.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res.go b/res.go index 79a716b28a8..84cbfab2361 100644 --- a/res.go +++ b/res.go @@ -645,7 +645,7 @@ func (r *DefaultRes) ViewBind(vars Map) error { return r.c.ViewBind(vars) } -// getLocationFromRoute get URL location from route using parameters. +// getLocationFromRoute gets the URL location from a route using parameters. // Nil receivers and missing routes return ErrNotFound to match Route.URL semantics. func (r *DefaultRes) getLocationFromRoute(route *Route, params Map) (string, error) { if r == nil || route == nil || route.Path == "" { From 9f828e72323c52513a7f707d1e860580bdd7cf7a Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Sat, 11 Apr 2026 14:23:28 -0400 Subject: [PATCH 17/18] Update router.go Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- router.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/router.go b/router.go index 44fc901295d..17f5b3da88b 100644 --- a/router.go +++ b/router.go @@ -168,7 +168,7 @@ func preferredGreedyParameters(paramName string) []string { case plusParam: return preferredPlusGreedyParameters case wildcardParam: - return preferredWildcardGreedyParameters + return defaultGreedyParameterKeys } } From 77044e24404a29870a9d5b67439bf7d8c8fc51f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Apr 2026 19:00:06 +0000 Subject: [PATCH 18/18] =?UTF-8?q?=F0=9F=90=9B=20fix=20deterministic=20case?= =?UTF-8?q?-insensitive=20Route.URL=20key=20selection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/6b2ae8da-f708-42b5-9e9b-7e3795de0ec5 Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- router.go | 8 +++++--- router_test.go | 8 ++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/router.go b/router.go index 17f5b3da88b..a9a830f5ed9 100644 --- a/router.go +++ b/router.go @@ -127,14 +127,16 @@ func buildRouteURL(route *Route, params Map) (string, error) { if val, found = params[segment.ParamName]; !found && !route.caseSensitive { // Fall back to a case-insensitive match using a deterministic winner var matchedKey string + foundMatch := false for key := range params { - if utils.EqualFold(key, segment.ParamName) && (!found || key < matchedKey) { + if utils.EqualFold(key, segment.ParamName) && (!foundMatch || key < matchedKey) { matchedKey = key - found = true + foundMatch = true } } - if found { + if foundMatch { val = params[matchedKey] + found = true } } diff --git a/router_test.go b/router_test.go index 4e646d71968..882dfde1292 100644 --- a/router_test.go +++ b/router_test.go @@ -2449,6 +2449,14 @@ func Test_Route_URL(t *testing.T) { require.NoError(t, err) require.Equal(t, "/user/fiber", url) + // When multiple keys case-fold to the same param name and no exact key + // exists, the lexicographically-smallest key wins deterministically. + for range 50 { + url, err = route.URL(Map{"nAme": "second", "Name": "first"}) + require.NoError(t, err) + require.Equal(t, "/user/first", url) + } + // When multiple keys case-fold to the same param name, prefer the exact match. url, err = route.URL(Map{"name": "exact", "Name": "fallback"}) require.NoError(t, err)