From 1b2b27fc637fa0ce8e3c3a7134327a71ecdeabee Mon Sep 17 00:00:00 2001 From: Daniel Canedo Date: Fri, 28 Nov 2025 23:38:16 -0400 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20ErrorHandler=20incorr?= =?UTF-8?q?ect=20selection=20due=20map=20iteration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app.go b/app.go index 8fa3bbf768..8895747b55 100644 --- a/app.go +++ b/app.go @@ -1041,8 +1041,9 @@ func (app *App) ErrorHandler(ctx *Ctx, err error) error { mountedPrefixParts int ) - for prefix, subApp := range app.mountFields.appList { - if prefix != "" && strings.HasPrefix(ctx.path, prefix) { + for _, prefix := range app.mountFields.appListKeys { + subApp := app.mountFields.appList[prefix] + if prefix != "" && strings.HasPrefix(ctx.Path(), prefix) { parts := len(strings.Split(prefix, "/")) if mountedPrefixParts <= parts { if subApp.configured.ErrorHandler != nil { From 76201c033f7135c81ab1f0aa0944ac00a7d38b00 Mon Sep 17 00:00:00 2001 From: Daniel Canedo Date: Sat, 29 Nov 2025 21:03:49 -0400 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20Path=20matching=20log?= =?UTF-8?q?ic=20for=20ErrorHandler=20selection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.go | 6 +++++- utils/strings.go | 13 +++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app.go b/app.go index 8895747b55..d6067022ef 100644 --- a/app.go +++ b/app.go @@ -1041,9 +1041,13 @@ func (app *App) ErrorHandler(ctx *Ctx, err error) error { mountedPrefixParts int ) + normalizedPath := utils.AddTrailingSlash(ctx.Path()) + for _, prefix := range app.mountFields.appListKeys { subApp := app.mountFields.appList[prefix] - if prefix != "" && strings.HasPrefix(ctx.Path(), prefix) { + normalizedPrefix := utils.AddTrailingSlash(prefix) + + if prefix != "" && strings.HasPrefix(normalizedPath, normalizedPrefix) { parts := len(strings.Split(prefix, "/")) if mountedPrefixParts <= parts { if subApp.configured.ErrorHandler != nil { diff --git a/utils/strings.go b/utils/strings.go index 109d132f1e..8206c99a52 100644 --- a/utils/strings.go +++ b/utils/strings.go @@ -4,6 +4,10 @@ package utils +import ( + "strings" +) + // ToLower converts ascii string to lower-case func ToLower(b string) string { res := make([]byte, len(b)) @@ -73,3 +77,12 @@ func EqualFold(b, s string) bool { } return true } + +// AddTrailingSlash appends a trailing '/' to v if it does not already end with one +func AddTrailingSlash(s string) string { + if strings.HasSuffix(s, "/") { + return s + } + + return s + "/" +} From 35357a94fba8e47c7d019f7f1584ad083563562f Mon Sep 17 00:00:00 2001 From: Daniel Canedo Date: Sat, 29 Nov 2025 21:04:02 -0400 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=9A=A8=20Test:=20ErrorHandler=20selec?= =?UTF-8?q?tion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app_test.go | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/app_test.go b/app_test.go index cef3d51706..3fd882c567 100644 --- a/app_test.go +++ b/app_test.go @@ -1977,3 +1977,74 @@ func Benchmark_Ctx_AcquireReleaseFlow(b *testing.B) { } }) } + +func TestErrorHandler_PicksRightOne(t *testing.T) { + t.Parallel() + // common handler to be used by all routes, + // it will always fail by returning an error since + // we need to test that the right ErrorHandler is invoked + handler := func(c *Ctx) error { + return errors.New("random error") + } + + // subapp /api/v1/users [no custom error handler] + appAPIV1Users := New() + appAPIV1Users.Get("/", handler) + + // subapp /api/v1/use [with custom error handler] + appAPIV1UseEH := func(c *Ctx, _ error) error { + return c.SendString("/api/v1/use error handler") + } + appAPIV1Use := New(Config{ErrorHandler: appAPIV1UseEH}) + appAPIV1Use.Get("/", handler) + + // subapp: /api/v1 [with custom error handler] + appV1EH := func(c *Ctx, _ error) error { + return c.SendString("/api/v1 error handler") + } + appV1 := New(Config{ErrorHandler: appV1EH}) + appV1.Get("/", handler) + appV1.Mount("/users", appAPIV1Users) + appV1.Mount("/use", appAPIV1Use) + + // root app [no custom error handler] + app := New() + app.Get("/", handler) + app.Mount("/api/v1", appV1) + + testCases := []struct { + path string // the endpoint url to test + expected string // the expected error response + }{ + // /api/v1/users mount doesn't have custom ErrorHandler + // so it should use the upper-nearest one (/api/v1) + {"/api/v1/users", "/api/v1 error handler"}, + + // /api/v1/users mount has a custom ErrorHandler + {"/api/v1/use", "/api/v1/use error handler"}, + + // /api/v1 mount has a custom ErrorHandler + {"/api/v1", "/api/v1 error handler"}, + + // / mount doesn't have custom ErrorHandler, since is + // the root path i will use Fiber's default Error Handler + {"/", "random error"}, + } + + for _, testCase := range testCases { + t.Run(testCase.path, func(t *testing.T) { + t.Parallel() + resp, err := app.Test(httptest.NewRequest(MethodGet, testCase.path, nil)) + if err != nil { + t.Fatal(err) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + + utils.AssertEqual(t, testCase.expected, string(body)) + }) + } +} From 0ab8c3fb693f301a2a6cf184c1ef06454920f66f Mon Sep 17 00:00:00 2001 From: Daniel Canedo Date: Sun, 30 Nov 2025 13:53:36 -0400 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=9A=A8=20Test:=20AddTrailingSlash()?= =?UTF-8?q?=20util=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- utils/strings_test.go | 63 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/utils/strings_test.go b/utils/strings_test.go index c0de5875c6..c57a1299e5 100644 --- a/utils/strings_test.go +++ b/utils/strings_test.go @@ -215,3 +215,66 @@ func Test_EqualFold(t *testing.T) { res = EqualFold("/MY4/NAME/IS/:PARAM/*", "/my4/nAME/IS/:param/*") AssertEqual(t, true, res) } + +func Test_AddTrailingSlash(t *testing.T) { + t.Parallel() + tests := []struct { + name string + in string + want string + }{ + { + name: "already has trailing slash", + in: "path/", + want: "path/", + }, + { + name: "no trailing slash", + in: "path", + want: "path/", + }, + { + name: "empty string", + in: "", + want: "/", + }, + { + name: "root slash", + in: "/", + want: "/", + }, + { + name: "multi-level path", + in: "a/b/c", + want: "a/b/c/", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := AddTrailingSlash(tt.in) + if got != tt.want { + t.Fatalf("AddTrailingSlash(%q) = %q, want %q", tt.in, got, tt.want) + } + }) + } +} + +func Benchmark_AddTrailingSlash(b *testing.B) { + cases := map[string]string{ + "AlreadyHasSlash": "example/path/", + "NoSlash": "example/path", + "Empty": "", + "LongString": strings.Repeat("a", 10_000), + "LongStringWithSlash": strings.Repeat("a", 10_000) + "/", + } + + for name, input := range cases { + b.Run(name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = AddTrailingSlash(input) + } + }) + } +}