From d02f939ccf8f53582be227232ff78df2c8860929 Mon Sep 17 00:00:00 2001 From: Team Humaki Date: Sun, 20 Sep 2026 07:47:41 -0700 Subject: [PATCH] group: join prefix and path with a slash Fixes #3099. Group.GET("posts") under "/v1" concatenated to /v1posts. Insert a slash when neither side has one; keep Static("") /prefix* wildcards. --- group_test.go | 15 +++++++++++++++ route.go | 18 +++++++++++++++++- route_test.go | 21 +++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/group_test.go b/group_test.go index 56b2a465f..bc59fd433 100644 --- a/group_test.go +++ b/group_test.go @@ -94,6 +94,21 @@ func TestGroup_multiLevelGroup(t *testing.T) { assert.Equal(t, `OK`, body) } +func TestGroup_pathWithoutLeadingSlash(t *testing.T) { + e := New() + g := e.Group("/v1") + g.GET("posts", func(c *Context) error { + return c.String(http.StatusOK, "ok") + }) + + status, body := request(http.MethodGet, "/v1/posts", e) + assert.Equal(t, http.StatusOK, status) + assert.Equal(t, "ok", body) + + status, _ = request(http.MethodGet, "/v1posts", e) + assert.Equal(t, http.StatusNotFound, status) +} + func TestGroupFile(t *testing.T) { e := New() g := e.Group("/group") diff --git a/route.go b/route.go index 2468a8816..a38899303 100644 --- a/route.go +++ b/route.go @@ -43,9 +43,25 @@ func (r Route) ToRouteInfo(params []string) RouteInfo { } } +// joinPathPrefix concatenates a group prefix and a route path. +// A missing slash between "/v1" and "posts" used to produce "/v1posts". +// Wildcard suffixes (`*` / `/*`) stay concatenated so Group.Static("") keeps `/prefix*`. +func joinPathPrefix(prefix, path string) string { + if prefix == "" { + return path + } + if path == "" { + return prefix + } + if prefix[len(prefix)-1] != '/' && path[0] != '/' && path[0] != '*' { + return prefix + "/" + path + } + return prefix + path +} + // WithPrefix recreates Route with added group prefix and group middlewares it is grouped to. func (r Route) WithPrefix(pathPrefix string, middlewares []MiddlewareFunc) Route { - r.Path = pathPrefix + r.Path + r.Path = joinPathPrefix(pathPrefix, r.Path) if len(middlewares) > 0 { m := make([]MiddlewareFunc, 0, len(middlewares)+len(r.Middlewares)) diff --git a/route_test.go b/route_test.go index 8e2f170ee..ba32f8ff3 100644 --- a/route_test.go +++ b/route_test.go @@ -145,6 +145,27 @@ func TestRoute_ForGroup(t *testing.T) { assert.Equal(t, r.Name, "test route") } +func TestJoinPathPrefix(t *testing.T) { + tests := []struct { + prefix string + path string + want string + }{ + {prefix: "/v1", path: "/posts", want: "/v1/posts"}, + {prefix: "/v1", path: "posts", want: "/v1/posts"}, + {prefix: "/v1/", path: "posts", want: "/v1/posts"}, + {prefix: "/v1/", path: "/posts", want: "/v1//posts"}, + {prefix: "/users", path: "/test", want: "/users/test"}, + {prefix: "/users", path: ":id", want: "/users/:id"}, + {prefix: "/test", path: "*", want: "/test*"}, + {prefix: "", path: "posts", want: "posts"}, + {prefix: "/v1", path: "", want: "/v1"}, + } + for _, tt := range tests { + assert.Equal(t, tt.want, joinPathPrefix(tt.prefix, tt.path), "prefix=%q path=%q", tt.prefix, tt.path) + } +} + func exampleRoutes() Routes { return Routes{ RouteInfo{