From 283ef3219686f720656787e0d50af1dd300e4401 Mon Sep 17 00:00:00 2001 From: Kashiwa <13825170+ksw2000@users.noreply.github.com> Date: Thu, 20 Feb 2025 19:26:35 +0800 Subject: [PATCH 01/53] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20genericParseType=20?= =?UTF-8?q?parsing=20large=20uint=20leads=20to=20overflow=20(#3315)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🩹 Fix: genericParseType parsing large uint leads to overflow * ♻️ Refactor: use strconv.FormatUint instead of fmt.Sprintf --- ctx_test.go | 4 ++++ helpers.go | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ctx_test.go b/ctx_test.go index af308866..082b0d44 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -5522,6 +5522,10 @@ func Test_GenericParseTypeUints(t *testing.T) { value: uint(4), str: "4", }, + { + value: ^uint(0), + str: strconv.FormatUint(uint64(^uint(0)), 10), + }, } for _, test := range uints { diff --git a/helpers.go b/helpers.go index a400be0d..7e9e202c 100644 --- a/helpers.go +++ b/helpers.go @@ -796,7 +796,7 @@ func genericParseType[V GenericType](str string, v V, defaultValue ...V) V { case int64: return genericParseInt[V](str, 64, func(i int64) V { return assertValueType[V, int64](i) }, defaultValue...) case uint: - return genericParseUint[V](str, 32, func(i uint64) V { return assertValueType[V, uint](uint(i)) }, defaultValue...) + return genericParseUint[V](str, 0, func(i uint64) V { return assertValueType[V, uint](uint(i)) }, defaultValue...) case uint8: return genericParseUint[V](str, 8, func(i uint64) V { return assertValueType[V, uint8](uint8(i)) }, defaultValue...) case uint16: From cdb862add1f21afd71c49f8f61e1accc402b3120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Thu, 20 Feb 2025 12:58:08 +0100 Subject: [PATCH 02/53] add more adpater documenation --- docs/extra/faq.md | 22 +++++- docs/middleware/adaptor.md | 144 ++++++++++++++++++++----------------- 2 files changed, 97 insertions(+), 69 deletions(-) diff --git a/docs/extra/faq.md b/docs/extra/faq.md index d29b20d2..45d664dd 100644 --- a/docs/extra/faq.md +++ b/docs/extra/faq.md @@ -30,7 +30,7 @@ app.Use(func(c fiber.Ctx) error { }) ``` -## How can i use live reload ? +## How can I use live reload? [Air](https://github.com/air-verse/air) is a handy tool that automatically restarts your Go applications whenever the source code changes, making your development process faster and more efficient. @@ -99,10 +99,12 @@ If you have questions or just want to have a chat, feel free to join us via this ![](/img/support-discord.png) -## Does fiber support sub domain routing ? +## Does Fiber support subdomain routing? Yes we do, here are some examples: -This example works v2 + +
+Example ```go package main @@ -170,4 +172,18 @@ func main() { } ``` +
+ If more information is needed, please refer to this issue [#750](https://github.com/gofiber/fiber/issues/750) + +## How can I handle conversions between Fiber and net/http? + +The `adaptor` package provides utilities for converting between Fiber and `net/http`. It allows seamless integration of `net/http` handlers, middleware, and requests into Fiber applications, and vice versa. + +For details on how to: + +* Convert `net/http` handlers to Fiber handlers +* Convert Fiber handlers to `net/http` handlers +* Convert `fiber.Ctx` to `http.Request` + +See the dedicated documentation: [Adaptor Documentation](../middleware/adaptor.md). diff --git a/docs/middleware/adaptor.md b/docs/middleware/adaptor.md index 3103635d..527cced6 100644 --- a/docs/middleware/adaptor.md +++ b/docs/middleware/adaptor.md @@ -4,24 +4,36 @@ id: adaptor # Adaptor -Converter for net/http handlers to/from Fiber request handlers, special thanks to [@arsmn](https://github.com/arsmn)! +The `adaptor` package provides utilities for converting between Fiber and `net/http`. It allows seamless integration of `net/http` handlers, middleware, and requests into Fiber applications, and vice versa. -## Signatures +Special thanks to [@arsmn](https://github.com/arsmn) for contributions! -| Name | Signature | Description -| :--- | :--- | :--- -| HTTPHandler | `HTTPHandler(h http.Handler) fiber.Handler` | http.Handler -> fiber.Handler -| HTTPHandlerFunc | `HTTPHandlerFunc(h http.HandlerFunc) fiber.Handler` | http.HandlerFunc -> fiber.Handler -| HTTPMiddleware | `HTTPHandlerFunc(mw func(http.Handler) http.Handler) fiber.Handler` | func(http.Handler) http.Handler -> fiber.Handler -| FiberHandler | `FiberHandler(h fiber.Handler) http.Handler` | fiber.Handler -> http.Handler -| FiberHandlerFunc | `FiberHandlerFunc(h fiber.Handler) http.HandlerFunc` | fiber.Handler -> http.HandlerFunc -| FiberApp | `FiberApp(app *fiber.App) http.HandlerFunc` | Fiber app -> http.HandlerFunc -| ConvertRequest | `ConvertRequest(c fiber.Ctx, forServer bool) (*http.Request, error)` | fiber.Ctx -> http.Request -| CopyContextToFiberContext | `CopyContextToFiberContext(context any, requestContext *fasthttp.RequestCtx)` | context.Context -> fasthttp.RequestCtx +## Features -## Examples +- Convert `net/http` handlers and middleware to Fiber handlers. +- Convert Fiber handlers to `net/http` handlers. +- Convert Fiber context (`fiber.Ctx`) into an `http.Request`. -### net/http to Fiber +## API Reference + +| Name | Signature | Description | +|-----------------------------|-------------------------------------------------------------------------------|------------------------------------------------------------------| +| `HTTPHandler` | `HTTPHandler(h http.Handler) fiber.Handler` | Converts `http.Handler` to `fiber.Handler` | +| `HTTPHandlerFunc` | `HTTPHandlerFunc(h http.HandlerFunc) fiber.Handler` | Converts `http.HandlerFunc` to `fiber.Handler` | +| `HTTPMiddleware` | `HTTPMiddleware(mw func(http.Handler) http.Handler) fiber.Handler` | Converts `http.Handler` middleware to `fiber.Handler` middleware | +| `FiberHandler` | `FiberHandler(h fiber.Handler) http.Handler` | Converts `fiber.Handler` to `http.Handler` | +| `FiberHandlerFunc` | `FiberHandlerFunc(h fiber.Handler) http.HandlerFunc` | Converts `fiber.Handler` to `http.HandlerFunc` | +| `FiberApp` | `FiberApp(app *fiber.App) http.HandlerFunc` | Converts an entire Fiber app to an `http.HandlerFunc` | +| `ConvertRequest` | `ConvertRequest(c fiber.Ctx, forServer bool) (*http.Request, error)` | Converts `fiber.Ctx` into an `http.Request` | +| `CopyContextToFiberContext` | `CopyContextToFiberContext(context any, requestContext *fasthttp.RequestCtx)` | Copies `context.Context` to `fasthttp.RequestCtx` | + +--- + +## Usage Examples + +### 1. Using `net/http` Handlers in Fiber + +This example demonstrates how to use standard `net/http` handlers inside a Fiber application: ```go package main @@ -29,35 +41,27 @@ package main import ( "fmt" "net/http" - "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/middleware/adaptor" ) func main() { - // New fiber app app := fiber.New() - // http.Handler -> fiber.Handler - app.Get("/", adaptor.HTTPHandler(handler(greet))) + // Convert an http.Handler to a Fiber handler + app.Get("/", adaptor.HTTPHandler(http.HandlerFunc(helloHandler))) - // http.HandlerFunc -> fiber.Handler - app.Get("/func", adaptor.HTTPHandlerFunc(greet)) - - // Listen on port 3000 app.Listen(":3000") } -func handler(f http.HandlerFunc) http.Handler { - return http.HandlerFunc(f) -} - -func greet(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, "Hello World!") +func helloHandler(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "Hello from net/http!") } ``` -### net/http middleware to Fiber +### 2. Using `net/http` Middleware in Fiber + +Middleware written for `net/http` can be used in Fiber: ```go package main @@ -65,111 +69,119 @@ package main import ( "log" "net/http" - "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/middleware/adaptor" ) func main() { - // New fiber app app := fiber.New() - // http middleware -> fiber.Handler - app.Use(adaptor.HTTPMiddleware(logMiddleware)) + // Apply an http middleware in Fiber + app.Use(adaptor.HTTPMiddleware(loggingMiddleware)) + + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("Hello Fiber!") + }) - // Listen on port 3000 app.Listen(":3000") } -func logMiddleware(next http.Handler) http.Handler { +func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log.Println("log middleware") + log.Println("Request received") next.ServeHTTP(w, r) }) } ``` -### Fiber Handler to net/http +### 3. Using Fiber Handlers in `net/http` + +You can embed Fiber handlers inside `net/http`: ```go package main import ( "net/http" - "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/middleware/adaptor" ) func main() { - // fiber.Handler -> http.Handler - http.Handle("/", adaptor.FiberHandler(greet)) - - // fiber.Handler -> http.HandlerFunc - http.HandleFunc("/func", adaptor.FiberHandlerFunc(greet)) - - // Listen on port 3000 + // Convert Fiber handler to an http.Handler + http.Handle("/", adaptor.FiberHandler(helloFiber)) + + // Convert Fiber handler to http.HandlerFunc + http.HandleFunc("/func", adaptor.FiberHandlerFunc(helloFiber)) + http.ListenAndServe(":3000", nil) } -func greet(c fiber.Ctx) error { - return c.SendString("Hello World!") +func helloFiber(c fiber.Ctx) error { + return c.SendString("Hello from Fiber!") } ``` -### Fiber App to net/http +### 4. Running a Fiber App in `net/http` + +You can wrap a full Fiber app inside `net/http`: ```go package main import ( "net/http" - "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/middleware/adaptor" ) func main() { app := fiber.New() + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("Hello from Fiber!") + }) - app.Get("/greet", greet) - - // Listen on port 3000 + // Run Fiber inside an http server http.ListenAndServe(":3000", adaptor.FiberApp(app)) } - -func greet(c fiber.Ctx) error { - return c.SendString("Hello World!") -} ``` -### Fiber Context to (net/http).Request +### 5. Converting Fiber Context (`fiber.Ctx`) to `http.Request` + +If you need to use an `http.Request` inside a Fiber handler: ```go package main import ( "net/http" - "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/middleware/adaptor" ) func main() { app := fiber.New() - - app.Get("/greet", greetWithHTTPReq) - - // Listen on port 3000 - http.ListenAndServe(":3000", adaptor.FiberApp(app)) + app.Get("/request", handleRequest) + app.Listen(":3000") } -func greetWithHTTPReq(c fiber.Ctx) error { +func handleRequest(c fiber.Ctx) error { httpReq, err := adaptor.ConvertRequest(c, false) if err != nil { return err } - - return c.SendString("Request URL: " + httpReq.URL.String()) + return c.SendString("Converted Request URL: " + httpReq.URL.String()) } ``` + +--- + +## Summary + +The `adaptor` package allows easy interoperation between Fiber and `net/http`. You can: + +- Convert handlers and middleware in both directions. +- Run Fiber apps inside `net/http`. +- Convert `fiber.Ctx` to `http.Request`. + +This makes it simple to integrate Fiber with existing Go projects or migrate between frameworks as needed. From d655e08a48da06b735744dc4eeef796894204718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Thu, 20 Feb 2025 13:52:23 +0100 Subject: [PATCH 03/53] add more adpater documenation --- docs/extra/faq.md | 2 +- docs/middleware/adaptor.md | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/extra/faq.md b/docs/extra/faq.md index 45d664dd..82655685 100644 --- a/docs/extra/faq.md +++ b/docs/extra/faq.md @@ -178,7 +178,7 @@ If more information is needed, please refer to this issue [#750](https://github. ## How can I handle conversions between Fiber and net/http? -The `adaptor` package provides utilities for converting between Fiber and `net/http`. It allows seamless integration of `net/http` handlers, middleware, and requests into Fiber applications, and vice versa. +The `adaptor` middleware provides utilities for converting between Fiber and `net/http`. It allows seamless integration of `net/http` handlers, middleware, and requests into Fiber applications, and vice versa. For details on how to: diff --git a/docs/middleware/adaptor.md b/docs/middleware/adaptor.md index 527cced6..dc7e6280 100644 --- a/docs/middleware/adaptor.md +++ b/docs/middleware/adaptor.md @@ -6,8 +6,6 @@ id: adaptor The `adaptor` package provides utilities for converting between Fiber and `net/http`. It allows seamless integration of `net/http` handlers, middleware, and requests into Fiber applications, and vice versa. -Special thanks to [@arsmn](https://github.com/arsmn) for contributions! - ## Features - Convert `net/http` handlers and middleware to Fiber handlers. @@ -23,8 +21,8 @@ Special thanks to [@arsmn](https://github.com/arsmn) for contributions! | `HTTPMiddleware` | `HTTPMiddleware(mw func(http.Handler) http.Handler) fiber.Handler` | Converts `http.Handler` middleware to `fiber.Handler` middleware | | `FiberHandler` | `FiberHandler(h fiber.Handler) http.Handler` | Converts `fiber.Handler` to `http.Handler` | | `FiberHandlerFunc` | `FiberHandlerFunc(h fiber.Handler) http.HandlerFunc` | Converts `fiber.Handler` to `http.HandlerFunc` | -| `FiberApp` | `FiberApp(app *fiber.App) http.HandlerFunc` | Converts an entire Fiber app to an `http.HandlerFunc` | -| `ConvertRequest` | `ConvertRequest(c fiber.Ctx, forServer bool) (*http.Request, error)` | Converts `fiber.Ctx` into an `http.Request` | +| `FiberApp` | `FiberApp(app *fiber.App) http.HandlerFunc` | Converts an entire Fiber app to a `http.HandlerFunc` | +| `ConvertRequest` | `ConvertRequest(c fiber.Ctx, forServer bool) (*http.Request, error)` | Converts `fiber.Ctx` into a `http.Request` | | `CopyContextToFiberContext` | `CopyContextToFiberContext(context any, requestContext *fasthttp.RequestCtx)` | Copies `context.Context` to `fasthttp.RequestCtx` | --- @@ -48,7 +46,7 @@ import ( func main() { app := fiber.New() - // Convert an http.Handler to a Fiber handler + // Convert a http.Handler to a Fiber handler app.Get("/", adaptor.HTTPHandler(http.HandlerFunc(helloHandler))) app.Listen(":3000") @@ -59,7 +57,7 @@ func helloHandler(w http.ResponseWriter, r *http.Request) { } ``` -### 2. Using `net/http` Middleware in Fiber +### 2. Using `net/http` Middleware with Fiber Middleware written for `net/http` can be used in Fiber: @@ -76,7 +74,7 @@ import ( func main() { app := fiber.New() - // Apply an http middleware in Fiber + // Apply a http middleware in Fiber app.Use(adaptor.HTTPMiddleware(loggingMiddleware)) app.Get("/", func(c fiber.Ctx) error { @@ -148,7 +146,7 @@ func main() { ### 5. Converting Fiber Context (`fiber.Ctx`) to `http.Request` -If you need to use an `http.Request` inside a Fiber handler: +If you need to use a `http.Request` inside a Fiber handler: ```go package main From 4b62d3d592248077c38f53d6117828f278e8eb3f Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Sun, 23 Feb 2025 00:32:51 +0800 Subject: [PATCH 04/53] =?UTF-8?q?=F0=9F=94=A5=20feat:=20Improve=20and=20Op?= =?UTF-8?q?timize=20ShutdownWithContext=20Func=20(#3162)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Optimize ShutdownWithContext method in app.go - Reorder mutex lock acquisition to the start of the function - Early return if server is not running - Use defer for executing shutdown hooks - Simplify nil check for hooks - Remove TODO comment This commit improves the readability, robustness, and execution order of the shutdown process. It ensures consistent state throughout the shutdown and guarantees hook execution even in error cases. * feat: Enhance ShutdownWithContext test for improved reliability - Add shutdown hook verification - Implement better synchronization with channels - Improve error handling and assertions - Adjust timeouts for more consistent results - Add server state check after shutdown attempt - Include comments explaining expected behavior This commit improves the comprehensiveness and reliability of the ShutdownWithContext test, ensuring proper verification of shutdown hooks, timeout behavior, and server state during long-running requests. * 📚 Doc: update the docs to explain shutdown & hook execution order * 🩹 Fix: Possible Data Race on shutdownHookCalled Variable * 🩹 Fix: Remove the default Case * 🩹 Fix: Import sync/atomic * 🩹 Fix: golangci-lint problem * 🎨 Style: add block in api.md * 🩹 Fix: go mod tidy * feat: Optimize ShutdownWithContext method in app.go - Reorder mutex lock acquisition to the start of the function - Early return if server is not running - Use defer for executing shutdown hooks - Simplify nil check for hooks - Remove TODO comment This commit improves the readability, robustness, and execution order of the shutdown process. It ensures consistent state throughout the shutdown and guarantees hook execution even in error cases. * feat: Enhance ShutdownWithContext test for improved reliability - Add shutdown hook verification - Implement better synchronization with channels - Improve error handling and assertions - Adjust timeouts for more consistent results - Add server state check after shutdown attempt - Include comments explaining expected behavior This commit improves the comprehensiveness and reliability of the ShutdownWithContext test, ensuring proper verification of shutdown hooks, timeout behavior, and server state during long-running requests. * 📚 Doc: update the docs to explain shutdown & hook execution order * 🩹 Fix: Possible Data Race on shutdownHookCalled Variable * 🩹 Fix: Remove the default Case * 🩹 Fix: Import sync/atomic * 🩹 Fix: golangci-lint problem * 🎨 Style: add block in api.md * 🩹 Fix: go mod tidy * ♻️ Refactor: replaced OnShutdown by OnPreShutdown and OnPostShutdown * ♻️ Refactor: streamline post-shutdown hook execution in graceful shutdown process * 🚨 Test: add test for gracefulShutdown * 🔥 Feature: Using executeOnPreShutdownHooks and executeOnPostShutdownHooks Instead of OnShutdownSuccess and OnShutdownError * 🩹 Fix: deal Listener err * 🩹 Fix: go lint error * 🩹 Fix: reduced memory alignment * 🩹 Fix: reduced memory alignment * 🩹 Fix: context should be created inside the concatenation. * 📚 Doc: update what_new.md and hooks.md * ♻️ Refactor: use blocking channel instead of time.Sleep * 🩹 Fix: Improve synchronization in error propagation test. * 🩹 Fix: Replace sleep with proper synchronization. * 🩹 Fix: Server but not shut down properly * 🩹 Fix: Using channels to synchronize and pass results * 🩹 Fix: timeout with long running request * 📚 Doc: remove OnShutdownError and OnShutdownSuccess from fiber.md * Update hooks.md * 🚨 Test: Add graceful shutdown timeout error test case * 📝 Doc: Restructure hooks documentation for OnPreShutdown and OnPostShutdown * 📝 Doc: Remove extra whitespace in hooks documentation --------- Co-authored-by: yingjie.huang Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> --- app.go | 23 +++-- app_test.go | 158 +++++++++++++++++++++++++------- docs/api/fiber.md | 6 +- docs/api/hooks.md | 20 +++-- docs/whats_new.md | 59 ++++++++++++ hooks.go | 80 ++++++++++------- hooks_test.go | 75 +++++++++++++++- listen.go | 28 +----- listen_test.go | 223 ++++++++++++++++------------------------------ 9 files changed, 421 insertions(+), 251 deletions(-) diff --git a/app.go b/app.go index c867cc4e..4bbd9efb 100644 --- a/app.go +++ b/app.go @@ -894,6 +894,13 @@ func (app *App) HandlersCount() uint32 { // // Make sure the program doesn't exit and waits instead for Shutdown to return. // +// Important: app.Listen() must be called in a separate goroutine, otherwise shutdown hooks will not work +// as Listen() is a blocking operation. Example: +// +// go app.Listen(":3000") +// // ... +// app.Shutdown() +// // Shutdown does not close keepalive connections so its recommended to set ReadTimeout to something else than 0. func (app *App) Shutdown() error { return app.ShutdownWithContext(context.Background()) @@ -918,17 +925,21 @@ func (app *App) ShutdownWithTimeout(timeout time.Duration) error { // // ShutdownWithContext does not close keepalive connections so its recommended to set ReadTimeout to something else than 0. func (app *App) ShutdownWithContext(ctx context.Context) error { - if app.hooks != nil { - // TODO: check should be defered? - app.hooks.executeOnShutdownHooks() - } - app.mutex.Lock() defer app.mutex.Unlock() + + var err error + if app.server == nil { return ErrNotRunning } - return app.server.ShutdownWithContext(ctx) + + // Execute the Shutdown hook + app.hooks.executeOnPreShutdownHooks() + defer app.hooks.executeOnPostShutdownHooks(err) + + err = app.server.ShutdownWithContext(ctx) + return err } // Server returns the underlying fasthttp server diff --git a/app_test.go b/app_test.go index d64f9a68..18f6cef9 100644 --- a/app_test.go +++ b/app_test.go @@ -21,6 +21,7 @@ import ( "regexp" "runtime" "strings" + "sync" "testing" "time" @@ -930,20 +931,29 @@ func Test_App_ShutdownWithTimeout(t *testing.T) { }) ln := fasthttputil.NewInmemoryListener() + serverReady := make(chan struct{}) // Signal that the server is ready to start + go func() { + serverReady <- struct{}{} err := app.Listener(ln) assert.NoError(t, err) }() - time.Sleep(1 * time.Second) + <-serverReady // Waiting for the server to be ready + + // Create a connection and send a request + connReady := make(chan struct{}) go func() { conn, err := ln.Dial() assert.NoError(t, err) _, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")) assert.NoError(t, err) + + connReady <- struct{}{} // Signal that the request has been sent }() - time.Sleep(1 * time.Second) + + <-connReady // Waiting for the request to be sent shutdownErr := make(chan error) go func() { @@ -964,46 +974,130 @@ func Test_App_ShutdownWithTimeout(t *testing.T) { func Test_App_ShutdownWithContext(t *testing.T) { t.Parallel() - app := New() - app.Get("/", func(ctx Ctx) error { - time.Sleep(5 * time.Second) - return ctx.SendString("body") + t.Run("successful shutdown", func(t *testing.T) { + t.Parallel() + app := New() + + // Fast request that should complete + app.Get("/", func(c Ctx) error { + return c.SendString("OK") + }) + + ln := fasthttputil.NewInmemoryListener() + serverStarted := make(chan bool, 1) + + go func() { + serverStarted <- true + if err := app.Listener(ln); err != nil { + t.Errorf("Failed to start listener: %v", err) + } + }() + + <-serverStarted + + // Execute normal request + conn, err := ln.Dial() + require.NoError(t, err) + _, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")) + require.NoError(t, err) + + // Shutdown with sufficient timeout + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + err = app.ShutdownWithContext(ctx) + require.NoError(t, err, "Expected successful shutdown") }) - ln := fasthttputil.NewInmemoryListener() + t.Run("shutdown with hooks", func(t *testing.T) { + t.Parallel() + app := New() - go func() { - err := app.Listener(ln) - assert.NoError(t, err) - }() + hookOrder := make([]string, 0) + var hookMutex sync.Mutex - time.Sleep(1 * time.Second) + app.Hooks().OnPreShutdown(func() error { + hookMutex.Lock() + hookOrder = append(hookOrder, "pre") + hookMutex.Unlock() + return nil + }) - go func() { - conn, err := ln.Dial() - assert.NoError(t, err) + app.Hooks().OnPostShutdown(func(_ error) error { + hookMutex.Lock() + hookOrder = append(hookOrder, "post") + hookMutex.Unlock() + return nil + }) - _, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")) - assert.NoError(t, err) - }() + ln := fasthttputil.NewInmemoryListener() + go func() { + if err := app.Listener(ln); err != nil { + t.Errorf("Failed to start listener: %v", err) + } + }() - time.Sleep(1 * time.Second) + time.Sleep(100 * time.Millisecond) - shutdownErr := make(chan error) - go func() { - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() - shutdownErr <- app.ShutdownWithContext(ctx) - }() + err := app.ShutdownWithContext(context.Background()) + require.NoError(t, err) - select { - case <-time.After(5 * time.Second): - t.Fatal("idle connections not closed on shutdown") - case err := <-shutdownErr: - if err == nil || !errors.Is(err, context.DeadlineExceeded) { - t.Fatalf("unexpected err %v. Expecting %v", err, context.DeadlineExceeded) + require.Equal(t, []string{"pre", "post"}, hookOrder, "Hooks should execute in order") + }) + + t.Run("timeout with long running request", func(t *testing.T) { + t.Parallel() + app := New() + + requestStarted := make(chan struct{}) + requestProcessing := make(chan struct{}) + + app.Get("/", func(c Ctx) error { + close(requestStarted) + // Wait for signal to continue processing the request + <-requestProcessing + time.Sleep(2 * time.Second) + return c.SendString("OK") + }) + + ln := fasthttputil.NewInmemoryListener() + go func() { + if err := app.Listener(ln); err != nil { + t.Errorf("Failed to start listener: %v", err) + } + }() + + // Ensure server is fully started + time.Sleep(100 * time.Millisecond) + + // Start a long-running request + go func() { + conn, err := ln.Dial() + if err != nil { + t.Errorf("Failed to dial: %v", err) + return + } + if _, err := conn.Write([]byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")); err != nil { + t.Errorf("Failed to write: %v", err) + } + }() + + // Wait for request to start + select { + case <-requestStarted: + // Request has started, signal to continue processing + close(requestProcessing) + case <-time.After(2 * time.Second): + t.Fatal("Request did not start in time") } - } + + // Attempt shutdown, should timeout + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + + err := app.ShutdownWithContext(ctx) + require.ErrorIs(t, err, context.DeadlineExceeded) + }) } // go test -run Test_App_Mixed_Routes_WithSameLen diff --git a/docs/api/fiber.md b/docs/api/fiber.md index 70320984..a79b2ba0 100644 --- a/docs/api/fiber.md +++ b/docs/api/fiber.md @@ -111,11 +111,9 @@ app.Listen(":8080", fiber.ListenConfig{ | EnablePrefork | `bool` | When set to true, this will spawn multiple Go processes listening on the same port. | `false` | | EnablePrintRoutes | `bool` | If set to true, will print all routes with their method, path, and handler. | `false` | | GracefulContext | `context.Context` | Field to shutdown Fiber by given context gracefully. | `nil` | -| ShutdownTimeout | `time.Duration` | Specifies the maximum duration to wait for the server to gracefully shutdown. When the timeout is reached, the graceful shutdown process is interrupted and forcibly terminated, and the `context.DeadlineExceeded` error is passed to the `OnShutdownError` callback. Set to 0 to disable the timeout and wait indefinitely. | `10 * time.Second` | +| ShutdownTimeout | `time.Duration` | Specifies the maximum duration to wait for the server to gracefully shutdown. When the timeout is reached, the graceful shutdown process is interrupted and forcibly terminated, and the `context.DeadlineExceeded` error is passed to the `OnPostShutdown` callback. Set to 0 to disable the timeout and wait indefinitely. | `10 * time.Second` | | ListenerAddrFunc | `func(addr net.Addr)` | Allows accessing and customizing `net.Listener`. | `nil` | | ListenerNetwork | `string` | Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only). WARNING: When prefork is set to true, only "tcp4" and "tcp6" can be chosen. | `tcp4` | -| OnShutdownError | `func(err error)` | Allows to customize error behavior when gracefully shutting down the server by given signal. Prints error with `log.Fatalf()` | `nil` | -| OnShutdownSuccess | `func()` | Allows customizing success behavior when gracefully shutting down the server by given signal. | `nil` | | TLSConfigFunc | `func(tlsConfig *tls.Config)` | Allows customizing `tls.Config` as you want. | `nil` | | AutoCertManager | `*autocert.Manager` | Manages TLS certificates automatically using the ACME protocol. Enables integration with Let's Encrypt or other ACME-compatible providers. | `nil` | | TLSMinVersion | `uint16` | Allows customizing the TLS minimum version. | `tls.VersionTLS12` | @@ -230,7 +228,7 @@ Shutdown gracefully shuts down the server without interrupting any active connec ShutdownWithTimeout will forcefully close any active connections after the timeout expires. -ShutdownWithContext shuts down the server including by force if the context's deadline is exceeded. +ShutdownWithContext shuts down the server including by force if the context's deadline is exceeded. Shutdown hooks will still be executed, even if an error occurs during the shutdown process, as they are deferred to ensure cleanup happens regardless of errors. ```go func (app *App) Shutdown() error diff --git a/docs/api/hooks.md b/docs/api/hooks.md index 48528666..a6e6c1ac 100644 --- a/docs/api/hooks.md +++ b/docs/api/hooks.md @@ -15,7 +15,8 @@ With Fiber you can execute custom user functions at specific method execution po - [OnGroupName](#ongroupname) - [OnListen](#onlisten) - [OnFork](#onfork) -- [OnShutdown](#onshutdown) +- [OnPreShutdown](#onpreshutdown) +- [OnPostShutdown](#onpostshutdown) - [OnMount](#onmount) ## Constants @@ -28,7 +29,8 @@ type OnGroupHandler = func(Group) error type OnGroupNameHandler = OnGroupHandler type OnListenHandler = func(ListenData) error type OnForkHandler = func(int) error -type OnShutdownHandler = func() error +type OnPreShutdownHandler = func() error +type OnPostShutdownHandler = func(error) error type OnMountHandler = func(*App) error ``` @@ -174,12 +176,20 @@ func main() { func (h *Hooks) OnFork(handler ...OnForkHandler) ``` -## OnShutdown +## OnPreShutdown -`OnShutdown` is a hook to execute user functions after shutdown. +`OnPreShutdown` is a hook to execute user functions before shutdown. ```go title="Signature" -func (h *Hooks) OnShutdown(handler ...OnShutdownHandler) +func (h *Hooks) OnPreShutdown(handler ...OnPreShutdownHandler) +``` + +## OnPostShutdown + +`OnPostShutdown` is a hook to execute user functions after shutdown. + +```go title="Signature" +func (h *Hooks) OnPostShutdown(handler ...OnPostShutdownHandler) ``` ## OnMount diff --git a/docs/whats_new.md b/docs/whats_new.md index 1958632a..7f0b6322 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -16,6 +16,8 @@ In this guide, we'll walk you through the most important changes in Fiber `v3` a Here's a quick overview of the changes in Fiber `v3`: - [🚀 App](#-app) +- [🎣 Hooks](#-hooks) +- [🚀 Listen](#-listen) - [🗺️ Router](#-router) - [🧠 Context](#-context) - [📎 Binding](#-binding) @@ -158,6 +160,63 @@ app.Listen(":444", fiber.ListenConfig{ }) ``` +## 🎣 Hooks + +We have made several changes to the Fiber hooks, including: + +- Added new shutdown hooks to provide better control over the shutdown process: + - `OnPreShutdown` - Executes before the server starts shutting down + - `OnPostShutdown` - Executes after the server has shut down, receives any shutdown error +- Deprecated `OnShutdown` in favor of the new pre/post shutdown hooks +- Improved shutdown hook execution order and reliability +- Added mutex protection for hook registration and execution + +Important: When using shutdown hooks, ensure app.Listen() is called in a separate goroutine: + +```go +// Correct usage +go app.Listen(":3000") +// ... register shutdown hooks +app.Shutdown() + +// Incorrect usage - hooks won't work +app.Listen(":3000") // This blocks +app.Shutdown() // Never reached +``` + +## 🚀 Listen + +We have made several changes to the Fiber listen, including: + +- Removed `OnShutdownError` and `OnShutdownSuccess` from `ListenerConfig` in favor of using `OnPostShutdown` hook which receives the shutdown error + +```go +app := fiber.New() + +// Before - using ListenerConfig callbacks +app.Listen(":3000", fiber.ListenerConfig{ + OnShutdownError: func(err error) { + log.Printf("Shutdown error: %v", err) + }, + OnShutdownSuccess: func() { + log.Println("Shutdown successful") + }, +}) + +// After - using OnPostShutdown hook +app.Hooks().OnPostShutdown(func(err error) error { + if err != nil { + log.Printf("Shutdown error: %v", err) + } else { + log.Println("Shutdown successful") + } + return nil +}) +go app.Listen(":3000") +``` + +This change simplifies the shutdown handling by consolidating the shutdown callbacks into a single hook that receives the error status. + ## 🗺 Router We have slightly adapted our router interface diff --git a/hooks.go b/hooks.go index 3da5c671..314717d0 100644 --- a/hooks.go +++ b/hooks.go @@ -6,14 +6,15 @@ import ( // OnRouteHandler Handlers define a function to create hooks for Fiber. type ( - OnRouteHandler = func(Route) error - OnNameHandler = OnRouteHandler - OnGroupHandler = func(Group) error - OnGroupNameHandler = OnGroupHandler - OnListenHandler = func(ListenData) error - OnShutdownHandler = func() error - OnForkHandler = func(int) error - OnMountHandler = func(*App) error + OnRouteHandler = func(Route) error + OnNameHandler = OnRouteHandler + OnGroupHandler = func(Group) error + OnGroupNameHandler = OnGroupHandler + OnListenHandler = func(ListenData) error + OnPreShutdownHandler = func() error + OnPostShutdownHandler = func(error) error + OnForkHandler = func(int) error + OnMountHandler = func(*App) error ) // Hooks is a struct to use it with App. @@ -22,14 +23,15 @@ type Hooks struct { app *App // Hooks - onRoute []OnRouteHandler - onName []OnNameHandler - onGroup []OnGroupHandler - onGroupName []OnGroupNameHandler - onListen []OnListenHandler - onShutdown []OnShutdownHandler - onFork []OnForkHandler - onMount []OnMountHandler + onRoute []OnRouteHandler + onName []OnNameHandler + onGroup []OnGroupHandler + onGroupName []OnGroupNameHandler + onListen []OnListenHandler + onPreShutdown []OnPreShutdownHandler + onPostShutdown []OnPostShutdownHandler + onFork []OnForkHandler + onMount []OnMountHandler } // ListenData is a struct to use it with OnListenHandler @@ -41,15 +43,16 @@ type ListenData struct { func newHooks(app *App) *Hooks { return &Hooks{ - app: app, - onRoute: make([]OnRouteHandler, 0), - onGroup: make([]OnGroupHandler, 0), - onGroupName: make([]OnGroupNameHandler, 0), - onName: make([]OnNameHandler, 0), - onListen: make([]OnListenHandler, 0), - onShutdown: make([]OnShutdownHandler, 0), - onFork: make([]OnForkHandler, 0), - onMount: make([]OnMountHandler, 0), + app: app, + onRoute: make([]OnRouteHandler, 0), + onGroup: make([]OnGroupHandler, 0), + onGroupName: make([]OnGroupNameHandler, 0), + onName: make([]OnNameHandler, 0), + onListen: make([]OnListenHandler, 0), + onPreShutdown: make([]OnPreShutdownHandler, 0), + onPostShutdown: make([]OnPostShutdownHandler, 0), + onFork: make([]OnForkHandler, 0), + onMount: make([]OnMountHandler, 0), } } @@ -96,10 +99,17 @@ func (h *Hooks) OnListen(handler ...OnListenHandler) { h.app.mutex.Unlock() } -// OnShutdown is a hook to execute user functions after Shutdown. -func (h *Hooks) OnShutdown(handler ...OnShutdownHandler) { +// OnPreShutdown is a hook to execute user functions before Shutdown. +func (h *Hooks) OnPreShutdown(handler ...OnPreShutdownHandler) { h.app.mutex.Lock() - h.onShutdown = append(h.onShutdown, handler...) + h.onPreShutdown = append(h.onPreShutdown, handler...) + h.app.mutex.Unlock() +} + +// OnPostShutdown is a hook to execute user functions after Shutdown. +func (h *Hooks) OnPostShutdown(handler ...OnPostShutdownHandler) { + h.app.mutex.Lock() + h.onPostShutdown = append(h.onPostShutdown, handler...) h.app.mutex.Unlock() } @@ -191,10 +201,18 @@ func (h *Hooks) executeOnListenHooks(listenData ListenData) error { return nil } -func (h *Hooks) executeOnShutdownHooks() { - for _, v := range h.onShutdown { +func (h *Hooks) executeOnPreShutdownHooks() { + for _, v := range h.onPreShutdown { if err := v(); err != nil { - log.Errorf("failed to call shutdown hook: %v", err) + log.Errorf("failed to call pre shutdown hook: %v", err) + } + } +} + +func (h *Hooks) executeOnPostShutdownHooks(err error) { + for _, v := range h.onPostShutdown { + if err := v(err); err != nil { + log.Errorf("failed to call post shutdown hook: %v", err) } } } diff --git a/hooks_test.go b/hooks_test.go index f96f5707..b95e0733 100644 --- a/hooks_test.go +++ b/hooks_test.go @@ -181,22 +181,89 @@ func Test_Hook_OnGroupName_Error(t *testing.T) { grp.Get("/test", testSimpleHandler) } -func Test_Hook_OnShutdown(t *testing.T) { +func Test_Hook_OnPrehutdown(t *testing.T) { t.Parallel() app := New() buf := bytebufferpool.Get() defer bytebufferpool.Put(buf) - app.Hooks().OnShutdown(func() error { - _, err := buf.WriteString("shutdowning") + app.Hooks().OnPreShutdown(func() error { + _, err := buf.WriteString("pre-shutdowning") require.NoError(t, err) return nil }) require.NoError(t, app.Shutdown()) - require.Equal(t, "shutdowning", buf.String()) + require.Equal(t, "pre-shutdowning", buf.String()) +} + +func Test_Hook_OnPostShutdown(t *testing.T) { + t.Run("should execute post shutdown hook with error", func(t *testing.T) { + app := New() + expectedErr := errors.New("test shutdown error") + + hookCalled := make(chan error, 1) + defer close(hookCalled) + + app.Hooks().OnPostShutdown(func(err error) error { + hookCalled <- err + return nil + }) + + go func() { + if err := app.Listen(":0"); err != nil { + return + } + }() + + time.Sleep(100 * time.Millisecond) + + app.hooks.executeOnPostShutdownHooks(expectedErr) + + select { + case err := <-hookCalled: + require.Equal(t, expectedErr, err) + case <-time.After(time.Second): + t.Fatal("hook execution timeout") + } + + require.NoError(t, app.Shutdown()) + }) + + t.Run("should execute multiple hooks in order", func(t *testing.T) { + app := New() + + execution := make([]int, 0) + + app.Hooks().OnPostShutdown(func(_ error) error { + execution = append(execution, 1) + return nil + }) + + app.Hooks().OnPostShutdown(func(_ error) error { + execution = append(execution, 2) + return nil + }) + + app.hooks.executeOnPostShutdownHooks(nil) + + require.Len(t, execution, 2, "expected 2 hooks to execute") + require.Equal(t, []int{1, 2}, execution, "hooks executed in wrong order") + }) + + t.Run("should handle hook error", func(_ *testing.T) { + app := New() + hookErr := errors.New("hook error") + + app.Hooks().OnPostShutdown(func(_ error) error { + return hookErr + }) + + // Should not panic + app.hooks.executeOnPostShutdownHooks(nil) + }) } func Test_Hook_OnListen(t *testing.T) { diff --git a/listen.go b/listen.go index 793d36d2..f33c9daf 100644 --- a/listen.go +++ b/listen.go @@ -60,17 +60,6 @@ type ListenConfig struct { // Default: nil BeforeServeFunc func(app *App) error `json:"before_serve_func"` - // OnShutdownError allows to customize error behavior when to graceful shutdown server by given signal. - // - // Print error with log.Fatalf() by default. - // Default: nil - OnShutdownError func(err error) - - // OnShutdownSuccess allows to customize success behavior when to graceful shutdown server by given signal. - // - // Default: nil - OnShutdownSuccess func() - // AutoCertManager manages TLS certificates automatically using the ACME protocol, // Enables integration with Let's Encrypt or other ACME-compatible providers. // @@ -102,7 +91,7 @@ type ListenConfig struct { CertClientFile string `json:"cert_client_file"` // When the graceful shutdown begins, use this field to set the timeout - // duration. If the timeout is reached, OnShutdownError will be called. + // duration. If the timeout is reached, OnPostShutdown will be called with the error. // Set to 0 to disable the timeout and wait indefinitely. // // Default: 10 * time.Second @@ -136,9 +125,6 @@ func listenConfigDefault(config ...ListenConfig) ListenConfig { return ListenConfig{ TLSMinVersion: tls.VersionTLS12, ListenerNetwork: NetworkTCP4, - OnShutdownError: func(err error) { - log.Fatalf("shutdown: %v", err) //nolint:revive // It's an option - }, ShutdownTimeout: 10 * time.Second, } } @@ -148,12 +134,6 @@ func listenConfigDefault(config ...ListenConfig) ListenConfig { cfg.ListenerNetwork = NetworkTCP4 } - if cfg.OnShutdownError == nil { - cfg.OnShutdownError = func(err error) { - log.Fatalf("shutdown: %v", err) //nolint:revive // It's an option - } - } - if cfg.TLSMinVersion == 0 { cfg.TLSMinVersion = tls.VersionTLS12 } @@ -517,11 +497,9 @@ func (app *App) gracefulShutdown(ctx context.Context, cfg ListenConfig) { } if err != nil { - cfg.OnShutdownError(err) + app.hooks.executeOnPostShutdownHooks(err) return } - if success := cfg.OnShutdownSuccess; success != nil { - success() - } + app.hooks.executeOnPostShutdownHooks(nil) } diff --git a/listen_test.go b/listen_test.go index 032f7d32..1a5bd77f 100644 --- a/listen_test.go +++ b/listen_test.go @@ -15,6 +15,7 @@ import ( "testing" "time" + "github.com/gofiber/utils/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/valyala/fasthttp" @@ -37,98 +38,42 @@ func Test_Listen(t *testing.T) { // go test -run Test_Listen_Graceful_Shutdown func Test_Listen_Graceful_Shutdown(t *testing.T) { - var mu sync.Mutex - var shutdown bool - - app := New() - - app.Get("/", func(c Ctx) error { - return c.SendString(c.Hostname()) + t.Run("Basic Graceful Shutdown", func(t *testing.T) { + testGracefulShutdown(t, 0) }) - ln := fasthttputil.NewInmemoryListener() - errs := make(chan error) + t.Run("Shutdown With Timeout", func(t *testing.T) { + testGracefulShutdown(t, 500*time.Millisecond) + }) - go func() { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - - errs <- app.Listener(ln, ListenConfig{ - DisableStartupMessage: true, - GracefulContext: ctx, - OnShutdownSuccess: func() { - mu.Lock() - shutdown = true - mu.Unlock() - }, - }) - }() - - // Server readiness check - for i := 0; i < 10; i++ { - conn, err := ln.Dial() - if err == nil { - conn.Close() //nolint:errcheck // ignore error - break - } - // Wait a bit before retrying - time.Sleep(100 * time.Millisecond) - if i == 9 { - t.Fatalf("Server did not become ready in time: %v", err) - } - } - - testCases := []struct { - ExpectedErr error - ExpectedBody string - Time time.Duration - ExpectedStatusCode int - }{ - {Time: 500 * time.Millisecond, ExpectedBody: "example.com", ExpectedStatusCode: StatusOK, ExpectedErr: nil}, - {Time: 3 * time.Second, ExpectedBody: "", ExpectedStatusCode: StatusOK, ExpectedErr: fasthttputil.ErrInmemoryListenerClosed}, - } - - for _, tc := range testCases { - time.Sleep(tc.Time) - - req := fasthttp.AcquireRequest() - req.SetRequestURI("http://example.com") - - client := fasthttp.HostClient{} - client.Dial = func(_ string) (net.Conn, error) { return ln.Dial() } - - resp := fasthttp.AcquireResponse() - err := client.Do(req, resp) - - require.Equal(t, tc.ExpectedErr, err) - require.Equal(t, tc.ExpectedStatusCode, resp.StatusCode()) - require.Equal(t, tc.ExpectedBody, string(resp.Body())) - - fasthttp.ReleaseRequest(req) - fasthttp.ReleaseResponse(resp) - } - - mu.Lock() - err := <-errs - require.True(t, shutdown) - require.NoError(t, err) - mu.Unlock() + t.Run("Shutdown With Timeout Error", func(t *testing.T) { + testGracefulShutdown(t, 1*time.Nanosecond) + }) } -// go test -run Test_Listen_Graceful_Shutdown_Timeout -func Test_Listen_Graceful_Shutdown_Timeout(t *testing.T) { +func testGracefulShutdown(t *testing.T, shutdownTimeout time.Duration) { + t.Helper() + var mu sync.Mutex - var shutdownSuccess bool - var shutdownTimeoutError error + var shutdown bool + var receivedErr error app := New() - app.Get("/", func(c Ctx) error { + time.Sleep(10 * time.Millisecond) return c.SendString(c.Hostname()) }) ln := fasthttputil.NewInmemoryListener() - errs := make(chan error) + errs := make(chan error, 1) + + app.hooks.OnPostShutdown(func(err error) error { + mu.Lock() + defer mu.Unlock() + shutdown = true + receivedErr = err + return nil + }) go func() { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) @@ -137,93 +82,83 @@ func Test_Listen_Graceful_Shutdown_Timeout(t *testing.T) { errs <- app.Listener(ln, ListenConfig{ DisableStartupMessage: true, GracefulContext: ctx, - ShutdownTimeout: 500 * time.Millisecond, - OnShutdownSuccess: func() { - mu.Lock() - shutdownSuccess = true - mu.Unlock() - }, - OnShutdownError: func(err error) { - mu.Lock() - shutdownTimeoutError = err - mu.Unlock() - }, + ShutdownTimeout: shutdownTimeout, }) }() - // Server readiness check - for i := 0; i < 10; i++ { + require.Eventually(t, func() bool { conn, err := ln.Dial() - // To test a graceful shutdown timeout, do not close the connection. if err == nil { - _ = conn - break - } - // Wait a bit before retrying - time.Sleep(100 * time.Millisecond) - if i == 9 { - t.Fatalf("Server did not become ready in time: %v", err) + if err := conn.Close(); err != nil { + t.Logf("error closing connection: %v", err) + } + return true } + return false + }, time.Second, 100*time.Millisecond, "Server failed to become ready") + + client := fasthttp.HostClient{ + Dial: func(_ string) (net.Conn, error) { return ln.Dial() }, } - testCases := []struct { - ExpectedErr error - ExpectedShutdownError error - ExpectedBody string - Time time.Duration - ExpectedStatusCode int - ExpectedShutdownSuccess bool - }{ + type testCase struct { + expectedErr error + expectedBody string + name string + waitTime time.Duration + expectedStatusCode int + closeConnection bool + } + + testCases := []testCase{ { - Time: 100 * time.Millisecond, - ExpectedBody: "example.com", - ExpectedStatusCode: StatusOK, - ExpectedErr: nil, - ExpectedShutdownError: nil, - ExpectedShutdownSuccess: false, + name: "Server running normally", + waitTime: 500 * time.Millisecond, + expectedBody: "example.com", + expectedStatusCode: StatusOK, + expectedErr: nil, + closeConnection: true, }, { - Time: 3 * time.Second, - ExpectedBody: "", - ExpectedStatusCode: StatusOK, - ExpectedErr: fasthttputil.ErrInmemoryListenerClosed, - ExpectedShutdownError: context.DeadlineExceeded, - ExpectedShutdownSuccess: false, + name: "Server shutdown complete", + waitTime: 3 * time.Second, + expectedBody: "", + expectedStatusCode: StatusOK, + expectedErr: fasthttputil.ErrInmemoryListenerClosed, + closeConnection: true, }, } for _, tc := range testCases { - time.Sleep(tc.Time) + t.Run(tc.name, func(t *testing.T) { + time.Sleep(tc.waitTime) - req := fasthttp.AcquireRequest() - req.SetRequestURI("http://example.com") + req := fasthttp.AcquireRequest() + defer fasthttp.ReleaseRequest(req) + req.SetRequestURI("http://example.com") - client := fasthttp.HostClient{} - client.Dial = func(_ string) (net.Conn, error) { return ln.Dial() } + resp := fasthttp.AcquireResponse() + defer fasthttp.ReleaseResponse(resp) - resp := fasthttp.AcquireResponse() - err := client.Do(req, resp) + err := client.Do(req, resp) - if err == nil { - require.NoError(t, err) - require.Equal(t, tc.ExpectedStatusCode, resp.StatusCode()) - require.Equal(t, tc.ExpectedBody, string(resp.Body())) - } else { - require.ErrorIs(t, err, tc.ExpectedErr) - } - - mu.Lock() - require.Equal(t, tc.ExpectedShutdownSuccess, shutdownSuccess) - require.Equal(t, tc.ExpectedShutdownError, shutdownTimeoutError) - mu.Unlock() - - fasthttp.ReleaseRequest(req) - fasthttp.ReleaseResponse(resp) + if tc.expectedErr == nil { + require.NoError(t, err) + require.Equal(t, tc.expectedStatusCode, resp.StatusCode()) + require.Equal(t, tc.expectedBody, utils.UnsafeString(resp.Body())) + } else { + require.ErrorIs(t, err, tc.expectedErr) + } + }) } mu.Lock() - err := <-errs - require.NoError(t, err) + require.True(t, shutdown) + if shutdownTimeout == 1*time.Nanosecond { + require.Error(t, receivedErr) + require.ErrorIs(t, receivedErr, context.DeadlineExceeded) + } + require.NoError(t, <-errs) mu.Unlock() } From 856537ef011ad4f6159e7120045380ceddd94922 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 23 Feb 2025 20:56:57 +0000 Subject: [PATCH 05/53] build(deps): bump github.com/valyala/fasthttp from 1.58.0 to 1.59.0 Bumps [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp) from 1.58.0 to 1.59.0. - [Release notes](https://github.com/valyala/fasthttp/releases) - [Commits](https://github.com/valyala/fasthttp/compare/v1.58.0...v1.59.0) --- updated-dependencies: - dependency-name: github.com/valyala/fasthttp dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 5 ++--- go.sum | 10 ++++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 37f61dbc..731af3fe 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/stretchr/testify v1.10.0 github.com/tinylib/msgp v1.2.5 github.com/valyala/bytebufferpool v1.0.0 - github.com/valyala/fasthttp v1.58.0 + github.com/valyala/fasthttp v1.59.0 golang.org/x/crypto v0.33.0 ) @@ -22,9 +22,8 @@ require ( github.com/klauspost/compress v1.17.11 // indirect github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/valyala/tcplisten v1.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect - golang.org/x/net v0.33.0 // indirect + golang.org/x/net v0.35.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index ae49ec80..bad0d773 100644 --- a/go.sum +++ b/go.sum @@ -26,18 +26,16 @@ github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE= -github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw= -github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= -github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI= +github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= From e7c1b3e5e2c374b7ff3def38292625950b3e8313 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Sun, 23 Feb 2025 16:00:39 -0500 Subject: [PATCH 06/53] Add new config option --- ctx.go | 1 + middleware/static/static.go | 1 + 2 files changed, 2 insertions(+) diff --git a/ctx.go b/ctx.go index 3af6b600..aecfacdc 100644 --- a/ctx.go +++ b/ctx.go @@ -1555,6 +1555,7 @@ func (c *DefaultCtx) SendFile(file string, config ...SendFile) error { AcceptByteRange: cfg.ByteRange, Compress: cfg.Compress, CompressBrotli: cfg.Compress, + CompressZstd: cfg.Compress, CompressedFileSuffixes: c.app.config.CompressedFileSuffixes, CacheDuration: cfg.CacheDuration, SkipCache: cfg.CacheDuration < 0, diff --git a/middleware/static/static.go b/middleware/static/static.go index 7afc7798..e9101cd0 100644 --- a/middleware/static/static.go +++ b/middleware/static/static.go @@ -66,6 +66,7 @@ func New(root string, cfg ...Config) fiber.Handler { AcceptByteRange: config.ByteRange, Compress: config.Compress, CompressBrotli: config.Compress, // Brotli compression won't work without this + CompressZstd: config.Compress, // Zstd compression won't work without this CompressedFileSuffixes: c.App().Config().CompressedFileSuffixes, CacheDuration: config.CacheDuration, SkipCache: config.CacheDuration < 0, From b1e858bc76fef274bd3adad86c394c656acab0e9 Mon Sep 17 00:00:00 2001 From: Kashiwa <13825170+ksw2000@users.noreply.github.com> Date: Mon, 24 Feb 2025 15:12:05 +0800 Subject: [PATCH 07/53] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20the=20va?= =?UTF-8?q?lue=20of=20map=20is=20unused=20in=20uniqueRouteStack=20(#3320)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> --- helpers.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/helpers.go b/helpers.go index 7e9e202c..eea6b370 100644 --- a/helpers.go +++ b/helpers.go @@ -192,12 +192,10 @@ func (app *App) methodExistCustom(c CustomCtx) bool { // uniqueRouteStack drop all not unique routes from the slice func uniqueRouteStack(stack []*Route) []*Route { var unique []*Route - m := make(map[*Route]int) + m := make(map[*Route]struct{}) for _, v := range stack { if _, ok := m[v]; !ok { - // Unique key found. Record position and collect - // in result. - m[v] = len(unique) + m[v] = struct{}{} unique = append(unique, v) } } From ef4effc8a06ccd1c4fa6fd02ecc2cf735d421a57 Mon Sep 17 00:00:00 2001 From: Kashiwa <13825170+ksw2000@users.noreply.github.com> Date: Mon, 24 Feb 2025 15:12:47 +0800 Subject: [PATCH 08/53] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20Reduce?= =?UTF-8?q?=20the=20Memory=20Usage=20of=20ignoreHeaders=20(#3322)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ♻️ Refactor: reduce the memory usage of ignoreHeaders Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> --- middleware/cache/cache.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/middleware/cache/cache.go b/middleware/cache/cache.go index 723b5321..f4a8ee3e 100644 --- a/middleware/cache/cache.go +++ b/middleware/cache/cache.go @@ -35,17 +35,17 @@ const ( noStore = "no-store" ) -var ignoreHeaders = map[string]any{ - "Connection": nil, - "Keep-Alive": nil, - "Proxy-Authenticate": nil, - "Proxy-Authorization": nil, - "TE": nil, - "Trailers": nil, - "Transfer-Encoding": nil, - "Upgrade": nil, - "Content-Type": nil, // already stored explicitly by the cache manager - "Content-Encoding": nil, // already stored explicitly by the cache manager +var ignoreHeaders = map[string]struct{}{ + "Connection": {}, + "Keep-Alive": {}, + "Proxy-Authenticate": {}, + "Proxy-Authorization": {}, + "TE": {}, + "Trailers": {}, + "Transfer-Encoding": {}, + "Upgrade": {}, + "Content-Type": {}, // already stored explicitly by the cache manager + "Content-Encoding": {}, // already stored explicitly by the cache manager } var cacheableStatusCodes = map[int]bool{ From 0d1ade4626e627ea9737ed7a3659dccd0204b38a Mon Sep 17 00:00:00 2001 From: Kashiwa <13825170+ksw2000@users.noreply.github.com> Date: Mon, 24 Feb 2025 15:13:47 +0800 Subject: [PATCH 09/53] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20Improve?= =?UTF-8?q?=20Performance=20of=20getSplicedStrList=20(#3318)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ Refactor: improve performance of getSplicedStrList goos: linux goarch: amd64 pkg: github.com/gofiber/fiber/v3 cpu: AMD EPYC 7763 64-Core Processor │ old.txt │ new.txt │ │ sec/op │ sec/op vs base │ _Utils_GetSplicedStrList-4 66.12n ± 1% 51.05n ± 1% -22.79% (p=0.000 n=50) │ old.txt │ new.txt │ │ B/op │ B/op vs base │ _Utils_GetSplicedStrList-4 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=50) ¹ ¹ all samples are equal │ old.txt │ new.txt │ │ allocs/op │ allocs/op vs base │ _Utils_GetSplicedStrList-4 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=50) ¹ ¹ all samples are equal * 🚨 Test: add more test for getSplicedStrList * 🩹 Fix: golangci-lint ifElseChain * ♻️ Refactor: use more descriptive variable names --- helpers.go | 33 ++++++++++++++------------------- helpers_test.go | 20 ++++++++++++++++++++ 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/helpers.go b/helpers.go index eea6b370..18728e56 100644 --- a/helpers.go +++ b/helpers.go @@ -321,28 +321,23 @@ func getSplicedStrList(headerValue string, dst []string) []string { return nil } - var ( - index int - character rune - lastElementEndsAt int - insertIndex int - ) - for index, character = range headerValue + "$" { - if character == ',' || index == len(headerValue) { - if insertIndex >= len(dst) { - oldSlice := dst - dst = make([]string, len(dst)+(len(dst)>>1)+2) - copy(dst, oldSlice) - } - dst[insertIndex] = utils.TrimLeft(headerValue[lastElementEndsAt:index], ' ') - lastElementEndsAt = index + 1 - insertIndex++ + dst = dst[:0] + segmentStart := 0 + isLeadingSpace := true + for i, c := range headerValue { + switch { + case c == ',': + dst = append(dst, headerValue[segmentStart:i]) + segmentStart = i + 1 + isLeadingSpace = true + case c == ' ' && isLeadingSpace: + segmentStart = i + 1 + default: + isLeadingSpace = false } } + dst = append(dst, headerValue[segmentStart:]) - if len(dst) > insertIndex { - dst = dst[:insertIndex] - } return dst } diff --git a/helpers_test.go b/helpers_test.go index 75698d87..5664bc48 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -303,6 +303,26 @@ func Test_Utils_GetSplicedStrList(t *testing.T) { headerValue: "gzip,", expectedList: []string{"gzip", ""}, }, + { + description: "has a space between words", + headerValue: " foo bar, hello world", + expectedList: []string{"foo bar", "hello world"}, + }, + { + description: "single comma", + headerValue: ",", + expectedList: []string{"", ""}, + }, + { + description: "multiple comma", + headerValue: ",,", + expectedList: []string{"", "", ""}, + }, + { + description: "comma with space", + headerValue: ", ,", + expectedList: []string{"", "", ""}, + }, } for _, tc := range testCases { From a7bf8171b1d218a91fb6c82c8578beb4dda74f8d Mon Sep 17 00:00:00 2001 From: RW Date: Mon, 24 Feb 2025 08:14:19 +0100 Subject: [PATCH 10/53] =?UTF-8?q?=F0=9F=90=9B=20bug:=20Fix=20handler=20ord?= =?UTF-8?q?er=20in=20routing=20(#3321)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix handler order in routing #3312 * fix handler order in routing #3312 * fix handler order in routing #3312 * fix handler order in routing #3312 * fix handler order in routing #3312 --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> --- app.go | 48 +++++++++++------------ app_test.go | 34 ++++++---------- docs/api/app.md | 22 +++++------ docs/partials/routing/handler.md | 28 +++++++------- group.go | 48 +++++++++++------------ hooks_test.go | 20 +++------- mount.go | 4 +- register.go | 66 ++++++++++++++++---------------- router.go | 43 +++++++++++---------- router_test.go | 56 +++++++++++++++++++++++---- 10 files changed, 198 insertions(+), 171 deletions(-) diff --git a/app.go b/app.go index 4bbd9efb..ec55f06a 100644 --- a/app.go +++ b/app.go @@ -750,7 +750,7 @@ func (app *App) Use(args ...any) Router { return app } - app.register([]string{methodUse}, prefix, nil, nil, handlers...) + app.register([]string{methodUse}, prefix, nil, handlers...) } return app @@ -758,67 +758,67 @@ func (app *App) Use(args ...any) Router { // Get registers a route for GET methods that requests a representation // of the specified resource. Requests using GET should only retrieve data. -func (app *App) Get(path string, handler Handler, middleware ...Handler) Router { - return app.Add([]string{MethodGet}, path, handler, middleware...) +func (app *App) Get(path string, handler Handler, handlers ...Handler) Router { + return app.Add([]string{MethodGet}, path, handler, handlers...) } // Head registers a route for HEAD methods that asks for a response identical // to that of a GET request, but without the response body. -func (app *App) Head(path string, handler Handler, middleware ...Handler) Router { - return app.Add([]string{MethodHead}, path, handler, middleware...) +func (app *App) Head(path string, handler Handler, handlers ...Handler) Router { + return app.Add([]string{MethodHead}, path, handler, handlers...) } // Post registers a route for POST methods that is used to submit an entity to the // specified resource, often causing a change in state or side effects on the server. -func (app *App) Post(path string, handler Handler, middleware ...Handler) Router { - return app.Add([]string{MethodPost}, path, handler, middleware...) +func (app *App) Post(path string, handler Handler, handlers ...Handler) Router { + return app.Add([]string{MethodPost}, path, handler, handlers...) } // Put registers a route for PUT methods that replaces all current representations // of the target resource with the request payload. -func (app *App) Put(path string, handler Handler, middleware ...Handler) Router { - return app.Add([]string{MethodPut}, path, handler, middleware...) +func (app *App) Put(path string, handler Handler, handlers ...Handler) Router { + return app.Add([]string{MethodPut}, path, handler, handlers...) } // Delete registers a route for DELETE methods that deletes the specified resource. -func (app *App) Delete(path string, handler Handler, middleware ...Handler) Router { - return app.Add([]string{MethodDelete}, path, handler, middleware...) +func (app *App) Delete(path string, handler Handler, handlers ...Handler) Router { + return app.Add([]string{MethodDelete}, path, handler, handlers...) } // Connect registers a route for CONNECT methods that establishes a tunnel to the // server identified by the target resource. -func (app *App) Connect(path string, handler Handler, middleware ...Handler) Router { - return app.Add([]string{MethodConnect}, path, handler, middleware...) +func (app *App) Connect(path string, handler Handler, handlers ...Handler) Router { + return app.Add([]string{MethodConnect}, path, handler, handlers...) } // Options registers a route for OPTIONS methods that is used to describe the // communication options for the target resource. -func (app *App) Options(path string, handler Handler, middleware ...Handler) Router { - return app.Add([]string{MethodOptions}, path, handler, middleware...) +func (app *App) Options(path string, handler Handler, handlers ...Handler) Router { + return app.Add([]string{MethodOptions}, path, handler, handlers...) } // Trace registers a route for TRACE methods that performs a message loop-back // test along the path to the target resource. -func (app *App) Trace(path string, handler Handler, middleware ...Handler) Router { - return app.Add([]string{MethodTrace}, path, handler, middleware...) +func (app *App) Trace(path string, handler Handler, handlers ...Handler) Router { + return app.Add([]string{MethodTrace}, path, handler, handlers...) } // Patch registers a route for PATCH methods that is used to apply partial // modifications to a resource. -func (app *App) Patch(path string, handler Handler, middleware ...Handler) Router { - return app.Add([]string{MethodPatch}, path, handler, middleware...) +func (app *App) Patch(path string, handler Handler, handlers ...Handler) Router { + return app.Add([]string{MethodPatch}, path, handler, handlers...) } // Add allows you to specify multiple HTTP methods to register a route. -func (app *App) Add(methods []string, path string, handler Handler, middleware ...Handler) Router { - app.register(methods, path, nil, handler, middleware...) +func (app *App) Add(methods []string, path string, handler Handler, handlers ...Handler) Router { + app.register(methods, path, nil, append([]Handler{handler}, handlers...)...) return app } // All will register the handler on all HTTP methods -func (app *App) All(path string, handler Handler, middleware ...Handler) Router { - return app.Add(app.config.RequestMethods, path, handler, middleware...) +func (app *App) All(path string, handler Handler, handlers ...Handler) Router { + return app.Add(app.config.RequestMethods, path, handler, handlers...) } // Group is used for Routes with common prefix to define a new sub-router with optional middleware. @@ -828,7 +828,7 @@ func (app *App) All(path string, handler Handler, middleware ...Handler) Router func (app *App) Group(prefix string, handlers ...Handler) Router { grp := &Group{Prefix: prefix, app: app} if len(handlers) > 0 { - app.register([]string{methodUse}, prefix, grp, nil, handlers...) + app.register([]string{methodUse}, prefix, grp, handlers...) } if err := app.hooks.executeOnGroupHooks(*grp); err != nil { panic(err) diff --git a/app_test.go b/app_test.go index 18f6cef9..97d48fce 100644 --- a/app_test.go +++ b/app_test.go @@ -480,14 +480,10 @@ func Test_App_Use_Params(t *testing.T) { require.NoError(t, err, "app.Test(req)") require.Equal(t, 200, resp.StatusCode, "Status code") - defer func() { - if err := recover(); err != nil { - require.Equal(t, "use: invalid handler func()\n", fmt.Sprintf("%v", err)) - } - }() - - app.Use("/:param/*", func() { - // this should panic + require.PanicsWithValue(t, "use: invalid handler func()\n", func() { + app.Use("/:param/*", func() { + // this should panic + }) }) } @@ -1149,12 +1145,10 @@ func Test_App_Mixed_Routes_WithSameLen(t *testing.T) { func Test_App_Group_Invalid(t *testing.T) { t.Parallel() - defer func() { - if err := recover(); err != nil { - require.Equal(t, "use: invalid handler int\n", fmt.Sprintf("%v", err)) - } - }() - New().Group("/").Use(1) + + require.PanicsWithValue(t, "use: invalid handler int\n", func() { + New().Group("/").Use(1) + }) } func Test_App_Group(t *testing.T) { @@ -1379,14 +1373,10 @@ func Test_App_Init_Error_View(t *testing.T) { t.Parallel() app := New(Config{Views: invalidView{}}) - defer func() { - if err := recover(); err != nil { - require.Equal(t, "implement me", fmt.Sprintf("%v", err)) - } - }() - - err := app.config.Views.Render(nil, "", nil) - require.NoError(t, err) + require.PanicsWithValue(t, "implement me", func() { + //nolint:errcheck // not needed + _ = app.config.Views.Render(nil, "", nil) + }) } // go test -run Test_App_Stack diff --git a/docs/api/app.md b/docs/api/app.md index 6582159c..23171a24 100644 --- a/docs/api/app.md +++ b/docs/api/app.md @@ -135,18 +135,18 @@ func (app *App) Route(path string) Register ```go type Register interface { - All(handler Handler, middleware ...Handler) Register - Get(handler Handler, middleware ...Handler) Register - Head(handler Handler, middleware ...Handler) Register - Post(handler Handler, middleware ...Handler) Register - Put(handler Handler, middleware ...Handler) Register - Delete(handler Handler, middleware ...Handler) Register - Connect(handler Handler, middleware ...Handler) Register - Options(handler Handler, middleware ...Handler) Register - Trace(handler Handler, middleware ...Handler) Register - Patch(handler Handler, middleware ...Handler) Register + All(handler Handler, handlers ...Handler) Register + Get(handler Handler, handlers ...Handler) Register + Head(handler Handler, handlers ...Handler) Register + Post(handler Handler, handlers ...Handler) Register + Put(handler Handler, handlers ...Handler) Register + Delete(handler Handler, handlers ...Handler) Register + Connect(handler Handler, handlers ...Handler) Register + Options(handler Handler, handlers ...Handler) Register + Trace(handler Handler, handlers ...Handler) Register + Patch(handler Handler, handlers ...Handler) Register - Add(methods []string, handler Handler, middleware ...Handler) Register + Add(methods []string, handler Handler, handlers ...Handler) Register Route(path string) Register } diff --git a/docs/partials/routing/handler.md b/docs/partials/routing/handler.md index 2b94a07c..8a0a1e09 100644 --- a/docs/partials/routing/handler.md +++ b/docs/partials/routing/handler.md @@ -9,22 +9,22 @@ Registers a route bound to a specific [HTTP method](https://developer.mozilla.or ```go title="Signatures" // HTTP methods -func (app *App) Get(path string, handler Handler, middlewares ...Handler) Router -func (app *App) Head(path string, handler Handler, middlewares ...Handler) Router -func (app *App) Post(path string, handler Handler, middlewares ...Handler) Router -func (app *App) Put(path string, handler Handler, middlewares ...Handler) Router -func (app *App) Delete(path string, handler Handler, middlewares ...Handler) Router -func (app *App) Connect(path string, handler Handler, middlewares ...Handler) Router -func (app *App) Options(path string, handler Handler, middlewares ...Handler) Router -func (app *App) Trace(path string, handler Handler, middlewares ...Handler) Router -func (app *App) Patch(path string, handler Handler, middlewares ...Handler) Router +func (app *App) Get(path string, handler Handler, handlers ...Handler) Router +func (app *App) Head(path string, handler Handler, handlers ...Handler) Router +func (app *App) Post(path string, handler Handler, handlers ...Handler) Router +func (app *App) Put(path string, handler Handler, handlers ...Handler) Router +func (app *App) Delete(path string, handler Handler, handlers ...Handler) Router +func (app *App) Connect(path string, handler Handler, handlers ...Handler) Router +func (app *App) Options(path string, handler Handler, handlers ...Handler) Router +func (app *App) Trace(path string, handler Handler, handlers ...Handler) Router +func (app *App) Patch(path string, handler Handler, handlers ...Handler) Router // Add allows you to specify a method as value -func (app *App) Add(method, path string, handler Handler, middlewares ...Handler) Router +func (app *App) Add(method, path string, handler Handler, handlers ...Handler) Router // All will register the route on all HTTP methods // Almost the same as app.Use but not bound to prefixes -func (app *App) All(path string, handler Handler, middlewares ...Handler) Router +func (app *App) All(path string, handler Handler, handlers ...Handler) Router ``` ```go title="Examples" @@ -47,9 +47,9 @@ Can be used for middleware packages and prefix catchers. These routes will only func (app *App) Use(args ...any) Router // Different usage variations -func (app *App) Use(handler Handler, middlewares ...Handler) Router -func (app *App) Use(path string, handler Handler, middlewares ...Handler) Router -func (app *App) Use(paths []string, handler Handler, middlewares ...Handler) Router +func (app *App) Use(handler Handler, handlers ...Handler) Router +func (app *App) Use(path string, handler Handler, handlers ...Handler) Router +func (app *App) Use(paths []string, handler Handler, handlers ...Handler) Router func (app *App) Use(path string, app *App) Router ``` diff --git a/group.go b/group.go index 4142b0ba..fe5cc8d0 100644 --- a/group.go +++ b/group.go @@ -97,7 +97,7 @@ func (grp *Group) Use(args ...any) Router { return grp } - grp.app.register([]string{methodUse}, getGroupPath(grp.Prefix, prefix), grp, nil, handlers...) + grp.app.register([]string{methodUse}, getGroupPath(grp.Prefix, prefix), grp, handlers...) } if !grp.anyRouteDefined { @@ -109,60 +109,60 @@ func (grp *Group) Use(args ...any) Router { // Get registers a route for GET methods that requests a representation // of the specified resource. Requests using GET should only retrieve data. -func (grp *Group) Get(path string, handler Handler, middleware ...Handler) Router { - return grp.Add([]string{MethodGet}, path, handler, middleware...) +func (grp *Group) Get(path string, handler Handler, handlers ...Handler) Router { + return grp.Add([]string{MethodGet}, path, handler, handlers...) } // Head registers a route for HEAD methods that asks for a response identical // to that of a GET request, but without the response body. -func (grp *Group) Head(path string, handler Handler, middleware ...Handler) Router { - return grp.Add([]string{MethodHead}, path, handler, middleware...) +func (grp *Group) Head(path string, handler Handler, handlers ...Handler) Router { + return grp.Add([]string{MethodHead}, path, handler, handlers...) } // Post registers a route for POST methods that is used to submit an entity to the // specified resource, often causing a change in state or side effects on the server. -func (grp *Group) Post(path string, handler Handler, middleware ...Handler) Router { - return grp.Add([]string{MethodPost}, path, handler, middleware...) +func (grp *Group) Post(path string, handler Handler, handlers ...Handler) Router { + return grp.Add([]string{MethodPost}, path, handler, handlers...) } // Put registers a route for PUT methods that replaces all current representations // of the target resource with the request payload. -func (grp *Group) Put(path string, handler Handler, middleware ...Handler) Router { - return grp.Add([]string{MethodPut}, path, handler, middleware...) +func (grp *Group) Put(path string, handler Handler, handlers ...Handler) Router { + return grp.Add([]string{MethodPut}, path, handler, handlers...) } // Delete registers a route for DELETE methods that deletes the specified resource. -func (grp *Group) Delete(path string, handler Handler, middleware ...Handler) Router { - return grp.Add([]string{MethodDelete}, path, handler, middleware...) +func (grp *Group) Delete(path string, handler Handler, handlers ...Handler) Router { + return grp.Add([]string{MethodDelete}, path, handler, handlers...) } // Connect registers a route for CONNECT methods that establishes a tunnel to the // server identified by the target resource. -func (grp *Group) Connect(path string, handler Handler, middleware ...Handler) Router { - return grp.Add([]string{MethodConnect}, path, handler, middleware...) +func (grp *Group) Connect(path string, handler Handler, handlers ...Handler) Router { + return grp.Add([]string{MethodConnect}, path, handler, handlers...) } // Options registers a route for OPTIONS methods that is used to describe the // communication options for the target resource. -func (grp *Group) Options(path string, handler Handler, middleware ...Handler) Router { - return grp.Add([]string{MethodOptions}, path, handler, middleware...) +func (grp *Group) Options(path string, handler Handler, handlers ...Handler) Router { + return grp.Add([]string{MethodOptions}, path, handler, handlers...) } // Trace registers a route for TRACE methods that performs a message loop-back // test along the path to the target resource. -func (grp *Group) Trace(path string, handler Handler, middleware ...Handler) Router { - return grp.Add([]string{MethodTrace}, path, handler, middleware...) +func (grp *Group) Trace(path string, handler Handler, handlers ...Handler) Router { + return grp.Add([]string{MethodTrace}, path, handler, handlers...) } // Patch registers a route for PATCH methods that is used to apply partial // modifications to a resource. -func (grp *Group) Patch(path string, handler Handler, middleware ...Handler) Router { - return grp.Add([]string{MethodPatch}, path, handler, middleware...) +func (grp *Group) Patch(path string, handler Handler, handlers ...Handler) Router { + return grp.Add([]string{MethodPatch}, path, handler, handlers...) } // Add allows you to specify multiple HTTP methods to register a route. -func (grp *Group) Add(methods []string, path string, handler Handler, middleware ...Handler) Router { - grp.app.register(methods, getGroupPath(grp.Prefix, path), grp, handler, middleware...) +func (grp *Group) Add(methods []string, path string, handler Handler, handlers ...Handler) Router { + grp.app.register(methods, getGroupPath(grp.Prefix, path), grp, append([]Handler{handler}, handlers...)...) if !grp.anyRouteDefined { grp.anyRouteDefined = true } @@ -171,8 +171,8 @@ func (grp *Group) Add(methods []string, path string, handler Handler, middleware } // All will register the handler on all HTTP methods -func (grp *Group) All(path string, handler Handler, middleware ...Handler) Router { - _ = grp.Add(grp.app.config.RequestMethods, path, handler, middleware...) +func (grp *Group) All(path string, handler Handler, handlers ...Handler) Router { + _ = grp.Add(grp.app.config.RequestMethods, path, handler, handlers...) return grp } @@ -183,7 +183,7 @@ func (grp *Group) All(path string, handler Handler, middleware ...Handler) Route func (grp *Group) Group(prefix string, handlers ...Handler) Router { prefix = getGroupPath(grp.Prefix, prefix) if len(handlers) > 0 { - grp.app.register([]string{methodUse}, prefix, grp, nil, handlers...) + grp.app.register([]string{methodUse}, prefix, grp, handlers...) } // Create new group diff --git a/hooks_test.go b/hooks_test.go index b95e0733..14269d0d 100644 --- a/hooks_test.go +++ b/hooks_test.go @@ -2,7 +2,6 @@ package fiber import ( "errors" - "fmt" "testing" "time" @@ -83,17 +82,14 @@ func Test_Hook_OnName(t *testing.T) { func Test_Hook_OnName_Error(t *testing.T) { t.Parallel() app := New() - defer func() { - if err := recover(); err != nil { - require.Equal(t, "unknown error", fmt.Sprintf("%v", err)) - } - }() app.Hooks().OnName(func(_ Route) error { return errors.New("unknown error") }) - app.Get("/", testSimpleHandler).Name("index") + require.PanicsWithError(t, "unknown error", func() { + app.Get("/", testSimpleHandler).Name("index") + }) } func Test_Hook_OnGroup(t *testing.T) { @@ -167,18 +163,14 @@ func Test_Hook_OnGroupName(t *testing.T) { func Test_Hook_OnGroupName_Error(t *testing.T) { t.Parallel() app := New() - defer func() { - if err := recover(); err != nil { - require.Equal(t, "unknown error", fmt.Sprintf("%v", err)) - } - }() app.Hooks().OnGroupName(func(_ Group) error { return errors.New("unknown error") }) - grp := app.Group("/x").Name("x.") - grp.Get("/test", testSimpleHandler) + require.PanicsWithError(t, "unknown error", func() { + _ = app.Group("/x").Name("x.") + }) } func Test_Hook_OnPrehutdown(t *testing.T) { diff --git a/mount.go b/mount.go index d97b226d..f05ec82d 100644 --- a/mount.go +++ b/mount.go @@ -55,7 +55,7 @@ func (app *App) mount(prefix string, subApp *App) Router { // register mounted group mountGroup := &Group{Prefix: prefix, app: subApp} - app.register([]string{methodUse}, prefix, mountGroup, nil) + app.register([]string{methodUse}, prefix, mountGroup) // Execute onMount hooks if err := subApp.hooks.executeOnMountHooks(app); err != nil { @@ -85,7 +85,7 @@ func (grp *Group) mount(prefix string, subApp *App) Router { // register mounted group mountGroup := &Group{Prefix: groupPath, app: subApp} - grp.app.register([]string{methodUse}, groupPath, mountGroup, nil) + grp.app.register([]string{methodUse}, groupPath, mountGroup) // Execute onMount hooks if err := subApp.hooks.executeOnMountHooks(grp.app); err != nil { diff --git a/register.go b/register.go index ab67447c..c7e8a12a 100644 --- a/register.go +++ b/register.go @@ -6,18 +6,18 @@ package fiber // Register defines all router handle interface generate by Route(). type Register interface { - All(handler Handler, middleware ...Handler) Register - Get(handler Handler, middleware ...Handler) Register - Head(handler Handler, middleware ...Handler) Register - Post(handler Handler, middleware ...Handler) Register - Put(handler Handler, middleware ...Handler) Register - Delete(handler Handler, middleware ...Handler) Register - Connect(handler Handler, middleware ...Handler) Register - Options(handler Handler, middleware ...Handler) Register - Trace(handler Handler, middleware ...Handler) Register - Patch(handler Handler, middleware ...Handler) Register + All(handler Handler, handlers ...Handler) Register + Get(handler Handler, handlers ...Handler) Register + Head(handler Handler, handlers ...Handler) Register + Post(handler Handler, handlers ...Handler) Register + Put(handler Handler, handlers ...Handler) Register + Delete(handler Handler, handlers ...Handler) Register + Connect(handler Handler, handlers ...Handler) Register + Options(handler Handler, handlers ...Handler) Register + Trace(handler Handler, handlers ...Handler) Register + Patch(handler Handler, handlers ...Handler) Register - Add(methods []string, handler Handler, middleware ...Handler) Register + Add(methods []string, handler Handler, handlers ...Handler) Register Route(path string) Register } @@ -45,68 +45,68 @@ type Registering struct { // }) // // This method will match all HTTP verbs: GET, POST, PUT, HEAD etc... -func (r *Registering) All(handler Handler, middleware ...Handler) Register { - r.app.register([]string{methodUse}, r.path, nil, handler, middleware...) +func (r *Registering) All(handler Handler, handlers ...Handler) Register { + r.app.register([]string{methodUse}, r.path, nil, append([]Handler{handler}, handlers...)...) return r } // Get registers a route for GET methods that requests a representation // of the specified resource. Requests using GET should only retrieve data. -func (r *Registering) Get(handler Handler, middleware ...Handler) Register { - r.app.Add([]string{MethodGet}, r.path, handler, middleware...) +func (r *Registering) Get(handler Handler, handlers ...Handler) Register { + r.app.Add([]string{MethodGet}, r.path, handler, handlers...) return r } // Head registers a route for HEAD methods that asks for a response identical // to that of a GET request, but without the response body. -func (r *Registering) Head(handler Handler, middleware ...Handler) Register { - return r.Add([]string{MethodHead}, handler, middleware...) +func (r *Registering) Head(handler Handler, handlers ...Handler) Register { + return r.Add([]string{MethodHead}, handler, handlers...) } // Post registers a route for POST methods that is used to submit an entity to the // specified resource, often causing a change in state or side effects on the server. -func (r *Registering) Post(handler Handler, middleware ...Handler) Register { - return r.Add([]string{MethodPost}, handler, middleware...) +func (r *Registering) Post(handler Handler, handlers ...Handler) Register { + return r.Add([]string{MethodPost}, handler, handlers...) } // Put registers a route for PUT methods that replaces all current representations // of the target resource with the request payload. -func (r *Registering) Put(handler Handler, middleware ...Handler) Register { - return r.Add([]string{MethodPut}, handler, middleware...) +func (r *Registering) Put(handler Handler, handlers ...Handler) Register { + return r.Add([]string{MethodPut}, handler, handlers...) } // Delete registers a route for DELETE methods that deletes the specified resource. -func (r *Registering) Delete(handler Handler, middleware ...Handler) Register { - return r.Add([]string{MethodDelete}, handler, middleware...) +func (r *Registering) Delete(handler Handler, handlers ...Handler) Register { + return r.Add([]string{MethodDelete}, handler, handlers...) } // Connect registers a route for CONNECT methods that establishes a tunnel to the // server identified by the target resource. -func (r *Registering) Connect(handler Handler, middleware ...Handler) Register { - return r.Add([]string{MethodConnect}, handler, middleware...) +func (r *Registering) Connect(handler Handler, handlers ...Handler) Register { + return r.Add([]string{MethodConnect}, handler, handlers...) } // Options registers a route for OPTIONS methods that is used to describe the // communication options for the target resource. -func (r *Registering) Options(handler Handler, middleware ...Handler) Register { - return r.Add([]string{MethodOptions}, handler, middleware...) +func (r *Registering) Options(handler Handler, handlers ...Handler) Register { + return r.Add([]string{MethodOptions}, handler, handlers...) } // Trace registers a route for TRACE methods that performs a message loop-back // test along the r.Path to the target resource. -func (r *Registering) Trace(handler Handler, middleware ...Handler) Register { - return r.Add([]string{MethodTrace}, handler, middleware...) +func (r *Registering) Trace(handler Handler, handlers ...Handler) Register { + return r.Add([]string{MethodTrace}, handler, handlers...) } // Patch registers a route for PATCH methods that is used to apply partial // modifications to a resource. -func (r *Registering) Patch(handler Handler, middleware ...Handler) Register { - return r.Add([]string{MethodPatch}, handler, middleware...) +func (r *Registering) Patch(handler Handler, handlers ...Handler) Register { + return r.Add([]string{MethodPatch}, handler, handlers...) } // Add allows you to specify multiple HTTP methods to register a route. -func (r *Registering) Add(methods []string, handler Handler, middleware ...Handler) Register { - r.app.register(methods, r.path, nil, handler, middleware...) +func (r *Registering) Add(methods []string, handler Handler, handlers ...Handler) Register { + r.app.register(methods, r.path, nil, append([]Handler{handler}, handlers...)...) return r } diff --git a/router.go b/router.go index 9612da17..a14d2edd 100644 --- a/router.go +++ b/router.go @@ -20,18 +20,18 @@ import ( type Router interface { Use(args ...any) Router - Get(path string, handler Handler, middleware ...Handler) Router - Head(path string, handler Handler, middleware ...Handler) Router - Post(path string, handler Handler, middleware ...Handler) Router - Put(path string, handler Handler, middleware ...Handler) Router - Delete(path string, handler Handler, middleware ...Handler) Router - Connect(path string, handler Handler, middleware ...Handler) Router - Options(path string, handler Handler, middleware ...Handler) Router - Trace(path string, handler Handler, middleware ...Handler) Router - Patch(path string, handler Handler, middleware ...Handler) Router + Get(path string, handler Handler, handlers ...Handler) Router + Head(path string, handler Handler, handlers ...Handler) Router + Post(path string, handler Handler, handlers ...Handler) Router + Put(path string, handler Handler, handlers ...Handler) Router + Delete(path string, handler Handler, handlers ...Handler) Router + Connect(path string, handler Handler, handlers ...Handler) Router + Options(path string, handler Handler, handlers ...Handler) Router + Trace(path string, handler Handler, handlers ...Handler) Router + Patch(path string, handler Handler, handlers ...Handler) Router - Add(methods []string, path string, handler Handler, middleware ...Handler) Router - All(path string, handler Handler, middleware ...Handler) Router + Add(methods []string, path string, handler Handler, handlers ...Handler) Router + All(path string, handler Handler, handlers ...Handler) Router Group(prefix string, handlers ...Handler) Router @@ -318,10 +318,16 @@ func (*App) copyRoute(route *Route) *Route { } } -func (app *App) register(methods []string, pathRaw string, group *Group, handler Handler, middleware ...Handler) { - handlers := middleware - if handler != nil { - handlers = append(handlers, handler) +func (app *App) register(methods []string, pathRaw string, group *Group, handlers ...Handler) { + // A regular route requires at least one ctx handler + if len(handlers) == 0 && group == nil { + panic(fmt.Sprintf("missing handler/middleware in route: %s\n", pathRaw)) + } + // No nil handlers allowed + for _, h := range handlers { + if nil == h { + panic(fmt.Sprintf("nil handler in route: %s\n", pathRaw)) + } } // Precompute path normalization ONCE @@ -343,17 +349,14 @@ func (app *App) register(methods []string, pathRaw string, group *Group, handler parsedRaw := parseRoute(pathRaw, app.customConstraints...) parsedPretty := parseRoute(pathPretty, app.customConstraints...) + isMount := group != nil && group.app != app + for _, method := range methods { method = utils.ToUpper(method) if method != methodUse && app.methodInt(method) == -1 { panic(fmt.Sprintf("add: invalid http method %s\n", method)) } - isMount := group != nil && group.app != app - if len(handlers) == 0 && !isMount { - panic(fmt.Sprintf("missing handler/middleware in route: %s\n", pathRaw)) - } - isUse := method == methodUse isStar := pathClean == "/*" isRoot := pathClean == "/" diff --git a/router_test.go b/router_test.go index fe5b3429..0ac0c212 100644 --- a/router_test.go +++ b/router_test.go @@ -7,7 +7,6 @@ package fiber import ( "encoding/json" "errors" - "fmt" "io" "net/http" "net/http/httptest" @@ -31,6 +30,39 @@ func init() { } } +func Test_Route_Handler_Order(t *testing.T) { + t.Parallel() + + app := New() + + var order []int + + handler1 := func(c Ctx) error { + order = append(order, 1) + return c.Next() + } + handler2 := func(c Ctx) error { + order = append(order, 2) + return c.Next() + } + handler3 := func(c Ctx) error { + order = append(order, 3) + return c.Next() + } + + app.Get("/test", handler1, handler2, handler3, func(c Ctx) error { + order = append(order, 4) + return c.SendStatus(200) + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/test", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + + expectedOrder := []int{1, 2, 3, 4} + require.Equal(t, expectedOrder, order, "Handler order") +} + func Test_Route_Match_SameLength(t *testing.T) { t.Parallel() @@ -294,12 +326,22 @@ func Test_Router_Register_Missing_Handler(t *testing.T) { t.Parallel() app := New() - defer func() { - if err := recover(); err != nil { - require.Equal(t, "missing handler/middleware in route: /doe\n", fmt.Sprintf("%v", err)) - } - }() - app.register([]string{"USE"}, "/doe", nil, nil) + + t.Run("No Handler", func(t *testing.T) { + t.Parallel() + + require.PanicsWithValue(t, "missing handler/middleware in route: /doe\n", func() { + app.register([]string{"USE"}, "/doe", nil) + }) + }) + + t.Run("Nil Handler", func(t *testing.T) { + t.Parallel() + + require.PanicsWithValue(t, "nil handler in route: /doe\n", func() { + app.register([]string{"USE"}, "/doe", nil, nil) + }) + }) } func Test_Ensure_Router_Interface_Implementation(t *testing.T) { From c85ec75fe69dacf1b249274a6e179fe930fe3f8c Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Mon, 24 Feb 2025 15:24:50 -0500 Subject: [PATCH 11/53] =?UTF-8?q?=F0=9F=A7=B9=20chore:=20Add=20go1.24=20to?= =?UTF-8?q?=20CI=20matrix=20(#3325)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add go1.24 to CI matrix * Create codecov.yml * Lower coverage threshold to 0.5% --- .github/README.md | 4 ++-- .github/codecov.yml | 9 +++++++++ .github/workflows/test.yml | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 .github/codecov.yml diff --git a/.github/README.md b/.github/README.md index cb7b19b3..4559cc72 100644 --- a/.github/README.md +++ b/.github/README.md @@ -124,7 +124,7 @@ We **listen** to our users in [issues](https://github.com/gofiber/fiber/issues), ## ⚠️ Limitations -- Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber v3 has been tested with Go version 1.23. +- Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber v3 has been tested with Go version 1.23 or higher. - Fiber is not compatible with net/http interfaces. This means you will not be able to use projects like gqlgen, go-swagger, or any others which are part of the net/http ecosystem. ## 👀 Examples @@ -708,7 +708,7 @@ List of externally hosted middleware modules and maintained by the [Fiber team]( | :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------- | | [contrib](https://github.com/gofiber/contrib) | Third-party middlewares | | [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | -| [template](https://github.com/gofiber/template) | This package contains 9 template engines that can be used with Fiber `v3`. Go version 1.23 or higher is required. | +| [template](https://github.com/gofiber/template) | This package contains 9 template engines that can be used with Fiber. | ## 🕶️ Awesome List diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 00000000..9a4b8ca6 --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,9 @@ +# ignore files or directories to be scanned by codecov +ignore: + - "./docs/" + +coverage: + status: + project: + default: + threshold: 0.5% diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a106152b..d5ddb215 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: unit: strategy: matrix: - go-version: [1.23.x] + go-version: [1.23.x, 1.24.x] platform: [ubuntu-latest, windows-latest, macos-latest, macos-13] runs-on: ${{ matrix.platform }} steps: From 38ffe73243be23207bf4af0d29a541914fa3eedf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 00:33:01 +0000 Subject: [PATCH 12/53] build(deps): bump golang.org/x/crypto from 0.33.0 to 0.35.0 Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.33.0 to 0.35.0. - [Commits](https://github.com/golang/crypto/compare/v0.33.0...v0.35.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 3 +-- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 731af3fe..e61f45e6 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,6 @@ module github.com/gofiber/fiber/v3 go 1.23 - require ( github.com/gofiber/schema v1.2.0 github.com/gofiber/utils/v2 v2.0.0-beta.7 @@ -12,7 +11,7 @@ require ( github.com/tinylib/msgp v1.2.5 github.com/valyala/bytebufferpool v1.0.0 github.com/valyala/fasthttp v1.59.0 - golang.org/x/crypto v0.33.0 + golang.org/x/crypto v0.35.0 ) require ( diff --git a/go.sum b/go.sum index bad0d773..4028ece8 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From e47c37f8f2238a3c5479c7128926a0fdf901562e Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Tue, 25 Feb 2025 07:35:13 -0500 Subject: [PATCH 13/53] Run go mod tidy --- go.mod | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index e61f45e6..ff227e11 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,7 @@ module github.com/gofiber/fiber/v3 -go 1.23 +go 1.23.0 + require ( github.com/gofiber/schema v1.2.0 github.com/gofiber/utils/v2 v2.0.0-beta.7 From 435fa42360b77210cda75c60472995e3b951c402 Mon Sep 17 00:00:00 2001 From: Kashiwa <13825170+ksw2000@users.noreply.github.com> Date: Wed, 26 Feb 2025 00:11:46 +0800 Subject: [PATCH 14/53] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20Migrate?= =?UTF-8?q?=20randString=20to=20rand=20v2=20(#3329)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ Refactor: migrate randString to rand/v2 * 🩹 Fix: golangci-lint --- client/hooks.go | 31 +++++++++++++++---------------- client/hooks_test.go | 2 +- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/client/hooks.go b/client/hooks.go index d804babb..ca6f5d6c 100644 --- a/client/hooks.go +++ b/client/hooks.go @@ -3,24 +3,22 @@ package client import ( "fmt" "io" - "math/rand" + "math/rand/v2" "mime/multipart" "os" "path/filepath" "regexp" "strconv" "strings" - "time" "github.com/gofiber/utils/v2" "github.com/valyala/fasthttp" ) -var ( - protocolCheck = regexp.MustCompile(`^https?://.*$`) - - headerAccept = "Accept" +var protocolCheck = regexp.MustCompile(`^https?://.*$`) +const ( + headerAccept = "Accept" applicationJSON = "application/json" applicationCBOR = "application/cbor" applicationXML = "application/xml" @@ -30,25 +28,26 @@ var ( letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" letterIdxBits = 6 // 6 bits to represent a letter index letterIdxMask = 1<= 0; { + //nolint:gosec // Not a concern + for i, cache, remain := n-1, rand.Uint64(), letterIdxMax; i >= 0; { if remain == 0 { - cache, remain = src.Int63(), letterIdxMax + //nolint:gosec // Not a concern + cache, remain = rand.Uint64(), letterIdxMax } - if idx := int(cache & int64(letterIdxMask)); idx < length { + if idx := cache & letterIdxMask; idx < length { b[i] = letterBytes[idx] i-- } - cache >>= int64(letterIdxBits) + cache >>= letterIdxBits remain-- } @@ -134,7 +133,7 @@ func parserRequestHeader(c *Client, req *Request) error { req.RawRequest.Header.SetContentType(multipartFormData) // If boundary is default, append a random string to it. if req.boundary == boundary { - req.boundary += randString(16) + req.boundary += unsafeRandString(16) } req.RawRequest.Header.SetMultipartFormBoundary(req.boundary) default: diff --git a/client/hooks_test.go b/client/hooks_test.go index 15ac9a38..d33d2bcf 100644 --- a/client/hooks_test.go +++ b/client/hooks_test.go @@ -38,7 +38,7 @@ func Test_Rand_String(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - got := randString(tt.args) + got := unsafeRandString(tt.args) require.Len(t, got, tt.args) }) } From d6d48d8cb758f80d435fbc35bd9dd17b0055474a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 17:12:25 +0100 Subject: [PATCH 15/53] build(deps): bump github.com/gofiber/schema from 1.2.0 to 1.3.0 (#3308) Bumps [github.com/gofiber/schema](https://github.com/gofiber/schema) from 1.2.0 to 1.3.0. - [Release notes](https://github.com/gofiber/schema/releases) - [Commits](https://github.com/gofiber/schema/compare/v1.2.0...v1.3.0) --- updated-dependencies: - dependency-name: github.com/gofiber/schema dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ff227e11..c1431515 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/gofiber/fiber/v3 go 1.23.0 require ( - github.com/gofiber/schema v1.2.0 + github.com/gofiber/schema v1.3.0 github.com/gofiber/utils/v2 v2.0.0-beta.7 github.com/google/uuid v1.6.0 github.com/mattn/go-colorable v0.1.14 diff --git a/go.sum b/go.sum index 4028ece8..de1f50ff 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/gofiber/schema v1.2.0 h1:j+ZRrNnUa/0ZuWrn/6kAtAufEr4jCJ+JuTURAMxNSZg= -github.com/gofiber/schema v1.2.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c= +github.com/gofiber/schema v1.3.0 h1:K3F3wYzAY+aivfCCEHPufCthu5/13r/lzp1nuk6mr3Q= +github.com/gofiber/schema v1.3.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c= github.com/gofiber/utils/v2 v2.0.0-beta.7 h1:NnHFrRHvhrufPABdWajcKZejz9HnCWmT/asoxRsiEbQ= github.com/gofiber/utils/v2 v2.0.0-beta.7/go.mod h1:J/M03s+HMdZdvhAeyh76xT72IfVqBzuz/OJkrMa7cwU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= From bc4c920ea6b36d2b9d0396853a640b8b043951b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=2E=20Efe=20=C3=87etin?= Date: Tue, 25 Feb 2025 21:45:19 +0300 Subject: [PATCH 16/53] bind: add support for multipart file binding (#3309) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * deps: update schema to v1.3.0 * bind: add support for multipart file binding * bind: fix linter * improve coverage * fix linter * add test cases --------- Co-authored-by: René --- binder/form.go | 12 ++- binder/form_test.go | 63 ++++++++++++++-- binder/mapping.go | 33 ++++++-- binder/mapping_test.go | 166 +++++++++++++++++++++++++++++++++++++++++ docs/api/bind.md | 32 ++++++++ docs/whats_new.md | 1 + 6 files changed, 294 insertions(+), 13 deletions(-) diff --git a/binder/form.go b/binder/form.go index a8f5b852..fab28034 100644 --- a/binder/form.go +++ b/binder/form.go @@ -1,6 +1,8 @@ package binder import ( + "mime/multipart" + "github.com/gofiber/utils/v2" "github.com/valyala/fasthttp" ) @@ -59,7 +61,15 @@ func (b *FormBinding) bindMultipart(req *fasthttp.Request, out any) error { } } - return parse(b.Name(), out, data) + files := make(map[string][]*multipart.FileHeader) + for key, values := range multipartForm.File { + err = formatBindData(out, files, key, values, b.EnableSplitting, true) + if err != nil { + return err + } + } + + return parse(b.Name(), out, data, files) } // Reset resets the FormBinding binder. diff --git a/binder/form_test.go b/binder/form_test.go index 55023cb3..d961f873 100644 --- a/binder/form_test.go +++ b/binder/form_test.go @@ -2,6 +2,7 @@ package binder import ( "bytes" + "io" "mime/multipart" "testing" @@ -98,10 +99,12 @@ func Test_FormBinder_BindMultipart(t *testing.T) { } type User struct { - Name string `form:"name"` - Names []string `form:"names"` - Posts []Post `form:"posts"` - Age int `form:"age"` + Avatar *multipart.FileHeader `form:"avatar"` + Name string `form:"name"` + Names []string `form:"names"` + Posts []Post `form:"posts"` + Avatars []*multipart.FileHeader `form:"avatars"` + Age int `form:"age"` } var user User @@ -118,6 +121,24 @@ func Test_FormBinder_BindMultipart(t *testing.T) { require.NoError(t, mw.WriteField("posts[1][title]", "post2")) require.NoError(t, mw.WriteField("posts[2][title]", "post3")) + writer, err := mw.CreateFormFile("avatar", "avatar.txt") + require.NoError(t, err) + + _, err = writer.Write([]byte("avatar")) + require.NoError(t, err) + + writer, err = mw.CreateFormFile("avatars", "avatar1.txt") + require.NoError(t, err) + + _, err = writer.Write([]byte("avatar1")) + require.NoError(t, err) + + writer, err = mw.CreateFormFile("avatars", "avatar2.txt") + require.NoError(t, err) + + _, err = writer.Write([]byte("avatar2")) + require.NoError(t, err) + require.NoError(t, mw.Close()) req.Header.SetContentType(mw.FormDataContentType()) @@ -127,7 +148,7 @@ func Test_FormBinder_BindMultipart(t *testing.T) { fasthttp.ReleaseRequest(req) }) - err := b.Bind(req, &user) + err = b.Bind(req, &user) require.NoError(t, err) require.Equal(t, "john", user.Name) @@ -139,6 +160,38 @@ func Test_FormBinder_BindMultipart(t *testing.T) { require.Equal(t, "post1", user.Posts[0].Title) require.Equal(t, "post2", user.Posts[1].Title) require.Equal(t, "post3", user.Posts[2].Title) + + require.NotNil(t, user.Avatar) + require.Equal(t, "avatar.txt", user.Avatar.Filename) + require.Equal(t, "application/octet-stream", user.Avatar.Header.Get("Content-Type")) + + file, err := user.Avatar.Open() + require.NoError(t, err) + + content, err := io.ReadAll(file) + require.NoError(t, err) + require.Equal(t, "avatar", string(content)) + + require.Len(t, user.Avatars, 2) + require.Equal(t, "avatar1.txt", user.Avatars[0].Filename) + require.Equal(t, "application/octet-stream", user.Avatars[0].Header.Get("Content-Type")) + + file, err = user.Avatars[0].Open() + require.NoError(t, err) + + content, err = io.ReadAll(file) + require.NoError(t, err) + require.Equal(t, "avatar1", string(content)) + + require.Equal(t, "avatar2.txt", user.Avatars[1].Filename) + require.Equal(t, "application/octet-stream", user.Avatars[1].Header.Get("Content-Type")) + + file, err = user.Avatars[1].Open() + require.NoError(t, err) + + content, err = io.ReadAll(file) + require.NoError(t, err) + require.Equal(t, "avatar2", string(content)) } func Benchmark_FormBinder_BindMultipart(b *testing.B) { diff --git a/binder/mapping.go b/binder/mapping.go index 70cb9cbc..bc95d028 100644 --- a/binder/mapping.go +++ b/binder/mapping.go @@ -3,6 +3,7 @@ package binder import ( "errors" "fmt" + "mime/multipart" "reflect" "strings" "sync" @@ -69,7 +70,7 @@ func init() { } // parse data into the map or struct -func parse(aliasTag string, out any, data map[string][]string) error { +func parse(aliasTag string, out any, data map[string][]string, files ...map[string][]*multipart.FileHeader) error { ptrVal := reflect.ValueOf(out) // Get pointer value @@ -83,11 +84,11 @@ func parse(aliasTag string, out any, data map[string][]string) error { } // Parse into the struct - return parseToStruct(aliasTag, out, data) + return parseToStruct(aliasTag, out, data, files...) } // Parse data into the struct with gorilla/schema -func parseToStruct(aliasTag string, out any, data map[string][]string) error { +func parseToStruct(aliasTag string, out any, data map[string][]string, files ...map[string][]*multipart.FileHeader) error { // Get decoder from pool schemaDecoder := decoderPoolMap[aliasTag].Get().(*schema.Decoder) //nolint:errcheck,forcetypeassert // not needed defer decoderPoolMap[aliasTag].Put(schemaDecoder) @@ -95,7 +96,7 @@ func parseToStruct(aliasTag string, out any, data map[string][]string) error { // Set alias tag schemaDecoder.SetAliasTag(aliasTag) - if err := schemaDecoder.Decode(out, data); err != nil { + if err := schemaDecoder.Decode(out, data, files...); err != nil { return fmt.Errorf("bind: %w", err) } @@ -250,7 +251,7 @@ func FilterFlags(content string) string { return content } -func formatBindData[T any](out any, data map[string][]string, key string, value T, enableSplitting, supportBracketNotation bool) error { //nolint:revive // it's okay +func formatBindData[T, K any](out any, data map[string][]T, key string, value K, enableSplitting, supportBracketNotation bool) error { //nolint:revive // it's okay var err error if supportBracketNotation && strings.Contains(key, "[") { key, err = parseParamSquareBrackets(key) @@ -261,10 +262,28 @@ func formatBindData[T any](out any, data map[string][]string, key string, value switch v := any(value).(type) { case string: - assignBindData(out, data, key, v, enableSplitting) + dataMap, ok := any(data).(map[string][]string) + if !ok { + return fmt.Errorf("unsupported value type: %T", value) + } + + assignBindData(out, dataMap, key, v, enableSplitting) case []string: + dataMap, ok := any(data).(map[string][]string) + if !ok { + return fmt.Errorf("unsupported value type: %T", value) + } + for _, val := range v { - assignBindData(out, data, key, val, enableSplitting) + assignBindData(out, dataMap, key, val, enableSplitting) + } + case []*multipart.FileHeader: + for _, val := range v { + valT, ok := any(val).(T) + if !ok { + return fmt.Errorf("unsupported value type: %T", value) + } + data[key] = append(data[key], valT) } default: return fmt.Errorf("unsupported value type: %T", value) diff --git a/binder/mapping_test.go b/binder/mapping_test.go index 75cdc783..9c7b92ee 100644 --- a/binder/mapping_test.go +++ b/binder/mapping_test.go @@ -2,6 +2,7 @@ package binder import ( "errors" + "mime/multipart" "reflect" "testing" @@ -9,6 +10,8 @@ import ( ) func Test_EqualFieldType(t *testing.T) { + t.Parallel() + var out int require.False(t, equalFieldType(&out, reflect.Int, "key")) @@ -47,6 +50,8 @@ func Test_EqualFieldType(t *testing.T) { } func Test_ParseParamSquareBrackets(t *testing.T) { + t.Parallel() + tests := []struct { err error input string @@ -101,6 +106,8 @@ func Test_ParseParamSquareBrackets(t *testing.T) { for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { + t.Parallel() + result, err := parseParamSquareBrackets(tt.input) if tt.err != nil { require.Error(t, err) @@ -114,6 +121,8 @@ func Test_ParseParamSquareBrackets(t *testing.T) { } func Test_parseToMap(t *testing.T) { + t.Parallel() + inputMap := map[string][]string{ "key1": {"value1", "value2"}, "key2": {"value3"}, @@ -147,6 +156,8 @@ func Test_parseToMap(t *testing.T) { } func Test_FilterFlags(t *testing.T) { + t.Parallel() + tests := []struct { input string expected string @@ -172,8 +183,163 @@ func Test_FilterFlags(t *testing.T) { for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { + t.Parallel() + result := FilterFlags(tt.input) require.Equal(t, tt.expected, result) }) } } + +func TestFormatBindData(t *testing.T) { + t.Parallel() + + t.Run("string value with valid key", func(t *testing.T) { + t.Parallel() + + out := struct{}{} + data := make(map[string][]string) + err := formatBindData(out, data, "name", "John", false, false) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(data["name"]) != 1 || data["name"][0] != "John" { + t.Fatalf("expected data[\"name\"] = [John], got %v", data["name"]) + } + }) + + t.Run("unsupported value type", func(t *testing.T) { + t.Parallel() + + out := struct{}{} + data := make(map[string][]string) + err := formatBindData(out, data, "age", 30, false, false) // int is unsupported + if err == nil { + t.Fatal("expected an error, got nil") + } + }) + + t.Run("bracket notation parsing error", func(t *testing.T) { + t.Parallel() + + out := struct{}{} + data := make(map[string][]string) + err := formatBindData(out, data, "invalid[", "value", false, true) // malformed bracket notation + if err == nil { + t.Fatal("expected an error, got nil") + } + }) + + t.Run("handling multipart file headers", func(t *testing.T) { + t.Parallel() + + out := struct{}{} + data := make(map[string][]*multipart.FileHeader) + files := []*multipart.FileHeader{ + {Filename: "file1.txt"}, + {Filename: "file2.txt"}, + } + err := formatBindData(out, data, "files", files, false, false) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(data["files"]) != 2 { + t.Fatalf("expected 2 files, got %d", len(data["files"])) + } + }) + + t.Run("type casting error", func(t *testing.T) { + t.Parallel() + + out := struct{}{} + data := map[string][]int{} // Incorrect type to force a casting error + err := formatBindData(out, data, "key", "value", false, false) + require.Equal(t, "unsupported value type: string", err.Error()) + }) +} + +func TestAssignBindData(t *testing.T) { + t.Parallel() + + t.Run("splitting enabled with comma", func(t *testing.T) { + t.Parallel() + + out := struct { + Colors []string `query:"colors"` + }{} + data := make(map[string][]string) + assignBindData(&out, data, "colors", "red,blue,green", true) + require.Len(t, data["colors"], 3) + }) + + t.Run("splitting disabled", func(t *testing.T) { + t.Parallel() + + var out []string + data := make(map[string][]string) + assignBindData(out, data, "color", "red,blue", false) + require.Len(t, data["color"], 1) + }) +} + +func Test_parseToStruct_MismatchedData(t *testing.T) { + t.Parallel() + + type User struct { + Name string `query:"name"` + Age int `query:"age"` + } + + data := map[string][]string{ + "name": {"John"}, + "age": {"invalidAge"}, + } + + err := parseToStruct("query", &User{}, data) + require.Error(t, err) + require.EqualError(t, err, "bind: schema: error converting value for \"age\"") +} + +func Test_formatBindData_ErrorCases(t *testing.T) { + t.Parallel() + + t.Run("unsupported value type int", func(t *testing.T) { + t.Parallel() + + out := struct{}{} + data := make(map[string][]string) + err := formatBindData(out, data, "age", 30, false, false) // int is unsupported + require.Error(t, err) + require.EqualError(t, err, "unsupported value type: int") + }) + + t.Run("unsupported value type map", func(t *testing.T) { + t.Parallel() + + out := struct{}{} + data := make(map[string][]string) + err := formatBindData(out, data, "map", map[string]string{"key": "value"}, false, false) // map is unsupported + require.Error(t, err) + require.EqualError(t, err, "unsupported value type: map[string]string") + }) + + t.Run("bracket notation parsing error", func(t *testing.T) { + t.Parallel() + + out := struct{}{} + data := make(map[string][]string) + err := formatBindData(out, data, "invalid[", "value", false, true) // malformed bracket notation + require.Error(t, err) + require.EqualError(t, err, "unmatched brackets") + }) + + t.Run("type casting error for []string", func(t *testing.T) { + t.Parallel() + + out := struct{}{} + data := make(map[string][]string) + err := formatBindData(out, data, "names", 123, false, false) // invalid type for []string + require.Error(t, err) + require.EqualError(t, err, "unsupported value type: int") + }) +} diff --git a/docs/api/bind.md b/docs/api/bind.md index d2b33631..eaad6305 100644 --- a/docs/api/bind.md +++ b/docs/api/bind.md @@ -120,6 +120,38 @@ curl -X POST -H "Content-Type: application/x-www-form-urlencoded" --data "name=j curl -X POST -H "Content-Type: multipart/form-data" -F "name=john" -F "pass=doe" localhost:3000 ``` +:::info +If you need to bind multipart file, you can use `*multipart.FileHeader`, `*[]*multipart.FileHeader` or `[]*multipart.FileHeader` as a field type. +::: + +```go title="Example" +type Person struct { + Name string `form:"name"` + Pass string `form:"pass"` + Avatar *multipart.FileHeader `form:"avatar"` +} + +app.Post("/", func(c fiber.Ctx) error { + p := new(Person) + + if err := c.Bind().Form(p); err != nil { + return err + } + + log.Println(p.Name) // john + log.Println(p.Pass) // doe + log.Println(p.Avatar.Filename) // file.txt + + // ... +}) +``` + +Run tests with the following `curl` command: + +```bash +curl -X POST -H "Content-Type: multipart/form-data" -F "name=john" -F "pass=doe" -F 'avatar=@filename' localhost:3000 +``` + ### JSON Binds the request JSON body to a struct. diff --git a/docs/whats_new.md b/docs/whats_new.md index 7f0b6322..4185a7e3 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -546,6 +546,7 @@ Fiber v3 introduces a new binding mechanism that simplifies the process of bindi - Unified binding from URL parameters, query parameters, headers, and request bodies. - Support for custom binders and constraints. - Improved error handling and validation. +- Support multipart file binding for `*multipart.FileHeader`, `*[]*multipart.FileHeader`, and `[]*multipart.FileHeader` field types.
Example From 47e9c8fa6d3f236733d282857cdb1e4a457e7e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Thu, 27 Feb 2025 08:45:47 +0100 Subject: [PATCH 17/53] only run benchmark when golang files are changed --- .github/workflows/benchmark.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index a086223d..d91f1494 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -3,11 +3,11 @@ on: branches: - master - main - paths-ignore: - - "**/*.md" + paths: + - "**.go" pull_request: - paths-ignore: - - "**/*.md" + paths: + - "**.go" permissions: # deployments permission to deploy GitHub pages website From b568915b70c43a7ed02f5909ee72f1151ecd79e5 Mon Sep 17 00:00:00 2001 From: Giovanni Rivera Date: Wed, 26 Feb 2025 23:46:41 -0800 Subject: [PATCH 18/53] =?UTF-8?q?=F0=9F=93=9A=20docs:=20Add=20Retry=20Addo?= =?UTF-8?q?n=20documentation=20(#3330)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📚 Doc: Add addon tab to /docs * 📚 Doc: Add retry to addon docs * 📚 Doc: Update retry README.md * 🎨 Styles: Update addon/retry docs to respect markdownlint-cli2 * 📚 Doc: Update addon tab description to be singular * 📚 Doc: Use retry prefix in retry docs * 📚 Doc: Add retry addon to whats_new.md * 🎨 Styles: Update whats_new.md to respect markdownlint-cli2 --- addon/retry/README.md | 47 ++++++++++---- docs/addon/_category_.json | 9 +++ docs/addon/retry.md | 126 ++++++++++++++++++++++++++++++++++++ docs/client/_category_.json | 2 +- docs/extra/_category_.json | 2 +- docs/guide/_category_.json | 2 +- docs/whats_new.md | 54 ++++++++++++++++ 7 files changed, 228 insertions(+), 14 deletions(-) create mode 100644 docs/addon/_category_.json create mode 100644 docs/addon/retry.md diff --git a/addon/retry/README.md b/addon/retry/README.md index a3af4407..e6e1c34b 100644 --- a/addon/retry/README.md +++ b/addon/retry/README.md @@ -19,17 +19,47 @@ a jitter is a way to break synchronization across the client and avoid collision ## Signatures ```go -func NewExponentialBackoff(config ...Config) *ExponentialBackoff +func NewExponentialBackoff(config ...retry.Config) *retry.ExponentialBackoff ``` ## Examples -Firstly, import the addon from Fiber, - ```go +package main + import ( + "fmt" + "github.com/gofiber/fiber/v3/addon/retry" + "github.com/gofiber/fiber/v3/client" ) + +func main() { + expBackoff := retry.NewExponentialBackoff(retry.Config{}) + + // Local variables that will be used inside of Retry + var resp *client.Response + var err error + + // Retry a network request and return an error to signify to try again + err = expBackoff.Retry(func() error { + client := client.New() + resp, err = client.Get("https://gofiber.io") + if err != nil { + return fmt.Errorf("GET gofiber.io failed: %w", err) + } + if resp.StatusCode() != 200 { + return fmt.Errorf("GET gofiber.io did not return OK 200") + } + return nil + }) + + // If all retries failed, panic + if err != nil { + panic(err) + } + fmt.Printf("GET gofiber.io succeeded with status code %d\n", resp.StatusCode()) +} ``` ## Default Config @@ -58,28 +88,23 @@ type Config struct { // // Optional. Default: 1 * time.Second InitialInterval time.Duration - + // MaxBackoffTime defines maximum time duration for backoff algorithm. When // the algorithm is reached this time, rest of the retries will be maximum // 32 seconds. // // Optional. Default: 32 * time.Second MaxBackoffTime time.Duration - + // Multiplier defines multiplier number of the backoff algorithm. // // Optional. Default: 2.0 Multiplier float64 - + // MaxRetryCount defines maximum retry count for the backoff algorithm. // // Optional. Default: 10 MaxRetryCount int - - // currentInterval tracks the current waiting time. - // - // Optional. Default: 1 * time.Second - currentInterval time.Duration } ``` diff --git a/docs/addon/_category_.json b/docs/addon/_category_.json new file mode 100644 index 00000000..0d3a6ea3 --- /dev/null +++ b/docs/addon/_category_.json @@ -0,0 +1,9 @@ +{ + "label": "\uD83D\uDD0C Addon", + "position": 5, + "collapsed": true, + "link": { + "type": "generated-index", + "description": "Addon is an additional useful package that can be used in Fiber." + } +} diff --git a/docs/addon/retry.md b/docs/addon/retry.md new file mode 100644 index 00000000..2ed6a9f5 --- /dev/null +++ b/docs/addon/retry.md @@ -0,0 +1,126 @@ +--- +id: retry +--- + +# Retry Addon + +Retry addon for [Fiber](https://github.com/gofiber/fiber) designed to apply retry mechanism for unsuccessful network +operations. This addon uses an exponential backoff algorithm with jitter. It calls the function multiple times and tries +to make it successful. If all calls are failed, then, it returns an error. It adds a jitter at each retry step because adding +a jitter is a way to break synchronization across the client and avoid collision. + +## Table of Contents + +- [Retry Addon](#retry-addon) +- [Table of Contents](#table-of-contents) +- [Signatures](#signatures) +- [Examples](#examples) +- [Default Config](#default-config) +- [Custom Config](#custom-config) +- [Config](#config) +- [Default Config Example](#default-config-example) + +## Signatures + +```go +func NewExponentialBackoff(config ...retry.Config) *retry.ExponentialBackoff +``` + +## Examples + +```go +package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v3/addon/retry" + "github.com/gofiber/fiber/v3/client" +) + +func main() { + expBackoff := retry.NewExponentialBackoff(retry.Config{}) + + // Local variables that will be used inside of Retry + var resp *client.Response + var err error + + // Retry a network request and return an error to signify to try again + err = expBackoff.Retry(func() error { + client := client.New() + resp, err = client.Get("https://gofiber.io") + if err != nil { + return fmt.Errorf("GET gofiber.io failed: %w", err) + } + if resp.StatusCode() != 200 { + return fmt.Errorf("GET gofiber.io did not return OK 200") + } + return nil + }) + + // If all retries failed, panic + if err != nil { + panic(err) + } + fmt.Printf("GET gofiber.io succeeded with status code %d\n", resp.StatusCode()) +} +``` + +## Default Config + +```go +retry.NewExponentialBackoff() +``` + +## Custom Config + +```go +retry.NewExponentialBackoff(retry.Config{ + InitialInterval: 2 * time.Second, + MaxBackoffTime: 64 * time.Second, + Multiplier: 2.0, + MaxRetryCount: 15, +}) +``` + +## Config + +```go +// Config defines the config for addon. +type Config struct { + // InitialInterval defines the initial time interval for backoff algorithm. + // + // Optional. Default: 1 * time.Second + InitialInterval time.Duration + + // MaxBackoffTime defines maximum time duration for backoff algorithm. When + // the algorithm is reached this time, rest of the retries will be maximum + // 32 seconds. + // + // Optional. Default: 32 * time.Second + MaxBackoffTime time.Duration + + // Multiplier defines multiplier number of the backoff algorithm. + // + // Optional. Default: 2.0 + Multiplier float64 + + // MaxRetryCount defines maximum retry count for the backoff algorithm. + // + // Optional. Default: 10 + MaxRetryCount int +} +``` + +## Default Config Example + +```go +// DefaultConfig is the default config for retry. +var DefaultConfig = Config{ + InitialInterval: 1 * time.Second, + MaxBackoffTime: 32 * time.Second, + Multiplier: 2.0, + MaxRetryCount: 10, + currentInterval: 1 * time.Second, +} +``` diff --git a/docs/client/_category_.json b/docs/client/_category_.json index 61fad7ac..f5a63aa2 100644 --- a/docs/client/_category_.json +++ b/docs/client/_category_.json @@ -1,6 +1,6 @@ { "label": "\uD83C\uDF0E Client", - "position": 5, + "position": 6, "link": { "type": "generated-index", "description": "HTTP client for Fiber." diff --git a/docs/extra/_category_.json b/docs/extra/_category_.json index 3398bed5..55d68c94 100644 --- a/docs/extra/_category_.json +++ b/docs/extra/_category_.json @@ -1,6 +1,6 @@ { "label": "\uD83E\uDDE9 Extra", - "position": 6, + "position": 8, "link": { "type": "generated-index", "description": "Extra contents for Fiber." diff --git a/docs/guide/_category_.json b/docs/guide/_category_.json index 62226d5d..fd203a8b 100644 --- a/docs/guide/_category_.json +++ b/docs/guide/_category_.json @@ -1,6 +1,6 @@ { "label": "\uD83D\uDCD6 Guide", - "position": 5, + "position": 7, "link": { "type": "generated-index", "description": "Guides for Fiber." diff --git a/docs/whats_new.md b/docs/whats_new.md index 4185a7e3..5c3dd6ac 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -33,6 +33,7 @@ Here's a quick overview of the changes in Fiber `v3`: - [Filesystem](#filesystem) - [Monitor](#monitor) - [Healthcheck](#healthcheck) +- [🔌 Addons](#-addons) - [📋 Migration guide](#-migration-guide) ## Drop for old Go versions @@ -939,6 +940,59 @@ The Healthcheck middleware has been enhanced to support more than two routes, wi Refer to the [healthcheck middleware migration guide](./middleware/healthcheck.md) or the [general migration guide](#-migration-guide) to review the changes. +## 🔌 Addons + +In v3, Fiber introduced Addons. Addons are additional useful packages that can be used in Fiber. + +### Retry + +The Retry addon is a new addon that implements a retry mechanism for unsuccessful network operations. It uses an exponential backoff algorithm with jitter. +It calls the function multiple times and tries to make it successful. If all calls are failed, then, it returns an error. +It adds a jitter at each retry step because adding a jitter is a way to break synchronization across the client and avoid collision. + +
+Example + +```go +package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v3/addon/retry" + "github.com/gofiber/fiber/v3/client" +) + +func main() { + expBackoff := retry.NewExponentialBackoff(retry.Config{}) + + // Local variables that will be used inside of Retry + var resp *client.Response + var err error + + // Retry a network request and return an error to signify to try again + err = expBackoff.Retry(func() error { + client := client.New() + resp, err = client.Get("https://gofiber.io") + if err != nil { + return fmt.Errorf("GET gofiber.io failed: %w", err) + } + if resp.StatusCode() != 200 { + return fmt.Errorf("GET gofiber.io did not return OK 200") + } + return nil + }) + + // If all retries failed, panic + if err != nil { + panic(err) + } + fmt.Printf("GET gofiber.io succeeded with status code %d\n", resp.StatusCode()) +} +``` + +
+ ## 📋 Migration guide - [🚀 App](#-app-1) From 8e395fd4e33a92cefee7119d5941065cde9d5c36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 12:36:56 +0000 Subject: [PATCH 19/53] build(deps): bump codecov/codecov-action from 5.3.1 to 5.4.0 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.3.1 to 5.4.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v5.3.1...v5.4.0) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d5ddb215..914f20d0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,7 +32,7 @@ jobs: - name: Upload coverage reports to Codecov if: ${{ matrix.platform == 'ubuntu-latest' && matrix.go-version == '1.23.x' }} - uses: codecov/codecov-action@v5.3.1 + uses: codecov/codecov-action@v5.4.0 with: token: ${{ secrets.CODECOV_TOKEN }} file: ./coverage.txt From 6afba957f13913b69447701ce3658f20a1e36be6 Mon Sep 17 00:00:00 2001 From: Kashiwa <13825170+ksw2000@users.noreply.github.com> Date: Sun, 2 Mar 2025 01:14:50 +0800 Subject: [PATCH 20/53] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20sorting=20error=20i?= =?UTF-8?q?n=20sortAcceptedTypes=20(#3331)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🩹 Fix: correct sorting error in sortAcceptedTypes. * ♻️ Refactor: remove redundant branch --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> --- helpers.go | 11 +++-------- helpers_test.go | 8 ++++---- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/helpers.go b/helpers.go index 18728e56..a452e13c 100644 --- a/helpers.go +++ b/helpers.go @@ -483,7 +483,7 @@ func getOffer(header []byte, isAccepted func(spec, offer string, specParams head if len(acceptedTypes) > 1 { // Sort accepted types by quality and specificity, preserving order of equal elements - sortAcceptedTypes(&acceptedTypes) + sortAcceptedTypes(acceptedTypes) } // Find the first offer that matches the accepted types @@ -511,19 +511,14 @@ func getOffer(header []byte, isAccepted func(spec, offer string, specParams head // A type with parameters has higher priority than an equivalent one without parameters. // e.g., text/html;a=1;b=2 comes before text/html;a=1 // See: https://www.rfc-editor.org/rfc/rfc9110#name-content-negotiation-fields -func sortAcceptedTypes(acceptedTypes *[]acceptedType) { - if acceptedTypes == nil || len(*acceptedTypes) < 2 { - return - } - at := *acceptedTypes - +func sortAcceptedTypes(at []acceptedType) { for i := 1; i < len(at); i++ { lo, hi := 0, i-1 for lo <= hi { mid := (lo + hi) / 2 if at[i].quality < at[mid].quality || (at[i].quality == at[mid].quality && at[i].specificity < at[mid].specificity) || - (at[i].quality == at[mid].quality && at[i].specificity < at[mid].specificity && len(at[i].params) < len(at[mid].params)) || + (at[i].quality == at[mid].quality && at[i].specificity == at[mid].specificity && len(at[i].params) < len(at[mid].params)) || (at[i].quality == at[mid].quality && at[i].specificity == at[mid].specificity && len(at[i].params) == len(at[mid].params) && at[i].order > at[mid].order) { lo = mid + 1 } else { diff --git a/helpers_test.go b/helpers_test.go index 5664bc48..5e56bade 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -354,7 +354,6 @@ func Test_Utils_SortAcceptedTypes(t *testing.T) { {spec: "text/html", quality: 1, specificity: 3, order: 0}, {spec: "text/*", quality: 0.5, specificity: 2, order: 1}, {spec: "*/*", quality: 0.1, specificity: 1, order: 2}, - {spec: "application/json", quality: 0.999, specificity: 3, order: 3}, {spec: "application/xml", quality: 1, specificity: 3, order: 4}, {spec: "application/pdf", quality: 1, specificity: 3, order: 5}, {spec: "image/png", quality: 1, specificity: 3, order: 6}, @@ -363,8 +362,9 @@ func Test_Utils_SortAcceptedTypes(t *testing.T) { {spec: "image/gif", quality: 1, specificity: 3, order: 9}, {spec: "text/plain", quality: 1, specificity: 3, order: 10}, {spec: "application/json", quality: 0.999, specificity: 3, params: headerParams{"a": []byte("1")}, order: 11}, + {spec: "application/json", quality: 0.999, specificity: 3, order: 3}, } - sortAcceptedTypes(&acceptedTypes) + sortAcceptedTypes(acceptedTypes) require.Equal(t, []acceptedType{ {spec: "text/html", quality: 1, specificity: 3, order: 0}, {spec: "application/xml", quality: 1, specificity: 3, order: 4}, @@ -390,7 +390,7 @@ func Benchmark_Utils_SortAcceptedTypes_Sorted(b *testing.B) { acceptedTypes[0] = acceptedType{spec: "text/html", quality: 1, specificity: 1, order: 0} acceptedTypes[1] = acceptedType{spec: "text/*", quality: 0.5, specificity: 1, order: 1} acceptedTypes[2] = acceptedType{spec: "*/*", quality: 0.1, specificity: 1, order: 2} - sortAcceptedTypes(&acceptedTypes) + sortAcceptedTypes(acceptedTypes) } require.Equal(b, "text/html", acceptedTypes[0].spec) require.Equal(b, "text/*", acceptedTypes[1].spec) @@ -414,7 +414,7 @@ func Benchmark_Utils_SortAcceptedTypes_Unsorted(b *testing.B) { acceptedTypes[8] = acceptedType{spec: "image/*", quality: 1, specificity: 2, order: 8} acceptedTypes[9] = acceptedType{spec: "image/gif", quality: 1, specificity: 3, order: 9} acceptedTypes[10] = acceptedType{spec: "text/plain", quality: 1, specificity: 3, order: 10} - sortAcceptedTypes(&acceptedTypes) + sortAcceptedTypes(acceptedTypes) } require.Equal(b, []acceptedType{ {spec: "text/html", quality: 1, specificity: 3, order: 0}, From 9e6f4fd408aa1c93dff654392614995a5c73748f Mon Sep 17 00:00:00 2001 From: Kashiwa <13825170+ksw2000@users.noreply.github.com> Date: Mon, 3 Mar 2025 15:31:20 +0800 Subject: [PATCH 21/53] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20reduce?= =?UTF-8?q?=20the=20memory=20usage=20of=20RoutePatternMatch=20(#3335)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ Refactor: improve RoutePatternMatch by adding RemoveEscapeCharBytes ``` goos: linux goarch: amd64 pkg: github.com/gofiber/fiber/v3 cpu: AMD EPYC 7763 64-Core Processor │ route_pattern_match_old.txt │ route_pattern_match_new.txt │ │ sec/op │ sec/op vs base │ _RoutePatternMatch//api/v1/const_|_match_|_/api/v1/const-4 263.4n ± 2% 249.0n ± 4% -5.47% (p=0.001 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1-4 258.7n ± 4% 244.7n ± 2% -5.43% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/-4 254.6n ± 4% 246.3n ± 2% -3.26% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/something-4 265.1n ± 4% 255.6n ± 3% -3.60% (p=0.001 n=10) _RoutePatternMatch//api/:param/fixedEnd_|_match_|_/api/abc/fixedEnd-4 775.9n ± 3% 775.6n ± 2% ~ (p=0.424 n=10) _RoutePatternMatch//api/:param/fixedEnd_|_not_match_|_/api/abc/def/fixedEnd-4 796.7n ± 3% 767.1n ± 2% -3.72% (p=0.001 n=10) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity-4 916.2n ± 1% 904.8n ± 3% ~ (p=0.052 n=10) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/-4 913.8n ± 4% 909.1n ± 3% ~ (p=0.393 n=10) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/1-4 915.0n ± 3% 907.2n ± 2% ~ (p=0.165 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v-4 917.5n ± 2% 876.7n ± 2% -4.46% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v2-4 918.5n ± 2% 886.8n ± 2% -3.45% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v1/-4 935.6n ± 2% 901.9n ± 2% -3.60% (p=0.000 n=10) geomean 588.3n 570.7n -2.99% │ route_pattern_match_old.txt │ route_pattern_match_new.txt │ │ B/op │ B/op vs base │ _RoutePatternMatch//api/v1/const_|_match_|_/api/v1/const-4 168.0 ± 0% 152.0 ± 0% -9.52% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1-4 160.0 ± 0% 144.0 ± 0% -10.00% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/-4 160.0 ± 0% 144.0 ± 0% -10.00% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/something-4 176.0 ± 0% 160.0 ± 0% -9.09% (p=0.000 n=10) _RoutePatternMatch//api/:param/fixedEnd_|_match_|_/api/abc/fixedEnd-4 440.0 ± 0% 440.0 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/:param/fixedEnd_|_not_match_|_/api/abc/def/fixedEnd-4 464.0 ± 0% 440.0 ± 0% -5.17% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity-4 536.0 ± 0% 536.0 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/-4 536.0 ± 0% 536.0 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/1-4 536.0 ± 0% 536.0 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v-4 544.0 ± 0% 528.0 ± 0% -2.94% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v2-4 544.0 ± 0% 528.0 ± 0% -2.94% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v1/-4 544.0 ± 0% 528.0 ± 0% -2.94% (p=0.000 n=10) geomean 353.7 337.9 -4.47% ¹ all samples are equal │ route_pattern_match_old.txt │ route_pattern_match_new.txt │ │ allocs/op │ allocs/op vs base │ _RoutePatternMatch//api/v1/const_|_match_|_/api/v1/const-4 6.000 ± 0% 5.000 ± 0% -16.67% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1-4 6.000 ± 0% 5.000 ± 0% -16.67% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/-4 6.000 ± 0% 5.000 ± 0% -16.67% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/something-4 6.000 ± 0% 5.000 ± 0% -16.67% (p=0.000 n=10) _RoutePatternMatch//api/:param/fixedEnd_|_match_|_/api/abc/fixedEnd-4 13.00 ± 0% 13.00 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/:param/fixedEnd_|_not_match_|_/api/abc/def/fixedEnd-4 14.00 ± 0% 13.00 ± 0% -7.14% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity-4 14.00 ± 0% 14.00 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/-4 14.00 ± 0% 14.00 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/1-4 14.00 ± 0% 14.00 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v-4 15.00 ± 0% 14.00 ± 0% -6.67% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v2-4 15.00 ± 0% 14.00 ± 0% -6.67% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v1/-4 15.00 ± 0% 14.00 ± 0% -6.67% (p=0.000 n=10) geomean 10.67 9.811 -8.08% ¹ all samples are equal ``` * ♻️ Refactor: returned type of analyseParameterPart and analyseConstantPart ``` goos: linux goarch: amd64 pkg: github.com/gofiber/fiber/v3 cpu: AMD EPYC 7763 64-Core Processor │ route_pattern_match_old.txt │ route_pattern_match_new3.txt │ │ sec/op │ sec/op vs base │ _RoutePatternMatch//api/v1/const_|_match_|_/api/v1/const-4 264.3n ± 2% 253.8n ± 2% -3.95% (p=0.001 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1-4 258.5n ± 1% 247.6n ± 2% -4.24% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/-4 260.8n ± 3% 249.7n ± 4% -4.26% (p=0.003 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/something-4 265.4n ± 2% 256.1n ± 2% -3.49% (p=0.000 n=10) _RoutePatternMatch//api/:param/fixedEnd_|_match_|_/api/abc/fixedEnd-4 783.8n ± 2% 777.5n ± 3% ~ (p=0.218 n=10) _RoutePatternMatch//api/:param/fixedEnd_|_not_match_|_/api/abc/def/fixedEnd-4 797.8n ± 1% 773.6n ± 3% -3.03% (p=0.001 n=10) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity-4 920.3n ± 2% 926.0n ± 3% ~ (p=0.896 n=10) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/-4 920.4n ± 4% 908.2n ± 2% ~ (p=0.063 n=10) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/1-4 927.9n ± 2% 919.0n ± 3% ~ (p=0.579 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v-4 920.4n ± 3% 889.5n ± 3% -3.36% (p=0.007 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v2-4 916.9n ± 2% 891.9n ± 2% -2.73% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v1/-4 938.8n ± 5% 891.2n ± 2% -5.07% (p=0.000 n=10) geomean 591.7n 575.5n -2.73% │ route_pattern_match_old.txt │ route_pattern_match_new3.txt │ │ B/op │ B/op vs base │ _RoutePatternMatch//api/v1/const_|_match_|_/api/v1/const-4 168.0 ± 0% 152.0 ± 0% -9.52% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1-4 160.0 ± 0% 144.0 ± 0% -10.00% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/-4 160.0 ± 0% 144.0 ± 0% -10.00% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/something-4 176.0 ± 0% 160.0 ± 0% -9.09% (p=0.000 n=10) _RoutePatternMatch//api/:param/fixedEnd_|_match_|_/api/abc/fixedEnd-4 440.0 ± 0% 440.0 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/:param/fixedEnd_|_not_match_|_/api/abc/def/fixedEnd-4 464.0 ± 0% 440.0 ± 0% -5.17% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity-4 536.0 ± 0% 536.0 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/-4 536.0 ± 0% 536.0 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/1-4 536.0 ± 0% 536.0 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v-4 544.0 ± 0% 528.0 ± 0% -2.94% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v2-4 544.0 ± 0% 528.0 ± 0% -2.94% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v1/-4 544.0 ± 0% 528.0 ± 0% -2.94% (p=0.000 n=10) geomean 353.7 337.9 -4.47% ¹ all samples are equal │ route_pattern_match_old.txt │ route_pattern_match_new3.txt │ │ allocs/op │ allocs/op vs base │ _RoutePatternMatch//api/v1/const_|_match_|_/api/v1/const-4 6.000 ± 0% 5.000 ± 0% -16.67% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1-4 6.000 ± 0% 5.000 ± 0% -16.67% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/-4 6.000 ± 0% 5.000 ± 0% -16.67% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/something-4 6.000 ± 0% 5.000 ± 0% -16.67% (p=0.000 n=10) _RoutePatternMatch//api/:param/fixedEnd_|_match_|_/api/abc/fixedEnd-4 13.00 ± 0% 13.00 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/:param/fixedEnd_|_not_match_|_/api/abc/def/fixedEnd-4 14.00 ± 0% 13.00 ± 0% -7.14% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity-4 14.00 ± 0% 14.00 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/-4 14.00 ± 0% 14.00 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/1-4 14.00 ± 0% 14.00 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v-4 15.00 ± 0% 14.00 ± 0% -6.67% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v2-4 15.00 ± 0% 14.00 ± 0% -6.67% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v1/-4 15.00 ± 0% 14.00 ± 0% -6.67% (p=0.000 n=10) geomean 10.67 9.811 -8.08% ¹ all samples are equal ``` --------- Co-authored-by: RW --- path.go | 60 +++++++++++++++++++++++++++++++-------------------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/path.go b/path.go index 282073ec..e8b90eaf 100644 --- a/path.go +++ b/path.go @@ -152,11 +152,11 @@ func RoutePatternMatch(path, pattern string, cfg ...Config) bool { pattern = "/" + pattern } - patternPretty := pattern + patternPretty := []byte(pattern) // Case-sensitive routing, all to lowercase if !config.CaseSensitive { - patternPretty = utils.ToLower(patternPretty) + patternPretty = utils.ToLowerBytes(patternPretty) path = utils.ToLower(path) } // Strict routing, remove trailing slashes @@ -164,12 +164,12 @@ func RoutePatternMatch(path, pattern string, cfg ...Config) bool { patternPretty = utils.TrimRight(patternPretty, '/') } - parser := parseRoute(patternPretty) + parser := parseRoute(string(patternPretty)) - if patternPretty == "/" && path == "/" { + if string(patternPretty) == "/" && path == "/" { return true // '*' wildcard matches any path - } else if patternPretty == "/*" { + } else if string(patternPretty) == "/*" { return true } @@ -180,35 +180,28 @@ func RoutePatternMatch(path, pattern string, cfg ...Config) bool { } } // Check for a simple match - patternPretty = RemoveEscapeChar(patternPretty) - if len(patternPretty) == len(path) && patternPretty == path { - return true - } - // No match - return false + patternPretty = RemoveEscapeCharBytes(patternPretty) + + return string(patternPretty) == path } // parseRoute analyzes the route and divides it into segments for constant areas and parameters, // this information is needed later when assigning the requests to the declared routes func parseRoute(pattern string, customConstraints ...CustomConstraint) routeParser { parser := routeParser{} - part := "" + var n int + var seg *routeSegment for len(pattern) > 0 { nextParamPosition := findNextParamPosition(pattern) // handle the parameter part if nextParamPosition == 0 { - processedPart, seg := parser.analyseParameterPart(pattern, customConstraints...) - parser.params, parser.segs, part = append(parser.params, seg.ParamName), append(parser.segs, seg), processedPart + n, seg = parser.analyseParameterPart(pattern, customConstraints...) + parser.params, parser.segs = append(parser.params, seg.ParamName), append(parser.segs, seg) } else { - processedPart, seg := parser.analyseConstantPart(pattern, nextParamPosition) - parser.segs, part = append(parser.segs, seg), processedPart + n, seg = parser.analyseConstantPart(pattern, nextParamPosition) + parser.segs = append(parser.segs, seg) } - - // reduce the pattern by the processed parts - if len(part) == len(pattern) { - break - } - pattern = pattern[len(part):] + pattern = pattern[n:] } // mark last segment if len(parser.segs) > 0 { @@ -283,7 +276,7 @@ func findNextParamPosition(pattern string) int { } // analyseConstantPart find the end of the constant part and create the route segment -func (*routeParser) analyseConstantPart(pattern string, nextParamPosition int) (string, *routeSegment) { +func (*routeParser) analyseConstantPart(pattern string, nextParamPosition int) (int, *routeSegment) { // handle the constant part processedPart := pattern if nextParamPosition != -1 { @@ -291,14 +284,14 @@ func (*routeParser) analyseConstantPart(pattern string, nextParamPosition int) ( processedPart = pattern[:nextParamPosition] } constPart := RemoveEscapeChar(processedPart) - return processedPart, &routeSegment{ + return len(processedPart), &routeSegment{ Const: constPart, Length: len(constPart), } } // analyseParameterPart find the parameter end and create the route segment -func (routeParser *routeParser) analyseParameterPart(pattern string, customConstraints ...CustomConstraint) (string, *routeSegment) { +func (routeParser *routeParser) analyseParameterPart(pattern string, customConstraints ...CustomConstraint) (int, *routeSegment) { isWildCard := pattern[0] == wildcardParam isPlusParam := pattern[0] == plusParam @@ -329,6 +322,7 @@ func (routeParser *routeParser) analyseParameterPart(pattern string, customConst // cut params part processedPart := pattern[0 : parameterEndPosition+1] + n := parameterEndPosition + 1 paramName := RemoveEscapeChar(GetTrimmedParam(processedPart)) // Check has constraint @@ -402,7 +396,7 @@ func (routeParser *routeParser) analyseParameterPart(pattern string, customConst segment.Constraints = constraints } - return processedPart, segment + return n, segment } // isInCharset check is the given character in the charset list @@ -618,7 +612,7 @@ func GetTrimmedParam(param string) string { return param[start:end] } -// RemoveEscapeChar remove escape characters +// RemoveEscapeChar removes escape characters func RemoveEscapeChar(word string) string { b := []byte(word) dst := 0 @@ -632,6 +626,18 @@ func RemoveEscapeChar(word string) string { return string(b[:dst]) } +// RemoveEscapeCharBytes removes escape characters +func RemoveEscapeCharBytes(word []byte) []byte { + dst := 0 + for src := 0; src < len(word); src++ { + if word[src] != '\\' { + word[dst] = word[src] + dst++ + } + } + return word[:dst] +} + func getParamConstraintType(constraintPart string) TypeConstraint { switch constraintPart { case ConstraintInt: From 8e54c8f93809c4257b03548c16cffb8134fb5e3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Tue, 4 Mar 2025 12:46:05 +0800 Subject: [PATCH 22/53] =?UTF-8?q?=F0=9F=90=9B=20fix:=20Update=20binder=20i?= =?UTF-8?q?n=20form=5Ftest=20(#3336)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: error binder in form_test * fix: form tag * Fix: error package name --- binder/README.md | 2 +- binder/form_test.go | 10 +++++----- binder/mapping.go | 2 +- error.go | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/binder/README.md b/binder/README.md index 4d75fd1b..aded1efa 100644 --- a/binder/README.md +++ b/binder/README.md @@ -28,7 +28,7 @@ Fiber provides several default binders out of the box: ### Binding into a Struct -Fiber supports binding request data directly into a struct using [gorilla/schema](https://github.com/gorilla/schema). Here's an example: +Fiber supports binding request data directly into a struct using [gofiber/schema](https://github.com/gofiber/schema). Here's an example: ```go // Field names must start with an uppercase letter diff --git a/binder/form_test.go b/binder/form_test.go index d961f873..ac421d82 100644 --- a/binder/form_test.go +++ b/binder/form_test.go @@ -58,19 +58,19 @@ func Benchmark_FormBinder_Bind(b *testing.B) { b.ReportAllocs() b.ResetTimer() - binder := &QueryBinding{ + binder := &FormBinding{ EnableSplitting: true, } type User struct { - Name string `query:"name"` - Posts []string `query:"posts"` - Age int `query:"age"` + Name string `form:"name"` + Posts []string `form:"posts"` + Age int `form:"age"` } var user User req := fasthttp.AcquireRequest() - req.URI().SetQueryString("name=john&age=42&posts=post1,post2,post3") + req.SetBodyString("name=john&age=42&posts=post1,post2,post3") req.Header.SetContentType("application/x-www-form-urlencoded") b.ResetTimer() diff --git a/binder/mapping.go b/binder/mapping.go index bc95d028..41026a16 100644 --- a/binder/mapping.go +++ b/binder/mapping.go @@ -87,7 +87,7 @@ func parse(aliasTag string, out any, data map[string][]string, files ...map[stri return parseToStruct(aliasTag, out, data, files...) } -// Parse data into the struct with gorilla/schema +// Parse data into the struct with gofiber/schema func parseToStruct(aliasTag string, out any, data map[string][]string, files ...map[string][]*multipart.FileHeader) error { // Get decoder from pool schemaDecoder := decoderPoolMap[aliasTag].Get().(*schema.Decoder) //nolint:errcheck,forcetypeassert // not needed diff --git a/error.go b/error.go index f0375657..8d0ca635 100644 --- a/error.go +++ b/error.go @@ -40,7 +40,7 @@ var ( ErrNoHandlers = errors.New("format: at least one handler is required, but none were set") ) -// gorilla/schema errors +// gofiber/schema errors type ( // ConversionError Conversion error exposes the internal schema.ConversionError for public use. ConversionError = schema.ConversionError From 64c1771c2687f66dd231eb8939ecd0f3c13541a0 Mon Sep 17 00:00:00 2001 From: nickajacks1 <128185314+nickajacks1@users.noreply.github.com> Date: Tue, 4 Mar 2025 23:01:43 -0800 Subject: [PATCH 23/53] =?UTF-8?q?=F0=9F=94=A5=20Feature:=20Add=20Req=20and?= =?UTF-8?q?=20Res=20API=20(#2894)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔥 feat: add Req and Res interfaces Split the existing Ctx API into two separate APIs for Requests and Responses. There are two goals to this change: 1. Reduce cognitive load by making it more obvious whether a Ctx method interacts with the request or the response. 2. Increase API parity with Express. * fix(req,res): several issues * Sprinkle in calls to Req() and Res() to a few unit tests * Fix improper initialization caught by ^ * Add a few missing methods * docs: organize Ctx methods by request and response * feat(req,res): sync more missed methods --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> --- ctx.go | 14 + ctx_interface.go | 6 +- ctx_interface_gen.go | 6 + ctx_test.go | 68 +- docs/api/ctx.md | 2024 +++++++++++++++++++++--------------------- req.go | 159 ++++ req_interface_gen.go | 49 + res.go | 118 +++ res_interface_gen.go | 38 + 9 files changed, 1443 insertions(+), 1039 deletions(-) create mode 100644 req.go create mode 100644 req_interface_gen.go create mode 100644 res.go create mode 100644 res_interface_gen.go diff --git a/ctx.go b/ctx.go index aecfacdc..07ffb1db 100644 --- a/ctx.go +++ b/ctx.go @@ -54,6 +54,8 @@ type DefaultCtx struct { fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx bind *Bind // Default bind reference redirect *Redirect // Default redirect reference + req *DefaultReq // Default request api reference + res *DefaultRes // Default response api reference values [maxParams]string // Route parameter values viewBindMap sync.Map // Default view map to bind template engine method string // HTTP method @@ -1463,6 +1465,18 @@ func (c *DefaultCtx) renderExtensions(bind any) { } } +// Req returns a convenience type whose API is limited to operations +// on the incoming request. +func (c *DefaultCtx) Req() Req { + return c.req +} + +// Res returns a convenience type whose API is limited to operations +// on the outgoing response. +func (c *DefaultCtx) Res() Res { + return c.res +} + // Route returns the matched Route struct. func (c *DefaultCtx) Route() *Route { if c.route == nil { diff --git a/ctx_interface.go b/ctx_interface.go index ca438d82..32e8ee39 100644 --- a/ctx_interface.go +++ b/ctx_interface.go @@ -32,10 +32,14 @@ type CustomCtx interface { func NewDefaultCtx(app *App) *DefaultCtx { // return ctx - return &DefaultCtx{ + ctx := &DefaultCtx{ // Set app reference app: app, } + ctx.req = &DefaultReq{ctx: ctx} + ctx.res = &DefaultRes{ctx: ctx} + + return ctx } func (app *App) newCtx() Ctx { diff --git a/ctx_interface_gen.go b/ctx_interface_gen.go index 101068a2..fffe218d 100644 --- a/ctx_interface_gen.go +++ b/ctx_interface_gen.go @@ -265,6 +265,12 @@ type Ctx interface { // We support the following engines: https://github.com/gofiber/template Render(name string, bind any, layouts ...string) error renderExtensions(bind any) + // Req returns a convenience type whose API is limited to operations + // on the incoming request. + Req() Req + // Res returns a convenience type whose API is limited to operations + // on the outgoing response. + Res() Res // Route returns the matched Route struct. Route() *Route // SaveFile saves any multipart file to disk. diff --git a/ctx_test.go b/ctx_test.go index 082b0d44..ee272d24 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -46,7 +46,7 @@ func Test_Ctx_Accepts(t *testing.T) { c.Request().Header.Set(HeaderAccept, "text/html,application/xhtml+xml,application/xml;q=0.9") require.Equal(t, "", c.Accepts("")) - require.Equal(t, "", c.Accepts()) + require.Equal(t, "", c.Req().Accepts()) require.Equal(t, ".xml", c.Accepts(".xml")) require.Equal(t, "", c.Accepts(".john")) require.Equal(t, "application/xhtml+xml", c.Accepts("application/xml", "application/xml+rss", "application/yaml", "application/xhtml+xml"), "must use client-preferred mime type") @@ -57,13 +57,13 @@ func Test_Ctx_Accepts(t *testing.T) { c.Request().Header.Set(HeaderAccept, "text/*, application/json") require.Equal(t, "html", c.Accepts("html")) require.Equal(t, "text/html", c.Accepts("text/html")) - require.Equal(t, "json", c.Accepts("json", "text")) + require.Equal(t, "json", c.Req().Accepts("json", "text")) require.Equal(t, "application/json", c.Accepts("application/json")) require.Equal(t, "", c.Accepts("image/png")) require.Equal(t, "", c.Accepts("png")) c.Request().Header.Set(HeaderAccept, "text/html, application/json") - require.Equal(t, "text/*", c.Accepts("text/*")) + require.Equal(t, "text/*", c.Req().Accepts("text/*")) c.Request().Header.Set(HeaderAccept, "*/*") require.Equal(t, "html", c.Accepts("html")) @@ -968,46 +968,46 @@ func Test_Ctx_Cookie(t *testing.T) { Expires: expire, // SameSite: CookieSameSiteStrictMode, // default is "lax" } - c.Cookie(cookie) + c.Res().Cookie(cookie) expect := "username=john; expires=" + httpdate + "; path=/; SameSite=Lax" - require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + require.Equal(t, expect, c.Res().Get(HeaderSetCookie)) expect = "username=john; expires=" + httpdate + "; path=/" cookie.SameSite = CookieSameSiteDisabled - c.Cookie(cookie) - require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + c.Res().Cookie(cookie) + require.Equal(t, expect, c.Res().Get(HeaderSetCookie)) expect = "username=john; expires=" + httpdate + "; path=/; SameSite=Strict" cookie.SameSite = CookieSameSiteStrictMode - c.Cookie(cookie) - require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + c.Res().Cookie(cookie) + require.Equal(t, expect, c.Res().Get(HeaderSetCookie)) expect = "username=john; expires=" + httpdate + "; path=/; secure; SameSite=None" cookie.Secure = true cookie.SameSite = CookieSameSiteNoneMode - c.Cookie(cookie) - require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + c.Res().Cookie(cookie) + require.Equal(t, expect, c.Res().Get(HeaderSetCookie)) expect = "username=john; path=/; secure; SameSite=None" // should remove expires and max-age headers cookie.SessionOnly = true cookie.Expires = expire cookie.MaxAge = 10000 - c.Cookie(cookie) - require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + c.Res().Cookie(cookie) + require.Equal(t, expect, c.Res().Get(HeaderSetCookie)) expect = "username=john; path=/; secure; SameSite=None" // should remove expires and max-age headers when no expire and no MaxAge (default time) cookie.SessionOnly = false cookie.Expires = time.Time{} cookie.MaxAge = 0 - c.Cookie(cookie) - require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + c.Res().Cookie(cookie) + require.Equal(t, expect, c.Res().Get(HeaderSetCookie)) expect = "username=john; path=/; secure; SameSite=None; Partitioned" cookie.Partitioned = true - c.Cookie(cookie) - require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + c.Res().Cookie(cookie) + require.Equal(t, expect, c.Res().Get(HeaderSetCookie)) } // go test -v -run=^$ -bench=Benchmark_Ctx_Cookie -benchmem -count=4 @@ -1033,8 +1033,8 @@ func Test_Ctx_Cookies(t *testing.T) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().Header.Set("Cookie", "john=doe") - require.Equal(t, "doe", c.Cookies("john")) - require.Equal(t, "default", c.Cookies("unknown", "default")) + require.Equal(t, "doe", c.Req().Cookies("john")) + require.Equal(t, "default", c.Req().Cookies("unknown", "default")) } // go test -run Test_Ctx_Format @@ -1058,13 +1058,13 @@ func Test_Ctx_Format(t *testing.T) { } c.Request().Header.Set(HeaderAccept, `text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7`) - err := c.Format(formatHandlers("application/xhtml+xml", "application/xml", "foo/bar")...) + err := c.Res().Format(formatHandlers("application/xhtml+xml", "application/xml", "foo/bar")...) require.Equal(t, "application/xhtml+xml", accepted) require.Equal(t, "application/xhtml+xml", c.GetRespHeader(HeaderContentType)) require.NoError(t, err) require.NotEqual(t, StatusNotAcceptable, c.Response().StatusCode()) - err = c.Format(formatHandlers("foo/bar;a=b")...) + err = c.Res().Format(formatHandlers("foo/bar;a=b")...) require.Equal(t, "foo/bar;a=b", accepted) require.Equal(t, "foo/bar;a=b", c.GetRespHeader(HeaderContentType)) require.NoError(t, err) @@ -1165,7 +1165,7 @@ func Test_Ctx_AutoFormat(t *testing.T) { require.Equal(t, "Hello, World!", string(c.Response().Body())) c.Request().Header.Set(HeaderAccept, MIMETextHTML) - err = c.AutoFormat("Hello, World!") + err = c.Res().AutoFormat("Hello, World!") require.NoError(t, err) require.Equal(t, "

Hello, World!

", string(c.Response().Body())) @@ -1175,7 +1175,7 @@ func Test_Ctx_AutoFormat(t *testing.T) { require.Equal(t, `"Hello, World!"`, string(c.Response().Body())) c.Request().Header.Set(HeaderAccept, MIMETextPlain) - err = c.AutoFormat(complex(1, 1)) + err = c.Res().AutoFormat(complex(1, 1)) require.NoError(t, err) require.Equal(t, "(1+1i)", string(c.Response().Body())) @@ -2939,7 +2939,7 @@ func Test_Ctx_SaveFile(t *testing.T) { app := New() app.Post("/test", func(c Ctx) error { - fh, err := c.FormFile("file") + fh, err := c.Req().FormFile("file") require.NoError(t, err) tempFile, err := os.CreateTemp(os.TempDir(), "test-") @@ -3075,7 +3075,7 @@ func Test_Ctx_ClearCookie(t *testing.T) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().Header.Set(HeaderCookie, "john=doe") - c.ClearCookie("john") + c.Res().ClearCookie("john") require.True(t, strings.HasPrefix(string(c.Response().Header.Peek(HeaderSetCookie)), "john=; expires=")) c.Request().Header.Set(HeaderCookie, "test1=dummy") @@ -3104,7 +3104,7 @@ func Test_Ctx_Download(t *testing.T) { require.Equal(t, expect, c.Response().Body()) require.Equal(t, `attachment; filename="Awesome+File%21"`, string(c.Response().Header.Peek(HeaderContentDisposition))) - require.NoError(t, c.Download("ctx.go")) + require.NoError(t, c.Res().Download("ctx.go")) require.Equal(t, `attachment; filename="ctx.go"`, string(c.Response().Header.Peek(HeaderContentDisposition))) } @@ -3136,7 +3136,7 @@ func Test_Ctx_SendFile(t *testing.T) { // test with custom error code c = app.AcquireCtx(&fasthttp.RequestCtx{}) - err = c.Status(StatusInternalServerError).SendFile("ctx.go") + err = c.Res().Status(StatusInternalServerError).SendFile("ctx.go") // check expectation require.NoError(t, err) require.Equal(t, expectFileContent, c.Response().Body()) @@ -3161,7 +3161,7 @@ func Test_Ctx_SendFile_ContentType(t *testing.T) { // 1) simple case c := app.AcquireCtx(&fasthttp.RequestCtx{}) - err := c.SendFile("./.github/testdata/fs/img/fiber.png") + err := c.Res().SendFile("./.github/testdata/fs/img/fiber.png") // check expectation require.NoError(t, err) require.Equal(t, StatusOK, c.Response().StatusCode()) @@ -3782,7 +3782,7 @@ func Test_Ctx_JSONP(t *testing.T) { require.Equal(t, `callback({"Age":20,"Name":"Grame"});`, string(c.Response().Body())) require.Equal(t, "text/javascript; charset=utf-8", string(c.Response().Header.Peek("content-type"))) - err = c.JSONP(Map{ + err = c.Res().JSONP(Map{ "Name": "Grame", "Age": 20, }, "john") @@ -4006,7 +4006,7 @@ func Test_Ctx_Render(t *testing.T) { err = c.Render("./.github/testdata/template-non-exists.html", nil) require.Error(t, err) - err = c.Render("./.github/testdata/template-invalid.html", nil) + err = c.Res().Render("./.github/testdata/template-invalid.html", nil) require.Error(t, err) } @@ -4907,7 +4907,7 @@ func Test_Ctx_Queries(t *testing.T) { c.Request().URI().SetQueryString("tags=apple,orange,banana&filters[tags]=apple,orange,banana&filters[category][name]=fruits&filters.tags=apple,orange,banana&filters.category.name=fruits") - queries = c.Queries() + queries = c.Req().Queries() require.Equal(t, "apple,orange,banana", queries["tags"]) require.Equal(t, "apple,orange,banana", queries["filters[tags]"]) require.Equal(t, "fruits", queries["filters[category][name]"]) @@ -5055,7 +5055,7 @@ func Test_Ctx_IsFromLocal_X_Forwarded(t *testing.T) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().Header.Set(HeaderXForwardedFor, "93.46.8.90") - require.False(t, c.IsFromLocal()) + require.False(t, c.Req().IsFromLocal()) } } @@ -5088,8 +5088,8 @@ func Test_Ctx_IsFromLocal_RemoteAddr(t *testing.T) { fastCtx := &fasthttp.RequestCtx{} fastCtx.SetRemoteAddr(localIPv6) c := app.AcquireCtx(fastCtx) - require.Equal(t, "::1", c.IP()) - require.True(t, c.IsFromLocal()) + require.Equal(t, "::1", c.Req().IP()) + require.True(t, c.Req().IsFromLocal()) } // Test for the case fasthttp remoteAddr is set to "0:0:0:0:0:0:0:1". { diff --git a/docs/api/ctx.md b/docs/api/ctx.md index fda9f373..312f997a 100644 --- a/docs/api/ctx.md +++ b/docs/api/ctx.md @@ -8,7 +8,432 @@ description: >- sidebar_position: 3 --- -## Accepts +### App + +Returns the [\*App](app.md) reference so you can easily access all application settings. + +```go title="Signature" +func (c fiber.Ctx) App() *App +``` + +```go title="Example" +app.Get("/stack", func(c fiber.Ctx) error { + return c.JSON(c.App().Stack()) +}) +``` + +### Bind + +Bind is a method that supports bindings for the request/response body, query parameters, URL parameters, cookies, and much more. +It returns a pointer to the [Bind](./bind.md) struct which contains all the methods to bind the request/response data. + +For detailed information, check the [Bind](./bind.md) documentation. + +```go title="Signature" +func (c fiber.Ctx) Bind() *Bind +``` + +```go title="Example" +app.Post("/", func(c fiber.Ctx) error { + user := new(User) + // Bind the request body to a struct: + return c.Bind().Body(user) +}) +``` + +### Context + +`Context` returns a context implementation that was set by the user earlier or returns a non-nil, empty context if it was not set earlier. + +```go title="Signature" +func (c fiber.Ctx) Context() context.Context +``` + +```go title="Example" +app.Get("/", func(c fiber.Ctx) error { + ctx := c.Context() + // ctx is context implementation set by user + + // ... +}) +``` + +### Drop + +Terminates the client connection silently without sending any HTTP headers or response body. + +This can be used for scenarios where you want to block certain requests without notifying the client, such as mitigating +DDoS attacks or protecting sensitive endpoints from unauthorized access. + +```go title="Signature" +func (c fiber.Ctx) Drop() error +``` + +```go title="Example" +app.Get("/", func(c fiber.Ctx) error { + if c.IP() == "192.168.1.1" { + return c.Drop() + } + + return c.SendString("Hello World!") +}) +``` + +### GetReqHeaders + +Returns the HTTP request headers as a map. Since a header can be set multiple times in a single request, the values of the map are slices of strings containing all the different values of the header. + +```go title="Signature" +func (c fiber.Ctx) GetReqHeaders() map[string][]string +``` + +:::info +Returned value is only valid within the handler. Do not store any references. +Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more...](../#zero-allocation) +::: + +### GetRespHeader + +Returns the HTTP response header specified by the field. + +:::tip +The match is **case-insensitive**. +::: + +```go title="Signature" +func (c fiber.Ctx) GetRespHeader(key string, defaultValue ...string) string +``` + +```go title="Example" +app.Get("/", func(c fiber.Ctx) error { + c.GetRespHeader("X-Request-Id") // "8d7ad5e3-aaf3-450b-a241-2beb887efd54" + c.GetRespHeader("Content-Type") // "text/plain" + c.GetRespHeader("something", "john") // "john" + // .. +}) +``` + +:::info +Returned value is only valid within the handler. Do not store any references. +Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more...](../#zero-allocation) +::: + +### GetRespHeaders + +Returns the HTTP response headers as a map. Since a header can be set multiple times in a single request, the values of the map are slices of strings containing all the different values of the header. + +```go title="Signature" +func (c fiber.Ctx) GetRespHeaders() map[string][]string +``` + +:::info +Returned value is only valid within the handler. Do not store any references. +Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more...](../#zero-allocation) +::: + +### GetRouteURL + +Generates URLs to named routes, with parameters. URLs are relative, for example: "/user/1831" + +```go title="Signature" +func (c fiber.Ctx) GetRouteURL(routeName string, params Map) (string, error) +``` + +```go title="Example" +app.Get("/", func(c fiber.Ctx) error { + return c.SendString("Home page") +}).Name("home") + +app.Get("/user/:id", func(c fiber.Ctx) error { + return c.SendString(c.Params("id")) +}).Name("user.show") + +app.Get("/test", func(c fiber.Ctx) error { + location, _ := c.GetRouteURL("user.show", fiber.Map{"id": 1}) + return c.SendString(location) +}) + +// /test returns "/user/1" +``` + +### Locals + +A method that stores variables scoped to the request and, therefore, are available only to the routes that match the request. The stored variables are removed after the request is handled. If any of the stored data implements the `io.Closer` interface, its `Close` method will be called before it's removed. + +:::tip +This is useful if you want to pass some **specific** data to the next middleware. Remember to perform type assertions when retrieving the data to ensure it is of the expected type. You can also use a non-exported type as a key to avoid collisions. +::: + +```go title="Signature" +func (c fiber.Ctx) Locals(key any, value ...any) any +``` + +```go title="Example" + +// keyType is an unexported type for keys defined in this package. +// This prevents collisions with keys defined in other packages. +type keyType int + +// userKey is the key for user.User values in Contexts. It is +// unexported; clients use user.NewContext and user.FromContext +// instead of using this key directly. +var userKey keyType + +app.Use(func(c fiber.Ctx) error { + c.Locals(userKey, "admin") // Stores the string "admin" under a non-exported type key + return c.Next() +}) + +app.Get("/admin", func(c fiber.Ctx) error { + user, ok := c.Locals(userKey).(string) // Retrieves the data stored under the key and performs a type assertion + if ok && user == "admin" { + return c.Status(fiber.StatusOK).SendString("Welcome, admin!") + } + return c.SendStatus(fiber.StatusForbidden) +}) +``` + +An alternative version of the `Locals` method that takes advantage of Go's generics feature is also available. This version allows for the manipulation and retrieval of local values within a request's context with a more specific data type. + +```go title="Signature" +func Locals[V any](c fiber.Ctx, key any, value ...V) V +``` + +```go title="Example" +app.Use(func(c fiber.Ctx) error { + fiber.Locals[string](c, "john", "doe") + fiber.Locals[int](c, "age", 18) + fiber.Locals[bool](c, "isHuman", true) + return c.Next() +}) + +app.Get("/test", func(c fiber.Ctx) error { + fiber.Locals[string](c, "john") // "doe" + fiber.Locals[int](c, "age") // 18 + fiber.Locals[bool](c, "isHuman") // true + return nil +}) +```` + +Make sure to understand and correctly implement the `Locals` method in both its standard and generic form for better control over route-specific data within your application. + +### Next + +When **Next** is called, it executes the next method in the stack that matches the current route. You can pass an error struct within the method that will end the chaining and call the [error handler](https://docs.gofiber.io/guide/error-handling). + +```go title="Signature" +func (c fiber.Ctx) Next() error +``` + +```go title="Example" +app.Get("/", func(c fiber.Ctx) error { + fmt.Println("1st route!") + return c.Next() +}) + +app.Get("*", func(c fiber.Ctx) error { + fmt.Println("2nd route!") + return c.Next() +}) + +app.Get("/", func(c fiber.Ctx) error { + fmt.Println("3rd route!") + return c.SendString("Hello, World!") +}) +``` + +### Redirect + +Returns the Redirect reference. + +For detailed information, check the [Redirect](./redirect.md) documentation. + +```go title="Signature" +func (c fiber.Ctx) Redirect() *Redirect +``` + +```go title="Example" +app.Get("/coffee", func(c fiber.Ctx) error { + return c.Redirect().To("/teapot") +}) + +app.Get("/teapot", func(c fiber.Ctx) error { + return c.Status(fiber.StatusTeapot).Send("🍵 short and stout 🍵") +}) +``` + +### Request + +Returns the [*fasthttp.Request](https://pkg.go.dev/github.com/valyala/fasthttp#Request) pointer. + +```go title="Signature" +func (c fiber.Ctx) Request() *fasthttp.Request +``` + +```go title="Example" +app.Get("/", func(c fiber.Ctx) error { + c.Request().Header.Method() + // => []byte("GET") +}) +``` + +### RequestCtx + +Returns [\*fasthttp.RequestCtx](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx) that is compatible with the `context.Context` interface that requires a deadline, a cancellation signal, and other values across API boundaries. + +```go title="Signature" +func (c fiber.Ctx) RequestCtx() *fasthttp.RequestCtx +``` + +:::info +Please read the [Fasthttp Documentation](https://pkg.go.dev/github.com/valyala/fasthttp?tab=doc) for more information. +::: + +### Response + +Returns the [\*fasthttp.Response](https://pkg.go.dev/github.com/valyala/fasthttp#Response) pointer. + +```go title="Signature" +func (c fiber.Ctx) Response() *fasthttp.Response +``` + +```go title="Example" +app.Get("/", func(c fiber.Ctx) error { + c.Response().BodyWriter().Write([]byte("Hello, World!")) + // => "Hello, World!" + return nil +}) +``` + +### Reset + +Resets the context fields by the given request when using server handlers. + +```go title="Signature" +func (c fiber.Ctx) Reset(fctx *fasthttp.RequestCtx) +``` + +It is used outside of the Fiber Handlers to reset the context for the next request. + +### RestartRouting + +Instead of executing the next method when calling [Next](ctx.md#next), **RestartRouting** restarts execution from the first method that matches the current route. This may be helpful after overriding the path, i.e., an internal redirect. Note that handlers might be executed again, which could result in an infinite loop. + +```go title="Signature" +func (c fiber.Ctx) RestartRouting() error +``` + +```go title="Example" +app.Get("/new", func(c fiber.Ctx) error { + return c.SendString("From /new") +}) + +app.Get("/old", func(c fiber.Ctx) error { + c.Path("/new") + return c.RestartRouting() +}) +``` + +### Route + +Returns the matched [Route](https://pkg.go.dev/github.com/gofiber/fiber?tab=doc#Route) struct. + +```go title="Signature" +func (c fiber.Ctx) Route() *Route +``` + +```go title="Example" +// http://localhost:8080/hello + +app.Get("/hello/:name", func(c fiber.Ctx) error { + r := c.Route() + fmt.Println(r.Method, r.Path, r.Params, r.Handlers) + // GET /hello/:name handler [name] + + // ... +}) +``` + +:::caution +Do not rely on `c.Route()` in middlewares **before** calling `c.Next()` - `c.Route()` returns the **last executed route**. +::: + +```go title="Example" +func MyMiddleware() fiber.Handler { + return func(c fiber.Ctx) error { + beforeNext := c.Route().Path // Will be '/' + err := c.Next() + afterNext := c.Route().Path // Will be '/hello/:name' + return err + } +} +``` + +### SetContext + +Sets the user-specified implementation for the `context.Context` interface. + +```go title="Signature" +func (c fiber.Ctx) SetContext(ctx context.Context) +``` + +```go title="Example" +app.Get("/", func(c fiber.Ctx) error { + ctx := context.Background() + c.SetContext(ctx) + // Here ctx could be any context implementation + + // ... +}) +``` + +### String + +Returns a unique string representation of the context. + +```go title="Signature" +func (c fiber.Ctx) String() string +``` + +```go title="Example" +app.Get("/", func(c fiber.Ctx) error { + c.String() // => "#0000000100000001 - 127.0.0.1:3000 <-> 127.0.0.1:61516 - GET http://localhost:3000/" + + // ... +}) +``` + +### ViewBind + +Adds variables to the default view variable map binding to the template engine. +Variables are read by the `Render` method and may be overwritten. + +```go title="Signature" +func (c fiber.Ctx) ViewBind(vars Map) error +``` + +```go title="Example" +app.Use(func(c fiber.Ctx) error { + c.ViewBind(fiber.Map{ + "Title": "Hello, World!", + }) + return c.Next() +}) + +app.Get("/", func(c fiber.Ctx) error { + return c.Render("xxx.tmpl", fiber.Map{}) // Render will use the Title variable +}) +``` + +## Request + +Methods which operate on the incoming request. + +:::tip +Use `c.Req()` to limit gopls suggestions to only these methods! +::: + +### Accepts Checks if the specified **extensions** or **content** **types** are acceptable. @@ -99,106 +524,7 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` -## App - -Returns the [\*App](app.md) reference so you can easily access all application settings. - -```go title="Signature" -func (c fiber.Ctx) App() *App -``` - -```go title="Example" -app.Get("/stack", func(c fiber.Ctx) error { - return c.JSON(c.App().Stack()) -}) -``` - -## Append - -Appends the specified **value** to the HTTP response header field. - -:::caution -If the header is **not** already set, it creates the header with the specified value. -::: - -```go title="Signature" -func (c fiber.Ctx) Append(field string, values ...string) -``` - -```go title="Example" -app.Get("/", func(c fiber.Ctx) error { - c.Append("Link", "http://google.com", "http://localhost") - // => Link: http://google.com, http://localhost - - c.Append("Link", "Test") - // => Link: http://google.com, http://localhost, Test - - // ... -}) -``` - -## Attachment - -Sets the HTTP response [Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header field to `attachment`. - -```go title="Signature" -func (c fiber.Ctx) Attachment(filename ...string) -``` - -```go title="Example" -app.Get("/", func(c fiber.Ctx) error { - c.Attachment() - // => Content-Disposition: attachment - - c.Attachment("./upload/images/logo.png") - // => Content-Disposition: attachment; filename="logo.png" - // => Content-Type: image/png - - // ... -}) -``` - -## AutoFormat - -Performs content-negotiation on the [Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) HTTP header. It uses [Accepts](ctx.md#accepts) to select a proper format. -The supported content types are `text/html`, `text/plain`, `application/json`, and `application/xml`. -For more flexible content negotiation, use [Format](ctx.md#format). - -:::info -If the header is **not** specified or there is **no** proper format, **text/plain** is used. -::: - -```go title="Signature" -func (c fiber.Ctx) AutoFormat(body any) error -``` - -```go title="Example" -app.Get("/", func(c fiber.Ctx) error { - // Accept: text/plain - c.AutoFormat("Hello, World!") - // => Hello, World! - - // Accept: text/html - c.AutoFormat("Hello, World!") - // =>

Hello, World!

- - type User struct { - Name string - } - user := User{"John Doe"} - - // Accept: application/json - c.AutoFormat(user) - // => {"Name":"John Doe"} - - // Accept: application/xml - c.AutoFormat(user) - // => John Doe - // .. -}) -``` - -## BaseURL +### BaseURL Returns the base URL (**protocol** + **host**) as a `string`. @@ -215,26 +541,7 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` -## Bind - -Bind is a method that supports bindings for the request/response body, query parameters, URL parameters, cookies, and much more. -It returns a pointer to the [Bind](./bind.md) struct which contains all the methods to bind the request/response data. - -For detailed information, check the [Bind](./bind.md) documentation. - -```go title="Signature" -func (c fiber.Ctx) Bind() *Bind -``` - -```go title="Example" -app.Post("/", func(c fiber.Ctx) error { - user := new(User) - // Bind the request body to a struct: - return c.Bind().Body(user) -}) -``` - -## Body +### Body As per the header `Content-Encoding`, this method will try to perform a file decompression from the **body** bytes. In case no `Content-Encoding` header is sent, it will perform as [BodyRaw](#bodyraw). @@ -256,7 +563,7 @@ Returned value is only valid within the handler. Do not store any references. Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more...](../#zero-allocation) ::: -## BodyRaw +### BodyRaw Returns the raw request **body**. @@ -278,59 +585,7 @@ Returned value is only valid within the handler. Do not store any references. Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more...](../#zero-allocation) ::: -## ClearCookie - -Expires a client cookie (or all cookies if left empty). - -```go title="Signature" -func (c fiber.Ctx) ClearCookie(key ...string) -``` - -```go title="Example" -app.Get("/", func(c fiber.Ctx) error { - // Clears all cookies: - c.ClearCookie() - - // Expire specific cookie by name: - c.ClearCookie("user") - - // Expire multiple cookies by names: - c.ClearCookie("token", "session", "track_id", "version") - // ... -}) -``` - -:::caution -Web browsers and other compliant clients will only clear the cookie if the given options are identical to those when creating the cookie, excluding `Expires` and `MaxAge`. `ClearCookie` will not set these values for you - a technique similar to the one shown below should be used to ensure your cookie is deleted. -::: - -```go title="Example" -app.Get("/set", func(c fiber.Ctx) error { - c.Cookie(&fiber.Cookie{ - Name: "token", - Value: "randomvalue", - Expires: time.Now().Add(24 * time.Hour), - HTTPOnly: true, - SameSite: "lax", - }) - - // ... -}) - -app.Get("/delete", func(c fiber.Ctx) error { - c.Cookie(&fiber.Cookie{ - Name: "token", - // Set expiry date to the past - Expires: time.Now().Add(-(time.Hour * 2)), - HTTPOnly: true, - SameSite: "lax", - }) - - // ... -}) -``` - -## ClientHelloInfo +### ClientHelloInfo `ClientHelloInfo` contains information from a ClientHello message in order to guide application logic in the `GetCertificate` and `GetConfigForClient` callbacks. You can refer to the [ClientHelloInfo](https://golang.org/pkg/crypto/tls/#ClientHelloInfo) struct documentation for more information on the returned struct. @@ -347,80 +602,7 @@ app.Get("/hello", func(c fiber.Ctx) error { }) ``` -## Context - -`Context` returns a context implementation that was set by the user earlier or returns a non-nil, empty context if it was not set earlier. - -```go title="Signature" -func (c fiber.Ctx) Context() context.Context -``` - -```go title="Example" -app.Get("/", func(c fiber.Ctx) error { - ctx := c.Context() - // ctx is context implementation set by user - - // ... -}) -``` - -## Cookie - -Sets a cookie. - -```go title="Signature" -func (c fiber.Ctx) Cookie(cookie *Cookie) -``` - -```go -type Cookie struct { - Name string `json:"name"` // The name of the cookie - Value string `json:"value"` // The value of the cookie - Path string `json:"path"` // Specifies a URL path which is allowed to receive the cookie - Domain string `json:"domain"` // Specifies the domain which is allowed to receive the cookie - MaxAge int `json:"max_age"` // The maximum age (in seconds) of the cookie - Expires time.Time `json:"expires"` // The expiration date of the cookie - Secure bool `json:"secure"` // Indicates that the cookie should only be transmitted over a secure HTTPS connection - HTTPOnly bool `json:"http_only"` // Indicates that the cookie is accessible only through the HTTP protocol - SameSite string `json:"same_site"` // Controls whether or not a cookie is sent with cross-site requests - Partitioned bool `json:"partitioned"` // Indicates if the cookie is stored in a partitioned cookie jar - SessionOnly bool `json:"session_only"` // Indicates if the cookie is a session-only cookie -} -``` - -```go title="Example" -app.Get("/", func(c fiber.Ctx) error { - // Create cookie - cookie := new(fiber.Cookie) - cookie.Name = "john" - cookie.Value = "doe" - cookie.Expires = time.Now().Add(24 * time.Hour) - - // Set cookie - c.Cookie(cookie) - // ... -}) -``` - -:::info -Partitioned cookies allow partitioning the cookie jar by top-level site, enhancing user privacy by preventing cookies from being shared across different sites. This feature is particularly useful in scenarios where a user interacts with embedded third-party services that should not have access to the main site's cookies. You can check out [CHIPS](https://developers.google.com/privacy-sandbox/3pcd/chips) for more information. -::: - -```go title="Example" -app.Get("/", func(c fiber.Ctx) error { - // Create a new partitioned cookie - cookie := new(fiber.Cookie) - cookie.Name = "user_session" - cookie.Value = "abc123" - cookie.Partitioned = true // This cookie will be stored in a separate jar when it's embedded into another website - - // Set the cookie in the response - c.Cookie(cookie) - return c.SendString("Partitioned cookie set") -}) -``` - -## Cookies +### Cookies Gets a cookie value by key. You can pass an optional default value that will be returned if the cookie key does not exist. @@ -442,150 +624,7 @@ Returned value is only valid within the handler. Do not store any references. Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more...](../#zero-allocation) ::: -## Download - -Transfers the file from the given path as an `attachment`. - -Typically, browsers will prompt the user to download. By default, the [Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header `filename=` parameter is the file path (_this typically appears in the browser dialog_). -Override this default with the **filename** parameter. - -```go title="Signature" -func (c fiber.Ctx) Download(file string, filename ...string) error -``` - -```go title="Example" -app.Get("/", func(c fiber.Ctx) error { - return c.Download("./files/report-12345.pdf") - // => Download report-12345.pdf - - return c.Download("./files/report-12345.pdf", "report.pdf") - // => Download report.pdf -}) -``` - -## Drop - -Terminates the client connection silently without sending any HTTP headers or response body. - -This can be used for scenarios where you want to block certain requests without notifying the client, such as mitigating -DDoS attacks or protecting sensitive endpoints from unauthorized access. - -```go title="Signature" -func (c fiber.Ctx) Drop() error -``` - -```go title="Example" -app.Get("/", func(c fiber.Ctx) error { - if c.IP() == "192.168.1.1" { - return c.Drop() - } - - return c.SendString("Hello World!") -}) -``` - -## End - -End immediately flushes the current response and closes the underlying connection. - -```go title="Signature" -func (c fiber.Ctx) End() error -``` - -```go title="Example" -app.Get("/", func(c fiber.Ctx) error { - c.SendString("Hello World!") - return c.End() -}) -``` - -:::caution -Calling `c.End()` will disallow further writes to the underlying connection. -::: - -End can be used to stop a middleware from modifying a response of a handler/other middleware down the method chain -when they regain control after calling `c.Next()`. - -```go title="Example" -// Error Logging/Responding middleware -app.Use(func(c fiber.Ctx) error { - err := c.Next() - - // Log errors & write the error to the response - if err != nil { - log.Printf("Got error in middleware: %v", err) - return c.Writef("(got error %v)", err) - } - - // No errors occured - return nil -}) - -// Handler with simulated error -app.Get("/", func(c fiber.Ctx) error { - // Closes the connection instantly after writing from this handler - // and disallow further modification of its response - defer c.End() - - c.SendString("Hello, ... I forgot what comes next!") - return errors.New("some error") -}) -``` - -## Format - -Performs content-negotiation on the [Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) HTTP header. It uses [Accepts](ctx.md#accepts) to select a proper format from the supplied offers. A default handler can be provided by setting the `MediaType` to `"default"`. If no offers match and no default is provided, a 406 (Not Acceptable) response is sent. The Content-Type is automatically set when a handler is selected. - -:::info -If the Accept header is **not** specified, the first handler will be used. -::: - -```go title="Signature" -func (c fiber.Ctx) Format(handlers ...ResFmt) error -``` - -```go title="Example" -// Accept: application/json => {"command":"eat","subject":"fruit"} -// Accept: text/plain => Eat Fruit! -// Accept: application/xml => Not Acceptable -app.Get("/no-default", func(c fiber.Ctx) error { - return c.Format( - fiber.ResFmt{"application/json", func(c fiber.Ctx) error { - return c.JSON(fiber.Map{ - "command": "eat", - "subject": "fruit", - }) - }}, - fiber.ResFmt{"text/plain", func(c fiber.Ctx) error { - return c.SendString("Eat Fruit!") - }}, - ) -}) - -// Accept: application/json => {"command":"eat","subject":"fruit"} -// Accept: text/plain => Eat Fruit! -// Accept: application/xml => Eat Fruit! -app.Get("/default", func(c fiber.Ctx) error { - textHandler := func(c fiber.Ctx) error { - return c.SendString("Eat Fruit!") - } - - handlers := []fiber.ResFmt{ - {"application/json", func(c fiber.Ctx) error { - return c.JSON(fiber.Map{ - "command": "eat", - "subject": "fruit", - }) - }}, - {"text/plain", textHandler}, - {"default", textHandler}, - } - - return c.Format(handlers...) -}) -``` - -## FormFile +### FormFile MultipartForm files can be retrieved by name, the **first** file from the given key is returned. @@ -603,7 +642,7 @@ app.Post("/", func(c fiber.Ctx) error { }) ``` -## FormValue +### FormValue Form values can be retrieved by name, the **first** value for the given key is returned. @@ -628,7 +667,7 @@ Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more.. ::: -## Fresh +### Fresh When the response is still **fresh** in the client's cache **true** is returned, otherwise **false** is returned to indicate that the client cache is now stale and the full response should be sent. @@ -640,7 +679,7 @@ Read more on [https://expressjs.com/en/4x/api.html\#req.fresh](https://expressjs func (c fiber.Ctx) Fresh() bool ``` -## Get +### Get Returns the HTTP request header specified by the field. @@ -666,84 +705,7 @@ Returned value is only valid within the handler. Do not store any references. Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more...](../#zero-allocation) ::: -## GetReqHeaders - -Returns the HTTP request headers as a map. Since a header can be set multiple times in a single request, the values of the map are slices of strings containing all the different values of the header. - -```go title="Signature" -func (c fiber.Ctx) GetReqHeaders() map[string][]string -``` - -:::info -Returned value is only valid within the handler. Do not store any references. -Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more...](../#zero-allocation) -::: - -## GetRespHeader - -Returns the HTTP response header specified by the field. - -:::tip -The match is **case-insensitive**. -::: - -```go title="Signature" -func (c fiber.Ctx) GetRespHeader(key string, defaultValue ...string) string -``` - -```go title="Example" -app.Get("/", func(c fiber.Ctx) error { - c.GetRespHeader("X-Request-Id") // "8d7ad5e3-aaf3-450b-a241-2beb887efd54" - c.GetRespHeader("Content-Type") // "text/plain" - c.GetRespHeader("something", "john") // "john" - // .. -}) -``` - -:::info -Returned value is only valid within the handler. Do not store any references. -Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more...](../#zero-allocation) -::: - -## GetRespHeaders - -Returns the HTTP response headers as a map. Since a header can be set multiple times in a single request, the values of the map are slices of strings containing all the different values of the header. - -```go title="Signature" -func (c fiber.Ctx) GetRespHeaders() map[string][]string -``` - -:::info -Returned value is only valid within the handler. Do not store any references. -Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more...](../#zero-allocation) -::: - -## GetRouteURL - -Generates URLs to named routes, with parameters. URLs are relative, for example: "/user/1831" - -```go title="Signature" -func (c fiber.Ctx) GetRouteURL(routeName string, params Map) (string, error) -``` - -```go title="Example" -app.Get("/", func(c fiber.Ctx) error { - return c.SendString("Home page") -}).Name("home") - -app.Get("/user/:id", func(c fiber.Ctx) error { - return c.SendString(c.Params("id")) -}).Name("user.show") - -app.Get("/test", func(c fiber.Ctx) error { - location, _ := c.GetRouteURL("user.show", fiber.Map{"id": 1}) - return c.SendString(location) -}) - -// /test returns "/user/1" -``` - -## Host +### Host Returns the host derived from the [Host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) HTTP header. @@ -769,7 +731,7 @@ Returned value is only valid within the handler. Do not store any references. Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more...](../#zero-allocation) ::: -## Hostname +### Hostname Returns the hostname derived from the [Host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) HTTP header. @@ -792,7 +754,7 @@ Returned value is only valid within the handler. Do not store any references. Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more...](../#zero-allocation) ::: -## IP +### IP Returns the remote IP address of the request. @@ -816,7 +778,7 @@ app := fiber.New(fiber.Config{ }) ``` -## IPs +### IPs Returns an array of IP addresses specified in the [X-Forwarded-For](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) request header. @@ -838,7 +800,7 @@ app.Get("/", func(c fiber.Ctx) error { Improper use of the X-Forwarded-For header can be a security risk. For details, see the [Security and privacy concerns](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#security_and_privacy_concerns) section. ::: -## Is +### Is Returns the matching **content type**, if the incoming request’s [Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) HTTP header field matches the [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) specified by the type parameter. @@ -862,7 +824,7 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` -## IsFromLocal +### IsFromLocal Returns `true` if the request came from localhost. @@ -879,7 +841,7 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` -## IsProxyTrusted +### IsProxyTrusted Checks the trustworthiness of the remote IP. If [`TrustProxy`](fiber.md#trustproxy) is `false`, it returns `true`. @@ -908,240 +870,7 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` -## JSON - -Converts any **interface** or **string** to JSON using the [encoding/json](https://pkg.go.dev/encoding/json) package. - -:::info -JSON also sets the content header to the `ctype` parameter. If no `ctype` is passed in, the header is set to `application/json`. -::: - -```go title="Signature" -func (c fiber.Ctx) JSON(data any, ctype ...string) error -``` - -```go title="Example" -type SomeStruct struct { - Name string - Age uint8 -} - -app.Get("/json", func(c fiber.Ctx) error { - // Create data struct: - data := SomeStruct{ - Name: "Grame", - Age: 20, - } - - return c.JSON(data) - // => Content-Type: application/json - // => {"Name": "Grame", "Age": 20} - - return c.JSON(fiber.Map{ - "name": "Grame", - "age": 20, - }) - // => Content-Type: application/json - // => {"name": "Grame", "age": 20} - - return c.JSON(fiber.Map{ - "type": "https://example.com/probs/out-of-credit", - "title": "You do not have enough credit.", - "status": 403, - "detail": "Your current balance is 30, but that costs 50.", - "instance": "/account/12345/msgs/abc", - }, "application/problem+json") - // => Content-Type: application/problem+json - // => "{ - // => "type": "https://example.com/probs/out-of-credit", - // => "title": "You do not have enough credit.", - // => "status": 403, - // => "detail": "Your current balance is 30, but that costs 50.", - // => "instance": "/account/12345/msgs/abc", - // => }" -}) -``` - -## JSONP - -Sends a JSON response with JSONP support. This method is identical to [JSON](ctx.md#json), except that it opts-in to JSONP callback support. By default, the callback name is simply `callback`. - -Override this by passing a **named string** in the method. - -```go title="Signature" -func (c fiber.Ctx) JSONP(data any, callback ...string) error -``` - -```go title="Example" -type SomeStruct struct { - Name string - Age uint8 -} - -app.Get("/", func(c fiber.Ctx) error { - // Create data struct: - data := SomeStruct{ - Name: "Grame", - Age: 20, - } - - return c.JSONP(data) - // => callback({"Name": "Grame", "Age": 20}) - - return c.JSONP(data, "customFunc") - // => customFunc({"Name": "Grame", "Age": 20}) -}) -``` - -## CBOR - -CBOR converts any interface or string to CBOR encoded bytes. - -:::info -CBOR also sets the content header to the `ctype` parameter. If no `ctype` is passed in, the header is set to `application/cbor`. -::: - -```go title="Signature" -func (c fiber.Ctx) CBOR(data any, ctype ...string) error -``` - -```go title="Example" -type SomeStruct struct { - Name string `cbor:"name"` - Age uint8 `cbor:"age"` -} - -app.Get("/cbor", func(c fiber.Ctx) error { - // Create data struct: - data := SomeStruct{ - Name: "Grame", - Age: 20, - } - - return c.CBOR(data) - // => Content-Type: application/cbor - // => \xa2dnameeGramecage\x14 - - return c.CBOR(fiber.Map{ - "name": "Grame", - "age": 20, - }) - // => Content-Type: application/cbor - // => \xa2dnameeGramecage\x14 - - return c.CBOR(fiber.Map{ - "type": "https://example.com/probs/out-of-credit", - "title": "You do not have enough credit.", - "status": 403, - "detail": "Your current balance is 30, but that costs 50.", - "instance": "/account/12345/msgs/abc", - }) - // => Content-Type: application/cbor - // => \xa5dtypex'https://example.com/probs/out-of-creditetitlex\x1eYou do not have enough credit.fstatus\x19\x01\x93fdetailx.Your current balance is 30, but that costs 50.hinstancew/account/12345/msgs/abc -}) -``` - -## Links - -Joins the links followed by the property to populate the response’s [Link](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link) HTTP header field. - -```go title="Signature" -func (c fiber.Ctx) Links(link ...string) -``` - -```go title="Example" -app.Get("/", func(c fiber.Ctx) error { - c.Links( - "http://api.example.com/users?page=2", "next", - "http://api.example.com/users?page=5", "last", - ) - // Link: ; rel="next", - // ; rel="last" - - // ... -}) -``` - -## Locals - -A method that stores variables scoped to the request and, therefore, are available only to the routes that match the request. The stored variables are removed after the request is handled. If any of the stored data implements the `io.Closer` interface, its `Close` method will be called before it's removed. - -:::tip -This is useful if you want to pass some **specific** data to the next middleware. Remember to perform type assertions when retrieving the data to ensure it is of the expected type. You can also use a non-exported type as a key to avoid collisions. -::: - -```go title="Signature" -func (c fiber.Ctx) Locals(key any, value ...any) any -``` - -```go title="Example" - -// keyType is an unexported type for keys defined in this package. -// This prevents collisions with keys defined in other packages. -type keyType int - -// userKey is the key for user.User values in Contexts. It is -// unexported; clients use user.NewContext and user.FromContext -// instead of using this key directly. -var userKey keyType - -app.Use(func(c fiber.Ctx) error { - c.Locals(userKey, "admin") // Stores the string "admin" under a non-exported type key - return c.Next() -}) - -app.Get("/admin", func(c fiber.Ctx) error { - user, ok := c.Locals(userKey).(string) // Retrieves the data stored under the key and performs a type assertion - if ok && user == "admin" { - return c.Status(fiber.StatusOK).SendString("Welcome, admin!") - } - return c.SendStatus(fiber.StatusForbidden) -}) -``` - -An alternative version of the `Locals` method that takes advantage of Go's generics feature is also available. This version allows for the manipulation and retrieval of local values within a request's context with a more specific data type. - -```go title="Signature" -func Locals[V any](c fiber.Ctx, key any, value ...V) V -``` - -```go title="Example" -app.Use(func(c fiber.Ctx) error { - fiber.Locals[string](c, "john", "doe") - fiber.Locals[int](c, "age", 18) - fiber.Locals[bool](c, "isHuman", true) - return c.Next() -}) - -app.Get("/test", func(c fiber.Ctx) error { - fiber.Locals[string](c, "john") // "doe" - fiber.Locals[int](c, "age") // 18 - fiber.Locals[bool](c, "isHuman") // true - return nil -}) -```` - -Make sure to understand and correctly implement the `Locals` method in both its standard and generic form for better control over route-specific data within your application. - -## Location - -Sets the response [Location](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location) HTTP header to the specified path parameter. - -```go title="Signature" -func (c fiber.Ctx) Location(path string) -``` - -```go title="Example" -app.Post("/", func(c fiber.Ctx) error { - c.Location("http://example.com") - - c.Location("/foo/bar") - - return nil -}) -``` - -## Method +### Method Returns a string corresponding to the HTTP method of the request: `GET`, `POST`, `PUT`, and so on. Optionally, you can override the method by passing a string. @@ -1161,7 +890,7 @@ app.Post("/override", func(c fiber.Ctx) error { }) ``` -## MultipartForm +### MultipartForm To access multipart form entries, you can parse the binary with `MultipartForm()`. This returns a `*multipart.Form`, allowing you to access form values and files. @@ -1200,32 +929,7 @@ app.Post("/", func(c fiber.Ctx) error { }) ``` -## Next - -When **Next** is called, it executes the next method in the stack that matches the current route. You can pass an error struct within the method that will end the chaining and call the [error handler](https://docs.gofiber.io/guide/error-handling). - -```go title="Signature" -func (c fiber.Ctx) Next() error -``` - -```go title="Example" -app.Get("/", func(c fiber.Ctx) error { - fmt.Println("1st route!") - return c.Next() -}) - -app.Get("*", func(c fiber.Ctx) error { - fmt.Println("2nd route!") - return c.Next() -}) - -app.Get("/", func(c fiber.Ctx) error { - fmt.Println("3rd route!") - return c.SendString("Hello, World!") -}) -``` - -## OriginalURL +### OriginalURL Returns the original request URL. @@ -1248,7 +952,7 @@ Returned value is only valid within the handler. Do not store any references. Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more...](../#zero-allocation) ::: -## Params +### Params This method can be used to get the route parameters. You can pass an optional default value that will be returned if the param key does not exist. @@ -1326,7 +1030,7 @@ The generic `Params` function supports returning the following data types based - String: `string` - Byte array: `[]byte` -## Path +### Path Contains the path part of the request URL. Optionally, you can override the path by passing a string. For internal redirects, you might want to call [RestartRouting](ctx.md#restartrouting) instead of [Next](ctx.md#next). @@ -1347,7 +1051,7 @@ app.Get("/users", func(c fiber.Ctx) error { }) ``` -## Port +### Port Returns the remote port of the request. @@ -1365,7 +1069,7 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` -## Protocol +### Protocol Contains the request protocol string: `http` or `https` for **TLS** requests. @@ -1383,7 +1087,7 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` -## Queries +### Queries `Queries` is a function that returns an object containing a property for each query string parameter in the route. @@ -1447,7 +1151,7 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` -## Query +### Query This method returns a string corresponding to a query string parameter by name. You can pass an optional default value that will be returned if the query key does not exist. @@ -1508,7 +1212,7 @@ The generic `Query` function supports returning the following data types based o - String: `string` - Byte array: `[]byte` -## Range +### Range Returns a struct containing the type and a slice of ranges. @@ -1529,142 +1233,7 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` -## Redirect - -Returns the Redirect reference. - -For detailed information, check the [Redirect](./redirect.md) documentation. - -```go title="Signature" -func (c fiber.Ctx) Redirect() *Redirect -``` - -```go title="Example" -app.Get("/coffee", func(c fiber.Ctx) error { - return c.Redirect().To("/teapot") -}) - -app.Get("/teapot", func(c fiber.Ctx) error { - return c.Status(fiber.StatusTeapot).Send("🍵 short and stout 🍵") -}) -``` - -## Render - -Renders a view with data and sends a `text/html` response. By default, `Render` uses the default [**Go Template engine**](https://pkg.go.dev/html/template/). If you want to use another view engine, please take a look at our [**Template middleware**](https://docs.gofiber.io/template). - -```go title="Signature" -func (c fiber.Ctx) Render(name string, bind any, layouts ...string) error -``` - -## Request - -Returns the [*fasthttp.Request](https://pkg.go.dev/github.com/valyala/fasthttp#Request) pointer. - -```go title="Signature" -func (c fiber.Ctx) Request() *fasthttp.Request -``` - -```go title="Example" -app.Get("/", func(c fiber.Ctx) error { - c.Request().Header.Method() - // => []byte("GET") -}) -``` - -## RequestCtx - -Returns [\*fasthttp.RequestCtx](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx) that is compatible with the `context.Context` interface that requires a deadline, a cancellation signal, and other values across API boundaries. - -```go title="Signature" -func (c fiber.Ctx) RequestCtx() *fasthttp.RequestCtx -``` - -:::info -Please read the [Fasthttp Documentation](https://pkg.go.dev/github.com/valyala/fasthttp?tab=doc) for more information. -::: - -## Response - -Returns the [\*fasthttp.Response](https://pkg.go.dev/github.com/valyala/fasthttp#Response) pointer. - -```go title="Signature" -func (c fiber.Ctx) Response() *fasthttp.Response -``` - -```go title="Example" -app.Get("/", func(c fiber.Ctx) error { - c.Response().BodyWriter().Write([]byte("Hello, World!")) - // => "Hello, World!" - return nil -}) -``` - -## Reset - -Resets the context fields by the given request when using server handlers. - -```go title="Signature" -func (c fiber.Ctx) Reset(fctx *fasthttp.RequestCtx) -``` - -It is used outside of the Fiber Handlers to reset the context for the next request. - -## RestartRouting - -Instead of executing the next method when calling [Next](ctx.md#next), **RestartRouting** restarts execution from the first method that matches the current route. This may be helpful after overriding the path, i.e., an internal redirect. Note that handlers might be executed again, which could result in an infinite loop. - -```go title="Signature" -func (c fiber.Ctx) RestartRouting() error -``` - -```go title="Example" -app.Get("/new", func(c fiber.Ctx) error { - return c.SendString("From /new") -}) - -app.Get("/old", func(c fiber.Ctx) error { - c.Path("/new") - return c.RestartRouting() -}) -``` - -## Route - -Returns the matched [Route](https://pkg.go.dev/github.com/gofiber/fiber?tab=doc#Route) struct. - -```go title="Signature" -func (c fiber.Ctx) Route() *Route -``` - -```go title="Example" -// http://localhost:8080/hello - -app.Get("/hello/:name", func(c fiber.Ctx) error { - r := c.Route() - fmt.Println(r.Method, r.Path, r.Params, r.Handlers) - // GET /hello/:name handler [name] - - // ... -}) -``` - -:::caution -Do not rely on `c.Route()` in middlewares **before** calling `c.Next()` - `c.Route()` returns the **last executed route**. -::: - -```go title="Example" -func MyMiddleware() fiber.Handler { - return func(c fiber.Ctx) error { - beforeNext := c.Route().Path // Will be '/' - err := c.Next() - afterNext := c.Route().Path // Will be '/hello/:name' - return err - } -} -``` - -## SaveFile +### SaveFile Method is used to save **any** multipart file to disk. @@ -1697,7 +1266,7 @@ app.Post("/", func(c fiber.Ctx) error { }) ``` -## SaveFileToStorage +### SaveFileToStorage Method is used to save **any** multipart file to an external storage system. @@ -1732,7 +1301,7 @@ app.Post("/", func(c fiber.Ctx) error { }) ``` -## Schema +### Schema Contains the request protocol string: `http` or `https` for TLS requests. @@ -1754,7 +1323,7 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` -## Secure +### Secure A boolean property that is `true` if a **TLS** connection is established. @@ -1767,7 +1336,557 @@ func (c fiber.Ctx) Secure() bool c.Protocol() == "https" ``` -## Send +### Stale + +[https://expressjs.com/en/4x/api.html#req.stale](https://expressjs.com/en/4x/api.html#req.stale) + +```go title="Signature" +func (c fiber.Ctx) Stale() bool +``` + +### Subdomains + +Returns a slice of subdomains in the domain name of the request. + +The application property `subdomain offset`, which defaults to `2`, is used for determining the beginning of the subdomain segments. + +```go title="Signature" +func (c fiber.Ctx) Subdomains(offset ...int) []string +``` + +```go title="Example" +// Host: "tobi.ferrets.example.com" + +app.Get("/", func(c fiber.Ctx) error { + c.Subdomains() // ["ferrets", "tobi"] + c.Subdomains(1) // ["tobi"] + + // ... +}) +``` + +### XHR + +A boolean property that is `true` if the request’s [X-Requested-With](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) header field is [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest), indicating that the request was issued by a client library (such as [jQuery](https://api.jquery.com/jQuery.ajax/)). + +```go title="Signature" +func (c fiber.Ctx) XHR() bool +``` + +```go title="Example" +// X-Requested-With: XMLHttpRequest + +app.Get("/", func(c fiber.Ctx) error { + c.XHR() // true + + // ... +}) +``` + +## Response + +Methods which modify the response object. + +:::tip +Use `c.Res()` to limit gopls suggestions to only these methods! +::: + +### Append + +Appends the specified **value** to the HTTP response header field. + +:::caution +If the header is **not** already set, it creates the header with the specified value. +::: + +```go title="Signature" +func (c fiber.Ctx) Append(field string, values ...string) +``` + +```go title="Example" +app.Get("/", func(c fiber.Ctx) error { + c.Append("Link", "http://google.com", "http://localhost") + // => Link: http://google.com, http://localhost + + c.Append("Link", "Test") + // => Link: http://google.com, http://localhost, Test + + // ... +}) +``` + +### Attachment + +Sets the HTTP response [Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header field to `attachment`. + +```go title="Signature" +func (c fiber.Ctx) Attachment(filename ...string) +``` + +```go title="Example" +app.Get("/", func(c fiber.Ctx) error { + c.Attachment() + // => Content-Disposition: attachment + + c.Attachment("./upload/images/logo.png") + // => Content-Disposition: attachment; filename="logo.png" + // => Content-Type: image/png + + // ... +}) +``` + +### AutoFormat + +Performs content-negotiation on the [Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) HTTP header. It uses [Accepts](ctx.md#accepts) to select a proper format. +The supported content types are `text/html`, `text/plain`, `application/json`, and `application/xml`. +For more flexible content negotiation, use [Format](ctx.md#format). + +:::info +If the header is **not** specified or there is **no** proper format, **text/plain** is used. +::: + +```go title="Signature" +func (c fiber.Ctx) AutoFormat(body any) error +``` + +```go title="Example" +app.Get("/", func(c fiber.Ctx) error { + // Accept: text/plain + c.AutoFormat("Hello, World!") + // => Hello, World! + + // Accept: text/html + c.AutoFormat("Hello, World!") + // =>

Hello, World!

+ + type User struct { + Name string + } + user := User{"John Doe"} + + // Accept: application/json + c.AutoFormat(user) + // => {"Name":"John Doe"} + + // Accept: application/xml + c.AutoFormat(user) + // => John Doe + // .. +}) +``` + +### ClearCookie + +Expires a client cookie (or all cookies if left empty). + +```go title="Signature" +func (c fiber.Ctx) ClearCookie(key ...string) +``` + +```go title="Example" +app.Get("/", func(c fiber.Ctx) error { + // Clears all cookies: + c.ClearCookie() + + // Expire specific cookie by name: + c.ClearCookie("user") + + // Expire multiple cookies by names: + c.ClearCookie("token", "session", "track_id", "version") + // ... +}) +``` + +:::caution +Web browsers and other compliant clients will only clear the cookie if the given options are identical to those when creating the cookie, excluding `Expires` and `MaxAge`. `ClearCookie` will not set these values for you - a technique similar to the one shown below should be used to ensure your cookie is deleted. +::: + +```go title="Example" +app.Get("/set", func(c fiber.Ctx) error { + c.Cookie(&fiber.Cookie{ + Name: "token", + Value: "randomvalue", + Expires: time.Now().Add(24 * time.Hour), + HTTPOnly: true, + SameSite: "lax", + }) + + // ... +}) + +app.Get("/delete", func(c fiber.Ctx) error { + c.Cookie(&fiber.Cookie{ + Name: "token", + // Set expiry date to the past + Expires: time.Now().Add(-(time.Hour * 2)), + HTTPOnly: true, + SameSite: "lax", + }) + + // ... +}) +``` + +### Cookie + +Sets a cookie. + +```go title="Signature" +func (c fiber.Ctx) Cookie(cookie *Cookie) +``` + +```go +type Cookie struct { + Name string `json:"name"` // The name of the cookie + Value string `json:"value"` // The value of the cookie + Path string `json:"path"` // Specifies a URL path which is allowed to receive the cookie + Domain string `json:"domain"` // Specifies the domain which is allowed to receive the cookie + MaxAge int `json:"max_age"` // The maximum age (in seconds) of the cookie + Expires time.Time `json:"expires"` // The expiration date of the cookie + Secure bool `json:"secure"` // Indicates that the cookie should only be transmitted over a secure HTTPS connection + HTTPOnly bool `json:"http_only"` // Indicates that the cookie is accessible only through the HTTP protocol + SameSite string `json:"same_site"` // Controls whether or not a cookie is sent with cross-site requests + Partitioned bool `json:"partitioned"` // Indicates if the cookie is stored in a partitioned cookie jar + SessionOnly bool `json:"session_only"` // Indicates if the cookie is a session-only cookie +} +``` + +```go title="Example" +app.Get("/", func(c fiber.Ctx) error { + // Create cookie + cookie := new(fiber.Cookie) + cookie.Name = "john" + cookie.Value = "doe" + cookie.Expires = time.Now().Add(24 * time.Hour) + + // Set cookie + c.Cookie(cookie) + // ... +}) +``` + +:::info +Partitioned cookies allow partitioning the cookie jar by top-level site, enhancing user privacy by preventing cookies from being shared across different sites. This feature is particularly useful in scenarios where a user interacts with embedded third-party services that should not have access to the main site's cookies. You can check out [CHIPS](https://developers.google.com/privacy-sandbox/3pcd/chips) for more information. +::: + +```go title="Example" +app.Get("/", func(c fiber.Ctx) error { + // Create a new partitioned cookie + cookie := new(fiber.Cookie) + cookie.Name = "user_session" + cookie.Value = "abc123" + cookie.Partitioned = true // This cookie will be stored in a separate jar when it's embedded into another website + + // Set the cookie in the response + c.Cookie(cookie) + return c.SendString("Partitioned cookie set") +}) +``` + +### Download + +Transfers the file from the given path as an `attachment`. + +Typically, browsers will prompt the user to download. By default, the [Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header `filename=` parameter is the file path (_this typically appears in the browser dialog_). +Override this default with the **filename** parameter. + +```go title="Signature" +func (c fiber.Ctx) Download(file string, filename ...string) error +``` + +```go title="Example" +app.Get("/", func(c fiber.Ctx) error { + return c.Download("./files/report-12345.pdf") + // => Download report-12345.pdf + + return c.Download("./files/report-12345.pdf", "report.pdf") + // => Download report.pdf +}) +``` + +### End + +End immediately flushes the current response and closes the underlying connection. + +```go title="Signature" +func (c fiber.Ctx) End() error +``` + +```go title="Example" +app.Get("/", func(c fiber.Ctx) error { + c.SendString("Hello World!") + return c.End() +}) +``` + +:::caution +Calling `c.End()` will disallow further writes to the underlying connection. +::: + +End can be used to stop a middleware from modifying a response of a handler/other middleware down the method chain +when they regain control after calling `c.Next()`. + +```go title="Example" +// Error Logging/Responding middleware +app.Use(func(c fiber.Ctx) error { + err := c.Next() + + // Log errors & write the error to the response + if err != nil { + log.Printf("Got error in middleware: %v", err) + return c.Writef("(got error %v)", err) + } + + // No errors occured + return nil +}) + +// Handler with simulated error +app.Get("/", func(c fiber.Ctx) error { + // Closes the connection instantly after writing from this handler + // and disallow further modification of its response + defer c.End() + + c.SendString("Hello, ... I forgot what comes next!") + return errors.New("some error") +}) +``` + +### Format + +Performs content-negotiation on the [Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) HTTP header. It uses [Accepts](ctx.md#accepts) to select a proper format from the supplied offers. A default handler can be provided by setting the `MediaType` to `"default"`. If no offers match and no default is provided, a 406 (Not Acceptable) response is sent. The Content-Type is automatically set when a handler is selected. + +:::info +If the Accept header is **not** specified, the first handler will be used. +::: + +```go title="Signature" +func (c fiber.Ctx) Format(handlers ...ResFmt) error +``` + +```go title="Example" +// Accept: application/json => {"command":"eat","subject":"fruit"} +// Accept: text/plain => Eat Fruit! +// Accept: application/xml => Not Acceptable +app.Get("/no-default", func(c fiber.Ctx) error { + return c.Format( + fiber.ResFmt{"application/json", func(c fiber.Ctx) error { + return c.JSON(fiber.Map{ + "command": "eat", + "subject": "fruit", + }) + }}, + fiber.ResFmt{"text/plain", func(c fiber.Ctx) error { + return c.SendString("Eat Fruit!") + }}, + ) +}) + +// Accept: application/json => {"command":"eat","subject":"fruit"} +// Accept: text/plain => Eat Fruit! +// Accept: application/xml => Eat Fruit! +app.Get("/default", func(c fiber.Ctx) error { + textHandler := func(c fiber.Ctx) error { + return c.SendString("Eat Fruit!") + } + + handlers := []fiber.ResFmt{ + {"application/json", func(c fiber.Ctx) error { + return c.JSON(fiber.Map{ + "command": "eat", + "subject": "fruit", + }) + }}, + {"text/plain", textHandler}, + {"default", textHandler}, + } + + return c.Format(handlers...) +}) +``` + +### JSON + +Converts any **interface** or **string** to JSON using the [encoding/json](https://pkg.go.dev/encoding/json) package. + +:::info +JSON also sets the content header to the `ctype` parameter. If no `ctype` is passed in, the header is set to `application/json`. +::: + +```go title="Signature" +func (c fiber.Ctx) JSON(data any, ctype ...string) error +``` + +```go title="Example" +type SomeStruct struct { + Name string + Age uint8 +} + +app.Get("/json", func(c fiber.Ctx) error { + // Create data struct: + data := SomeStruct{ + Name: "Grame", + Age: 20, + } + + return c.JSON(data) + // => Content-Type: application/json + // => {"Name": "Grame", "Age": 20} + + return c.JSON(fiber.Map{ + "name": "Grame", + "age": 20, + }) + // => Content-Type: application/json + // => {"name": "Grame", "age": 20} + + return c.JSON(fiber.Map{ + "type": "https://example.com/probs/out-of-credit", + "title": "You do not have enough credit.", + "status": 403, + "detail": "Your current balance is 30, but that costs 50.", + "instance": "/account/12345/msgs/abc", + }, "application/problem+json") + // => Content-Type: application/problem+json + // => "{ + // => "type": "https://example.com/probs/out-of-credit", + // => "title": "You do not have enough credit.", + // => "status": 403, + // => "detail": "Your current balance is 30, but that costs 50.", + // => "instance": "/account/12345/msgs/abc", + // => }" +}) +``` + +### JSONP + +Sends a JSON response with JSONP support. This method is identical to [JSON](ctx.md#json), except that it opts-in to JSONP callback support. By default, the callback name is simply `callback`. + +Override this by passing a **named string** in the method. + +```go title="Signature" +func (c fiber.Ctx) JSONP(data any, callback ...string) error +``` + +```go title="Example" +type SomeStruct struct { + Name string + Age uint8 +} + +app.Get("/", func(c fiber.Ctx) error { + // Create data struct: + data := SomeStruct{ + Name: "Grame", + Age: 20, + } + + return c.JSONP(data) + // => callback({"Name": "Grame", "Age": 20}) + + return c.JSONP(data, "customFunc") + // => customFunc({"Name": "Grame", "Age": 20}) +}) +``` + +### CBOR + +CBOR converts any interface or string to CBOR encoded bytes. + +:::info +CBOR also sets the content header to the `ctype` parameter. If no `ctype` is passed in, the header is set to `application/cbor`. +::: + +```go title="Signature" +func (c fiber.Ctx) CBOR(data any, ctype ...string) error +``` + +```go title="Example" +type SomeStruct struct { + Name string `cbor:"name"` + Age uint8 `cbor:"age"` +} + +app.Get("/cbor", func(c fiber.Ctx) error { + // Create data struct: + data := SomeStruct{ + Name: "Grame", + Age: 20, + } + + return c.CBOR(data) + // => Content-Type: application/cbor + // => \xa2dnameeGramecage\x14 + + return c.CBOR(fiber.Map{ + "name": "Grame", + "age": 20, + }) + // => Content-Type: application/cbor + // => \xa2dnameeGramecage\x14 + + return c.CBOR(fiber.Map{ + "type": "https://example.com/probs/out-of-credit", + "title": "You do not have enough credit.", + "status": 403, + "detail": "Your current balance is 30, but that costs 50.", + "instance": "/account/12345/msgs/abc", + }) + // => Content-Type: application/cbor + // => \xa5dtypex'https://example.com/probs/out-of-creditetitlex\x1eYou do not have enough credit.fstatus\x19\x01\x93fdetailx.Your current balance is 30, but that costs 50.hinstancew/account/12345/msgs/abc +}) +``` + +### Links + +Joins the links followed by the property to populate the response’s [Link](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link) HTTP header field. + +```go title="Signature" +func (c fiber.Ctx) Links(link ...string) +``` + +```go title="Example" +app.Get("/", func(c fiber.Ctx) error { + c.Links( + "http://api.example.com/users?page=2", "next", + "http://api.example.com/users?page=5", "last", + ) + // Link: ; rel="next", + // ; rel="last" + + // ... +}) +``` + +### Location + +Sets the response [Location](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location) HTTP header to the specified path parameter. + +```go title="Signature" +func (c fiber.Ctx) Location(path string) +``` + +```go title="Example" +app.Post("/", func(c fiber.Ctx) error { + c.Location("http://example.com") + + c.Location("/foo/bar") + + return nil +}) +``` + +### Render + +Renders a view with data and sends a `text/html` response. By default, `Render` uses the default [**Go Template engine**](https://pkg.go.dev/html/template/). If you want to use another view engine, please take a look at our [**Template middleware**](https://docs.gofiber.io/template). + +```go title="Signature" +func (c fiber.Ctx) Render(name string, bind any, layouts ...string) error +``` + +### Send Sets the HTTP response body. @@ -1802,7 +1921,7 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` -## SendFile +### SendFile Transfers the file from the given path. Sets the [Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) response HTTP header field based on the **file** extension or format. @@ -1916,7 +2035,7 @@ app.Get("/file", func(c fiber.Ctx) error { For sending multiple files from an embedded file system, [this functionality](../middleware/static.md#serving-files-using-embedfs) can be used. ::: -## SendStatus +### SendStatus Sets the status code and the correct status message in the body if the response body is **empty**. @@ -1939,7 +2058,7 @@ app.Get("/not-found", func(c fiber.Ctx) error { }) ``` -## SendStream +### SendStream Sets the response body to a stream of data and adds an optional body size. @@ -1954,7 +2073,7 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` -## SendString +### SendString Sets the response body to a string. @@ -1969,7 +2088,7 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` -## SendStreamWriter +### SendStreamWriter Sets the response body stream writer. @@ -2029,7 +2148,7 @@ app.Get("/wait", func(c fiber.Ctx) error { }) ``` -## Set +### Set Sets the response’s HTTP header field to the specified `key`, `value`. @@ -2046,33 +2165,7 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` -## SetContext - -Sets the user-specified implementation for the `context.Context` interface. - -```go title="Signature" -func (c fiber.Ctx) SetContext(ctx context.Context) -``` - -```go title="Example" -app.Get("/", func(c fiber.Ctx) error { - ctx := context.Background() - c.SetContext(ctx) - // Here ctx could be any context implementation - - // ... -}) -``` - -## Stale - -[https://expressjs.com/en/4x/api.html#req.stale](https://expressjs.com/en/4x/api.html#req.stale) - -```go title="Signature" -func (c fiber.Ctx) Stale() bool -``` - -## Status +### Status Sets the HTTP status for the response. @@ -2099,44 +2192,7 @@ app.Get("/world", func(c fiber.Ctx) error { }) ``` -## String - -Returns a unique string representation of the context. - -```go title="Signature" -func (c fiber.Ctx) String() string -``` - -```go title="Example" -app.Get("/", func(c fiber.Ctx) error { - c.String() // => "#0000000100000001 - 127.0.0.1:3000 <-> 127.0.0.1:61516 - GET http://localhost:3000/" - - // ... -}) -``` - -## Subdomains - -Returns a slice of subdomains in the domain name of the request. - -The application property `subdomain offset`, which defaults to `2`, is used for determining the beginning of the subdomain segments. - -```go title="Signature" -func (c fiber.Ctx) Subdomains(offset ...int) []string -``` - -```go title="Example" -// Host: "tobi.ferrets.example.com" - -app.Get("/", func(c fiber.Ctx) error { - c.Subdomains() // ["ferrets", "tobi"] - c.Subdomains(1) // ["tobi"] - - // ... -}) -``` - -## Type +### Type Sets the [Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) HTTP header to the MIME type listed [here](https://github.com/nginx/nginx/blob/master/conf/mime.types) specified by the file **extension**. @@ -2160,7 +2216,7 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` -## Vary +### Vary Adds the given header field to the [Vary](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary) response header. This will append the header if not already listed; otherwise, it leaves it listed in the current location. @@ -2187,29 +2243,7 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` -## ViewBind - -Adds variables to the default view variable map binding to the template engine. -Variables are read by the `Render` method and may be overwritten. - -```go title="Signature" -func (c fiber.Ctx) ViewBind(vars Map) error -``` - -```go title="Example" -app.Use(func(c fiber.Ctx) error { - c.ViewBind(fiber.Map{ - "Title": "Hello, World!", - }) - return c.Next() -}) - -app.Get("/", func(c fiber.Ctx) error { - return c.Render("xxx.tmpl", fiber.Map{}) // Render will use the Title variable -}) -``` - -## Write +### Write Adopts the `Writer` interface. @@ -2225,7 +2259,7 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` -## Writef +### Writef Writes a formatted string using a format specifier. @@ -2242,7 +2276,7 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` -## WriteString +### WriteString Writes a string to the response body. @@ -2257,25 +2291,7 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` -## XHR - -A boolean property that is `true` if the request’s [X-Requested-With](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) header field is [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest), indicating that the request was issued by a client library (such as [jQuery](https://api.jquery.com/jQuery.ajax/)). - -```go title="Signature" -func (c fiber.Ctx) XHR() bool -``` - -```go title="Example" -// X-Requested-With: XMLHttpRequest - -app.Get("/", func(c fiber.Ctx) error { - c.XHR() // true - - // ... -}) -``` - -## XML +### XML Converts any **interface** or **string** to XML using the standard `encoding/xml` package. diff --git a/req.go b/req.go new file mode 100644 index 00000000..1b6952ff --- /dev/null +++ b/req.go @@ -0,0 +1,159 @@ +package fiber + +import ( + "crypto/tls" + "mime/multipart" +) + +//go:generate ifacemaker --file req.go --struct DefaultReq --iface Req --pkg fiber --output req_interface_gen.go --not-exported true --iface-comment "Req" +type DefaultReq struct { + ctx *DefaultCtx +} + +func (r *DefaultReq) Accepts(offers ...string) string { + return r.ctx.Accepts(offers...) +} + +func (r *DefaultReq) AcceptsCharsets(offers ...string) string { + return r.ctx.AcceptsCharsets(offers...) +} + +func (r *DefaultReq) AcceptsEncodings(offers ...string) string { + return r.ctx.AcceptsEncodings(offers...) +} + +func (r *DefaultReq) AcceptsLanguages(offers ...string) string { + return r.ctx.AcceptsLanguages(offers...) +} + +func (r *DefaultReq) BaseURL() string { + return r.ctx.BaseURL() +} + +func (r *DefaultReq) Body() []byte { + return r.ctx.Body() +} + +func (r *DefaultReq) BodyRaw() []byte { + return r.ctx.BodyRaw() +} + +func (r *DefaultReq) ClientHelloInfo() *tls.ClientHelloInfo { + return r.ctx.ClientHelloInfo() +} + +func (r *DefaultReq) Cookies(key string, defaultValue ...string) string { + return r.ctx.Cookies(key, defaultValue...) +} + +func (r *DefaultReq) FormFile(key string) (*multipart.FileHeader, error) { + return r.ctx.FormFile(key) +} + +func (r *DefaultReq) FormValue(key string, defaultValue ...string) string { + return r.ctx.FormValue(key, defaultValue...) +} + +func (r *DefaultReq) Fresh() bool { + return r.ctx.Fresh() +} + +func (r *DefaultReq) Get(key string, defaultValue ...string) string { + return r.ctx.Get(key, defaultValue...) +} + +func (r *DefaultReq) Host() string { + return r.ctx.Host() +} + +func (r *DefaultReq) Hostname() string { + return r.ctx.Hostname() +} + +func (r *DefaultReq) IP() string { + return r.ctx.IP() +} + +func (r *DefaultReq) IPs() []string { + return r.ctx.IPs() +} + +func (r *DefaultReq) Is(extension string) bool { + return r.ctx.Is(extension) +} + +func (r *DefaultReq) IsFromLocal() bool { + return r.ctx.IsFromLocal() +} + +func (r *DefaultReq) IsProxyTrusted() bool { + return r.ctx.IsProxyTrusted() +} + +func (r *DefaultReq) Method(override ...string) string { + return r.ctx.Method(override...) +} + +func (r *DefaultReq) MultipartForm() (*multipart.Form, error) { + return r.ctx.MultipartForm() +} + +func (r *DefaultReq) OriginalURL() string { + return r.ctx.OriginalURL() +} + +func (r *DefaultReq) Params(key string, defaultValue ...string) string { + return r.ctx.Params(key, defaultValue...) +} + +func (r *DefaultReq) Path(override ...string) string { + return r.ctx.Path(override...) +} + +func (r *DefaultReq) Port() string { + return r.ctx.Port() +} + +func (r *DefaultReq) Protocol() string { + return r.ctx.Protocol() +} + +func (r *DefaultReq) Queries() map[string]string { + return r.ctx.Queries() +} + +func (r *DefaultReq) Query(key string, defaultValue ...string) string { + return r.ctx.Query(key, defaultValue...) +} + +func (r *DefaultReq) Range(size int) (Range, error) { + return r.ctx.Range(size) +} + +func (r *DefaultReq) Route() *Route { + return r.ctx.Route() +} + +func (r *DefaultReq) SaveFile(fileheader *multipart.FileHeader, path string) error { + return r.ctx.SaveFile(fileheader, path) +} + +func (r *DefaultReq) SaveFileToStorage(fileheader *multipart.FileHeader, path string, storage Storage) error { + return r.ctx.SaveFileToStorage(fileheader, path, storage) +} + +func (r *DefaultReq) Secure() bool { + return r.ctx.Secure() +} + +func (r *DefaultReq) Stale() bool { + return r.ctx.Stale() +} + +func (r *DefaultReq) Subdomains(offset ...int) []string { + return r.ctx.Subdomains(offset...) +} + +func (r *DefaultReq) XHR() bool { + return r.ctx.XHR() +} diff --git a/req_interface_gen.go b/req_interface_gen.go new file mode 100644 index 00000000..643854b7 --- /dev/null +++ b/req_interface_gen.go @@ -0,0 +1,49 @@ +// Code generated by ifacemaker; DO NOT EDIT. + +package fiber + +import ( + "crypto/tls" + "mime/multipart" +) + +// Req +type Req interface { + Accepts(offers ...string) string + AcceptsCharsets(offers ...string) string + AcceptsEncodings(offers ...string) string + AcceptsLanguages(offers ...string) string + BaseURL() string + Body() []byte + BodyRaw() []byte + ClientHelloInfo() *tls.ClientHelloInfo + Cookies(key string, defaultValue ...string) string + FormFile(key string) (*multipart.FileHeader, error) + FormValue(key string, defaultValue ...string) string + Fresh() bool + Get(key string, defaultValue ...string) string + Host() string + Hostname() string + IP() string + IPs() []string + Is(extension string) bool + IsFromLocal() bool + IsProxyTrusted() bool + Method(override ...string) string + MultipartForm() (*multipart.Form, error) + OriginalURL() string + Params(key string, defaultValue ...string) string + Path(override ...string) string + Port() string + Protocol() string + Queries() map[string]string + Query(key string, defaultValue ...string) string + Range(size int) (Range, error) + Route() *Route + SaveFile(fileheader *multipart.FileHeader, path string) error + SaveFileToStorage(fileheader *multipart.FileHeader, path string, storage Storage) error + Secure() bool + Stale() bool + Subdomains(offset ...int) []string + XHR() bool +} diff --git a/res.go b/res.go new file mode 100644 index 00000000..c2dd6f7f --- /dev/null +++ b/res.go @@ -0,0 +1,118 @@ +package fiber + +import ( + "bufio" +) + +//go:generate ifacemaker --file res.go --struct DefaultRes --iface Res --pkg fiber --output res_interface_gen.go --not-exported true --iface-comment "Res" +type DefaultRes struct { + ctx *DefaultCtx +} + +func (r *DefaultRes) Append(field string, values ...string) { + r.ctx.Append(field, values...) +} + +func (r *DefaultRes) Attachment(filename ...string) { + r.ctx.Attachment(filename...) +} + +func (r *DefaultRes) AutoFormat(body any) error { + return r.ctx.AutoFormat(body) +} + +func (r *DefaultRes) CBOR(body any, ctype ...string) error { + return r.ctx.CBOR(body, ctype...) +} + +func (r *DefaultRes) ClearCookie(key ...string) { + r.ctx.ClearCookie(key...) +} + +func (r *DefaultRes) Cookie(cookie *Cookie) { + r.ctx.Cookie(cookie) +} + +func (r *DefaultRes) Download(file string, filename ...string) error { + return r.ctx.Download(file, filename...) +} + +func (r *DefaultRes) Format(handlers ...ResFmt) error { + return r.ctx.Format(handlers...) +} + +func (r *DefaultRes) Get(key string, defaultValue ...string) string { + return r.ctx.GetRespHeader(key, defaultValue...) +} + +func (r *DefaultRes) JSON(body any, ctype ...string) error { + return r.ctx.JSON(body, ctype...) +} + +func (r *DefaultRes) JSONP(data any, callback ...string) error { + return r.ctx.JSONP(data, callback...) +} + +func (r *DefaultRes) Links(link ...string) { + r.ctx.Links(link...) +} + +func (r *DefaultRes) Location(path string) { + r.ctx.Location(path) +} + +func (r *DefaultRes) Render(name string, bind any, layouts ...string) error { + return r.ctx.Render(name, bind, layouts...) +} + +func (r *DefaultRes) Send(body []byte) error { + return r.ctx.Send(body) +} + +func (r *DefaultRes) SendFile(file string, config ...SendFile) error { + return r.ctx.SendFile(file, config...) +} + +func (r *DefaultRes) SendStatus(status int) error { + return r.ctx.SendStatus(status) +} + +func (r *DefaultRes) SendString(body string) error { + return r.ctx.SendString(body) +} + +func (r *DefaultRes) SendStreamWriter(streamWriter func(*bufio.Writer)) error { + return r.ctx.SendStreamWriter(streamWriter) +} + +func (r *DefaultRes) Set(key, val string) { + r.ctx.Set(key, val) +} + +func (r *DefaultRes) Status(status int) Ctx { + return r.ctx.Status(status) +} + +func (r *DefaultRes) Type(extension string, charset ...string) Ctx { + return r.ctx.Type(extension, charset...) +} + +func (r *DefaultRes) Vary(fields ...string) { + r.ctx.Vary(fields...) +} + +func (r *DefaultRes) Write(p []byte) (int, error) { + return r.ctx.Write(p) +} + +func (r *DefaultRes) Writef(f string, a ...any) (int, error) { + return r.ctx.Writef(f, a...) +} + +func (r *DefaultRes) WriteString(s string) (int, error) { + return r.ctx.WriteString(s) +} + +func (r *DefaultRes) XML(data any) error { + return r.ctx.XML(data) +} diff --git a/res_interface_gen.go b/res_interface_gen.go new file mode 100644 index 00000000..93a4036a --- /dev/null +++ b/res_interface_gen.go @@ -0,0 +1,38 @@ +// Code generated by ifacemaker; DO NOT EDIT. + +package fiber + +import ( + "bufio" +) + +// Res +type Res interface { + Append(field string, values ...string) + Attachment(filename ...string) + AutoFormat(body any) error + CBOR(body any, ctype ...string) error + ClearCookie(key ...string) + Cookie(cookie *Cookie) + Download(file string, filename ...string) error + Format(handlers ...ResFmt) error + Get(key string, defaultValue ...string) string + JSON(body any, ctype ...string) error + JSONP(data any, callback ...string) error + Links(link ...string) + Location(path string) + Render(name string, bind any, layouts ...string) error + Send(body []byte) error + SendFile(file string, config ...SendFile) error + SendStatus(status int) error + SendString(body string) error + SendStreamWriter(streamWriter func(*bufio.Writer)) error + Set(key, val string) + Status(status int) Ctx + Type(extension string, charset ...string) Ctx + Vary(fields ...string) + Write(p []byte) (int, error) + Writef(f string, a ...any) (int, error) + WriteString(s string) (int, error) + XML(data any) error +} From a46937efe324c6e1c5d1a94e974ce824ae05f4dc Mon Sep 17 00:00:00 2001 From: RW Date: Wed, 5 Mar 2025 15:07:49 +0100 Subject: [PATCH 24/53] Update release-drafter.yml --- .github/release-drafter.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 988db887..59792e48 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -17,6 +17,7 @@ categories: - title: '🧹 Updates' labels: - '🧹 Updates' + - '⚡️ Performance' - title: '🐛 Fixes' labels: - '☢️ Bug' @@ -48,6 +49,7 @@ version-resolver: - '☢️ Bug' - '🤖 Dependencies' - '🧹 Updates' + - '⚡️ Performance' default: patch template: | $CHANGES From a5c7b77aec011e5112164724a240618f02411b84 Mon Sep 17 00:00:00 2001 From: RW Date: Wed, 5 Mar 2025 15:08:00 +0100 Subject: [PATCH 25/53] Update release.yml --- .github/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/release.yml b/.github/release.yml index 8f1601cc..6f7e89c6 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -12,6 +12,7 @@ changelog: - title: '🧹 Updates' labels: - '🧹 Updates' + - '⚡️ Performance' - title: '🐛 Bug Fixes' labels: - '☢️ Bug' From 6953325df5d5cb4d22eb1f5373dceaad6353b9bc Mon Sep 17 00:00:00 2001 From: Kashiwa <13825170+ksw2000@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:00:18 +0800 Subject: [PATCH 26/53] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20replace?= =?UTF-8?q?=20findLastCharsetPosition=20with=20strings.LastIndexByte=20(#3?= =?UTF-8?q?338)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ Refactor: replace findLastCharsetPosition with strings.LastIndexByte * 🩹 Fix: correct loop condition in Go benchmark --- path.go | 18 ++---------------- path_test.go | 4 ++-- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/path.go b/path.go index e8b90eaf..fdd61e39 100644 --- a/path.go +++ b/path.go @@ -123,8 +123,6 @@ var ( parameterConstraintSeparatorChars = []byte{paramConstraintSeparator} // list of parameter constraint data start parameterConstraintDataStartChars = []byte{paramConstraintDataStart} - // list of parameter constraint data end - parameterConstraintDataEndChars = []byte{paramConstraintDataEnd} // list of parameter constraint data separator parameterConstraintDataSeparatorChars = []byte{paramConstraintDataSeparator} ) @@ -317,7 +315,7 @@ func (routeParser *routeParser) analyseParameterPart(pattern string, customConst // find constraint part if exists in the parameter part and remove it if parameterEndPosition > 0 { parameterConstraintStart = findNextNonEscapedCharsetPosition(pattern[0:parameterEndPosition], parameterConstraintStartChars) - parameterConstraintEnd = findLastCharsetPosition(pattern[0:parameterEndPosition+1], parameterConstraintEndChars) + parameterConstraintEnd = strings.LastIndexByte(pattern[0:parameterEndPosition+1], paramConstraintEnd) } // cut params part @@ -335,7 +333,7 @@ func (routeParser *routeParser) analyseParameterPart(pattern string, customConst for _, c := range userConstraints { start := findNextNonEscapedCharsetPosition(c, parameterConstraintDataStartChars) - end := findLastCharsetPosition(c, parameterConstraintDataEndChars) + end := strings.LastIndexByte(c, paramConstraintDataEnd) // Assign constraint if start != -1 && end != -1 { @@ -421,18 +419,6 @@ func findNextCharsetPosition(search string, charset []byte) int { return nextPosition } -// findLastCharsetPosition search the last char position from the charset -func findLastCharsetPosition(search string, charset []byte) int { - lastPosition := -1 - for _, char := range charset { - if pos := strings.LastIndexByte(search, char); pos != -1 && (pos < lastPosition || lastPosition == -1) { - lastPosition = pos - } - } - - return lastPosition -} - // findNextCharsetPositionConstraint search the next char position from the charset // unlike findNextCharsetPosition, it takes care of constraint start-end chars to parse route pattern func findNextCharsetPositionConstraint(search string, charset []byte) int { diff --git a/path_test.go b/path_test.go index 14eda46c..076066a4 100644 --- a/path_test.go +++ b/path_test.go @@ -217,7 +217,7 @@ func Benchmark_Path_matchParams(t *testing.B) { state = "not match" } t.Run(testCollection.pattern+" | "+state+" | "+c.url, func(b *testing.B) { - for i := 0; i <= b.N; i++ { + for i := 0; i < b.N; i++ { if match := parser.getMatch(c.url, c.url, &ctxParams, c.partialCheck); match { // Get testCases from the original path matchRes = true @@ -250,7 +250,7 @@ func Benchmark_RoutePatternMatch(t *testing.B) { state = "not match" } t.Run(testCollection.pattern+" | "+state+" | "+c.url, func(b *testing.B) { - for i := 0; i <= b.N; i++ { + for i := 0; i < b.N; i++ { if match := RoutePatternMatch(c.url, testCollection.pattern); match { // Get testCases from the original path matchRes = true From 6a9cd7d212fdd6027942d2845ca15c577824041a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Mar 2025 12:29:49 +0000 Subject: [PATCH 27/53] build(deps): bump golang.org/x/crypto from 0.35.0 to 0.36.0 Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.35.0 to 0.36.0. - [Commits](https://github.com/golang/crypto/compare/v0.35.0...v0.36.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index c1431515..5367e203 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/tinylib/msgp v1.2.5 github.com/valyala/bytebufferpool v1.0.0 github.com/valyala/fasthttp v1.59.0 - golang.org/x/crypto v0.35.0 + golang.org/x/crypto v0.36.0 ) require ( @@ -24,7 +24,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect golang.org/x/net v0.35.0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.22.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index de1f50ff..e586535a 100644 --- a/go.sum +++ b/go.sum @@ -32,15 +32,15 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= -golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From 4177ab4086a97648553f34bcff2ff81a137d31f3 Mon Sep 17 00:00:00 2001 From: vinicius Date: Fri, 7 Mar 2025 04:23:24 -0300 Subject: [PATCH 28/53] =?UTF-8?q?=F0=9F=94=A5=20feat:=20Add=20support=20fo?= =?UTF-8?q?r=20context.Context=20in=20keyauth=20middleware=20(#3287)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(middleware): add support to context.Context in keyauth middleware pretty straightforward option to use context.Context instead of just fiber.Ctx, tests added accordingly. * fix(middleware): include import that was missing from previous commit * fix(middleware): include missing import * Replace logger with panic * Update keyauth_test.go * Update keyauth_test.go --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> --- middleware/keyauth/keyauth.go | 22 ++++++-- middleware/keyauth/keyauth_test.go | 82 +++++++++++++++++++++--------- 2 files changed, 75 insertions(+), 29 deletions(-) diff --git a/middleware/keyauth/keyauth.go b/middleware/keyauth/keyauth.go index e245ba42..54ecdbe5 100644 --- a/middleware/keyauth/keyauth.go +++ b/middleware/keyauth/keyauth.go @@ -2,6 +2,7 @@ package keyauth import ( + "context" "errors" "fmt" "net/url" @@ -59,7 +60,10 @@ func New(config ...Config) fiber.Handler { valid, err := cfg.Validator(c, key) if err == nil && valid { + // Store in both Locals and Context c.Locals(tokenKey, key) + ctx := context.WithValue(c.Context(), tokenKey, key) + c.SetContext(ctx) return cfg.SuccessHandler(c) } return cfg.ErrorHandler(c, err) @@ -68,12 +72,20 @@ func New(config ...Config) fiber.Handler { // TokenFromContext returns the bearer token from the request context. // returns an empty string if the token does not exist -func TokenFromContext(c fiber.Ctx) string { - token, ok := c.Locals(tokenKey).(string) - if !ok { - return "" +func TokenFromContext(c any) string { + switch ctx := c.(type) { + case context.Context: + if token, ok := ctx.Value(tokenKey).(string); ok { + return token + } + case fiber.Ctx: + if token, ok := ctx.Locals(tokenKey).(string); ok { + return token + } + default: + panic("unsupported context type, expected fiber.Ctx or context.Context") } - return token + return "" } // MultipleKeySourceLookup creates a CustomKeyLookup function that checks multiple sources until one is found diff --git a/middleware/keyauth/keyauth_test.go b/middleware/keyauth/keyauth_test.go index 72c9d3c1..27c4e5a0 100644 --- a/middleware/keyauth/keyauth_test.go +++ b/middleware/keyauth/keyauth_test.go @@ -503,33 +503,67 @@ func Test_TokenFromContext_None(t *testing.T) { } func Test_TokenFromContext(t *testing.T) { - app := fiber.New() - // Wire up keyauth middleware to set TokenFromContext now - app.Use(New(Config{ - KeyLookup: "header:Authorization", - AuthScheme: "Basic", - Validator: func(_ fiber.Ctx, key string) (bool, error) { - if key == CorrectKey { - return true, nil - } - return false, ErrMissingOrMalformedAPIKey - }, - })) - // Define a test handler that checks TokenFromContext - app.Get("/", func(c fiber.Ctx) error { - return c.SendString(TokenFromContext(c)) + // Test that TokenFromContext returns the correct token + t.Run("fiber.Ctx", func(t *testing.T) { + app := fiber.New() + app.Use(New(Config{ + KeyLookup: "header:Authorization", + AuthScheme: "Basic", + Validator: func(_ fiber.Ctx, key string) (bool, error) { + if key == CorrectKey { + return true, nil + } + return false, ErrMissingOrMalformedAPIKey + }, + })) + app.Get("/", func(c fiber.Ctx) error { + return c.SendString(TokenFromContext(c)) + }) + + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + req.Header.Add("Authorization", "Basic "+CorrectKey) + res, err := app.Test(req) + require.NoError(t, err) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, CorrectKey, string(body)) }) - req := httptest.NewRequest(fiber.MethodGet, "/", nil) - req.Header.Add("Authorization", "Basic "+CorrectKey) - // Send - res, err := app.Test(req) - require.NoError(t, err) + t.Run("context.Context", func(t *testing.T) { + app := fiber.New() + app.Use(New(Config{ + KeyLookup: "header:Authorization", + AuthScheme: "Basic", + Validator: func(_ fiber.Ctx, key string) (bool, error) { + if key == CorrectKey { + return true, nil + } + return false, ErrMissingOrMalformedAPIKey + }, + })) + // Verify that TokenFromContext works with context.Context + app.Get("/", func(c fiber.Ctx) error { + ctx := c.Context() + token := TokenFromContext(ctx) + return c.SendString(token) + }) - // Read the response body into a string - body, err := io.ReadAll(res.Body) - require.NoError(t, err) - require.Equal(t, CorrectKey, string(body)) + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + req.Header.Add("Authorization", "Basic "+CorrectKey) + res, err := app.Test(req) + require.NoError(t, err) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, CorrectKey, string(body)) + }) + + t.Run("invalid context type", func(t *testing.T) { + require.Panics(t, func() { + _ = TokenFromContext("invalid") + }) + }) } func Test_AuthSchemeToken(t *testing.T) { From 600ebd95ce741474724de93ea4ca920678f60e54 Mon Sep 17 00:00:00 2001 From: Kashiwa <13825170+ksw2000@users.noreply.github.com> Date: Fri, 7 Mar 2025 22:33:22 +0800 Subject: [PATCH 29/53] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20replace?= =?UTF-8?q?=20isInCharset=20with=20bytes.IndexByte=20(#3342)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ctx.go | 2 +- path.go | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/ctx.go b/ctx.go index 07ffb1db..b2d13c0a 100644 --- a/ctx.go +++ b/ctx.go @@ -1349,7 +1349,7 @@ func (c *DefaultCtx) getLocationFromRoute(route Route, params Map) (string, erro for key, val := range params { isSame := key == segment.ParamName || (!c.app.config.CaseSensitive && utils.EqualFold(key, segment.ParamName)) - isGreedy := segment.IsGreedy && len(key) == 1 && isInCharset(key[0], greedyParameters) + isGreedy := segment.IsGreedy && len(key) == 1 && bytes.IndexByte(greedyParameters, key[0]) != -1 if isSame || isGreedy { _, err := buf.WriteString(utils.ToString(val)) if err != nil { diff --git a/path.go b/path.go index fdd61e39..76236e40 100644 --- a/path.go +++ b/path.go @@ -7,6 +7,7 @@ package fiber import ( + "bytes" "regexp" "strconv" "strings" @@ -308,7 +309,7 @@ func (routeParser *routeParser) analyseParameterPart(pattern string, customConst parameterEndPosition = 0 case parameterEndPosition == -1: parameterEndPosition = len(pattern) - 1 - case !isInCharset(pattern[parameterEndPosition+1], parameterDelimiterChars): + case bytes.IndexByte(parameterDelimiterChars, pattern[parameterEndPosition+1]) == -1: parameterEndPosition++ } @@ -397,16 +398,6 @@ func (routeParser *routeParser) analyseParameterPart(pattern string, customConst return n, segment } -// isInCharset check is the given character in the charset list -func isInCharset(searchChar byte, charset []byte) bool { - for _, char := range charset { - if char == searchChar { - return true - } - } - return false -} - // findNextCharsetPosition search the next char position from the charset func findNextCharsetPosition(search string, charset []byte) int { nextPosition := -1 From 1b26cf6b5eefb75899cbe4b97fd0c048eded6591 Mon Sep 17 00:00:00 2001 From: Kashiwa <13825170+ksw2000@users.noreply.github.com> Date: Mon, 10 Mar 2025 16:04:04 +0800 Subject: [PATCH 30/53] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20optimize?= =?UTF-8?q?=20routeParser=20by=20using=20sync.Pool=20(#3343)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ Refactor: add routerParser pool ``` goos: linux goarch: amd64 pkg: github.com/gofiber/fiber/v3 cpu: AMD EPYC 9J14 96-Core Processor │ ori.txt │ pool.txt │ │ sec/op │ sec/op vs base │ _RoutePatternMatch//api/v1/const_|_match_|_/api/v1/const-16 173.9n ± 0% 159.3n ± 1% -8.37% (p=0.000 n=20) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1-16 163.9n ± 0% 150.9n ± 0% -7.90% (p=0.000 n=20) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/-16 165.4n ± 1% 150.6n ± 1% -8.95% (p=0.000 n=20) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/something-16 174.9n ± 0% 160.6n ± 0% -8.15% (p=0.000 n=20) _RoutePatternMatch//api/:param/fixedEnd_|_match_|_/api/abc/fixedEnd-16 520.2n ± 0% 438.1n ± 1% -15.78% (p=0.000 n=20) _RoutePatternMatch//api/:param/fixedEnd_|_not_match_|_/api/abc/def/fixedEnd-16 521.8n ± 0% 436.8n ± 0% -16.29% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity-16 630.0n ± 0% 525.0n ± 0% -16.67% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/-16 633.3n ± 0% 526.4n ± 0% -16.89% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/1-16 627.8n ± 0% 527.5n ± 0% -15.97% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v-16 602.1n ± 0% 501.9n ± 0% -16.65% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v2-16 604.9n ± 0% 504.3n ± 0% -16.62% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v1/-16 616.7n ± 0% 512.8n ± 1% -16.86% (p=0.000 n=20) geomean 390.5n 336.5n -13.84% │ ori.txt │ pool.txt │ │ B/op │ B/op vs base │ _RoutePatternMatch//api/v1/const_|_match_|_/api/v1/const-16 152.0 ± 0% 144.0 ± 0% -5.26% (p=0.000 n=20) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1-16 144.0 ± 0% 136.0 ± 0% -5.56% (p=0.000 n=20) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/-16 144.0 ± 0% 136.0 ± 0% -5.56% (p=0.000 n=20) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/something-16 160.0 ± 0% 152.0 ± 0% -5.00% (p=0.000 n=20) _RoutePatternMatch//api/:param/fixedEnd_|_match_|_/api/abc/fixedEnd-16 440.0 ± 0% 368.0 ± 0% -16.36% (p=0.000 n=20) _RoutePatternMatch//api/:param/fixedEnd_|_not_match_|_/api/abc/def/fixedEnd-16 440.0 ± 0% 368.0 ± 0% -16.36% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity-16 536.0 ± 0% 432.0 ± 0% -19.40% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/-16 536.0 ± 0% 432.0 ± 0% -19.40% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/1-16 536.0 ± 0% 432.0 ± 0% -19.40% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v-16 528.0 ± 0% 424.0 ± 0% -19.70% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v2-16 528.0 ± 0% 424.0 ± 0% -19.70% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v1/-16 528.0 ± 0% 424.0 ± 0% -19.70% (p=0.000 n=20) geomean 337.9 288.8 -14.52% │ ori.txt │ pool.txt │ │ allocs/op │ allocs/op vs base │ _RoutePatternMatch//api/v1/const_|_match_|_/api/v1/const-16 5.000 ± 0% 4.000 ± 0% -20.00% (p=0.000 n=20) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1-16 5.000 ± 0% 4.000 ± 0% -20.00% (p=0.000 n=20) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/-16 5.000 ± 0% 4.000 ± 0% -20.00% (p=0.000 n=20) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/something-16 5.000 ± 0% 4.000 ± 0% -20.00% (p=0.000 n=20) _RoutePatternMatch//api/:param/fixedEnd_|_match_|_/api/abc/fixedEnd-16 13.000 ± 0% 9.000 ± 0% -30.77% (p=0.000 n=20) _RoutePatternMatch//api/:param/fixedEnd_|_not_match_|_/api/abc/def/fixedEnd-16 13.000 ± 0% 9.000 ± 0% -30.77% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity-16 14.000 ± 0% 9.000 ± 0% -35.71% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/-16 14.000 ± 0% 9.000 ± 0% -35.71% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/1-16 14.000 ± 0% 9.000 ± 0% -35.71% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v-16 14.000 ± 0% 9.000 ± 0% -35.71% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v2-16 14.000 ± 0% 9.000 ± 0% -35.71% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v1/-16 14.000 ± 0% 9.000 ± 0% -35.71% (p=0.000 n=20) geomean 9.811 6.868 -29.99% ``` * 🩹 Fix: golangci-lint problem --- path.go | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/path.go b/path.go index 76236e40..3876943d 100644 --- a/path.go +++ b/path.go @@ -11,6 +11,7 @@ import ( "regexp" "strconv" "strings" + "sync" "time" "unicode" @@ -26,6 +27,12 @@ type routeParser struct { plusCount int // number of plus parameters, used internally to give the plus parameter its number } +var routerParserPool = &sync.Pool{ + New: func() any { + return &routeParser{} + }, +} + // routeSegment holds the segment metadata type routeSegment struct { // const information @@ -163,7 +170,10 @@ func RoutePatternMatch(path, pattern string, cfg ...Config) bool { patternPretty = utils.TrimRight(patternPretty, '/') } - parser := parseRoute(string(patternPretty)) + parser, _ := routerParserPool.Get().(*routeParser) //nolint:errcheck // only contains routeParser + parser.reset() + parser.parseRoute(string(patternPretty)) + defer routerParserPool.Put(parser) if string(patternPretty) == "/" && path == "/" { return true @@ -184,10 +194,16 @@ func RoutePatternMatch(path, pattern string, cfg ...Config) bool { return string(patternPretty) == path } +func (parser *routeParser) reset() { + parser.segs = parser.segs[:0] + parser.params = parser.params[:0] + parser.wildCardCount = 0 + parser.plusCount = 0 +} + // parseRoute analyzes the route and divides it into segments for constant areas and parameters, // this information is needed later when assigning the requests to the declared routes -func parseRoute(pattern string, customConstraints ...CustomConstraint) routeParser { - parser := routeParser{} +func (parser *routeParser) parseRoute(pattern string, customConstraints ...CustomConstraint) { var n int var seg *routeSegment for len(pattern) > 0 { @@ -207,7 +223,13 @@ func parseRoute(pattern string, customConstraints ...CustomConstraint) routePars parser.segs[len(parser.segs)-1].IsLast = true } parser.segs = addParameterMetaInfo(parser.segs) +} +// parseRoute analyzes the route and divides it into segments for constant areas and parameters, +// this information is needed later when assigning the requests to the declared routes +func parseRoute(pattern string, customConstraints ...CustomConstraint) routeParser { + parser := routeParser{} + parser.parseRoute(pattern, customConstraints...) return parser } @@ -290,7 +312,7 @@ func (*routeParser) analyseConstantPart(pattern string, nextParamPosition int) ( } // analyseParameterPart find the parameter end and create the route segment -func (routeParser *routeParser) analyseParameterPart(pattern string, customConstraints ...CustomConstraint) (int, *routeSegment) { +func (parser *routeParser) analyseParameterPart(pattern string, customConstraints ...CustomConstraint) (int, *routeSegment) { isWildCard := pattern[0] == wildcardParam isPlusParam := pattern[0] == plusParam @@ -377,11 +399,11 @@ func (routeParser *routeParser) analyseParameterPart(pattern string, customConst // add access iterator to wildcard and plus if isWildCard { - routeParser.wildCardCount++ - paramName += strconv.Itoa(routeParser.wildCardCount) + parser.wildCardCount++ + paramName += strconv.Itoa(parser.wildCardCount) } else if isPlusParam { - routeParser.plusCount++ - paramName += strconv.Itoa(routeParser.plusCount) + parser.plusCount++ + paramName += strconv.Itoa(parser.plusCount) } segment := &routeSegment{ @@ -465,9 +487,9 @@ func splitNonEscaped(s, sep string) []string { } // getMatch parses the passed url and tries to match it against the route segments and determine the parameter positions -func (routeParser *routeParser) getMatch(detectionPath, path string, params *[maxParams]string, partialCheck bool) bool { //nolint: revive // Accepting a bool param is fine here +func (parser *routeParser) getMatch(detectionPath, path string, params *[maxParams]string, partialCheck bool) bool { //nolint: revive // Accepting a bool param is fine here var i, paramsIterator, partLen int - for _, segment := range routeParser.segs { + for _, segment := range parser.segs { partLen = len(detectionPath) // check const segment if !segment.IsParam { From c0599ee1d427d7dfa6241150109267b1d61a2cf4 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Mon, 10 Mar 2025 16:06:11 +0800 Subject: [PATCH 31/53] =?UTF-8?q?=F0=9F=94=A5=20feat:=20Add=20Skip=20funct?= =?UTF-8?q?ion=20to=20logger=20middleware=20(#3333)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔥 Feature(logger): Add Filter option to logger middleware * 📚 Doc(logger): Clarify Filter middleware description * 🚨 Test(logger): Enhance logger filter test with parallel subtests * 🔒 Test(logger): Add mutex to prevent race conditions in logger test * 🔥 Feature(logger): Add Filter option to logger middleware * 📚 Doc(logger): Clarify Filter middleware description * 🚨 Test(logger): Enhance logger filter test with parallel subtests * 🔒 Test(logger): Add mutex to prevent race conditions in logger test * 🚨 Test(logger): Refactor logger test to improve test isolation * Fix issue with unit-tests * Update middleware/logger/logger_test.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Apply logger filter as soon as possible * 📚 Doc: Add logger filter configuration example to whats_new.md * 📚 Doc: Update logger filter documentation in whats_new.md * 📚 Doc: Update logger filter documentation and examples * 🩹 Fix: improve what_new.md * Update logic for Filter() in Logger middleware. Add more unit-tests * Rename fields to match expressjs/morgan * Update middleware/logger/default_logger.go --------- Co-authored-by: Juan Calderon-Perez Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: RW --- docs/middleware/logger.md | 25 ++-- docs/whats_new.md | 25 ++++ middleware/logger/config.go | 21 ++- middleware/logger/default_logger.go | 14 +- middleware/logger/logger_test.go | 221 +++++++++++++++++++++++----- 5 files changed, 243 insertions(+), 63 deletions(-) diff --git a/docs/middleware/logger.md b/docs/middleware/logger.md index 07ff07c4..af16f384 100644 --- a/docs/middleware/logger.md +++ b/docs/middleware/logger.md @@ -55,13 +55,13 @@ app.Use(logger.New(logger.Config{ })) // Custom File Writer -file, err := os.OpenFile("./123.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) +accessLog, err := os.OpenFile("./access.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { - log.Fatalf("error opening file: %v", err) + log.Fatalf("error opening access.log file: %v", err) } -defer file.Close() +defer accessLog.Close() app.Use(logger.New(logger.Config{ - Output: file, + Stream: accessLog, })) // Add Custom Tags @@ -115,7 +115,7 @@ func main() { // Use the logger middleware with zerolog logger app.Use(logger.New(logger.Config{ - Output: logger.LoggerToWriter(zap, log.LevelDebug), + Stream: logger.LoggerToWriter(zap, log.LevelDebug), })) // Define a route @@ -129,7 +129,7 @@ func main() { ``` :::tip -Writing to os.File is goroutine-safe, but if you are using a custom Output that is not goroutine-safe, make sure to implement locking to properly serialize writes. +Writing to os.File is goroutine-safe, but if you are using a custom Stream that is not goroutine-safe, make sure to implement locking to properly serialize writes. ::: ## Config @@ -138,31 +138,30 @@ Writing to os.File is goroutine-safe, but if you are using a custom Output that | Property | Type | Description | Default | |:-----------------|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------| -| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | -| Done | `func(fiber.Ctx, []byte)` | Done is a function that is called after the log string for a request is written to Output, and pass the log string as parameter. | `nil` | +| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| Skip | `func(fiber.Ctx) bool` | Skip is a function to determine if logging is skipped or written to Stream. | `nil` | +| Done | `func(fiber.Ctx, []byte)` | Done is a function that is called after the log string for a request is written to Stream, and pass the log string as parameter. | `nil` | | CustomTags | `map[string]LogFunc` | tagFunctions defines the custom tag action. | `map[string]LogFunc` | | Format | `string` | Format defines the logging tags. | `[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n` | | TimeFormat | `string` | TimeFormat defines the time format for log timestamps. | `15:04:05` | | TimeZone | `string` | TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc | `"Local"` | | TimeInterval | `time.Duration` | TimeInterval is the delay before the timestamp is updated. | `500 * time.Millisecond` | -| Output | `io.Writer` | Output is a writer where logs are written. | `os.Stdout` | +| Stream | `io.Writer` | Stream is a writer where logs are written. | `os.Stdout` | | LoggerFunc | `func(c fiber.Ctx, data *Data, cfg Config) error` | Custom logger function for integration with logging libraries (Zerolog, Zap, Logrus, etc). Defaults to Fiber's default logger if not defined. | `see default_logger.go defaultLoggerInstance` | | DisableColors | `bool` | DisableColors defines if the logs output should be colorized. | `false` | -| enableColors | `bool` | Internal field for enabling colors in the log output. (This is not a user-configurable field) | - | -| enableLatency | `bool` | Internal field for enabling latency measurement in logs. (This is not a user-configurable field) | - | -| timeZoneLocation | `*time.Location` | Internal field for the time zone location. (This is not a user-configurable field) | - | ## Default Config ```go var ConfigDefault = Config{ Next: nil, + Skip nil, Done: nil, Format: "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n", TimeFormat: "15:04:05", TimeZone: "Local", TimeInterval: 500 * time.Millisecond, - Output: os.Stdout, + Stream: os.Stdout, DisableColors: false, LoggerFunc: defaultLoggerInstance, } diff --git a/docs/whats_new.md b/docs/whats_new.md index 5c3dd6ac..8528dc41 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -912,6 +912,31 @@ func main() {
+The `Skip` is a function to determine if logging is skipped or written to `Stream`. + +
+Example Usage + +```go +app.Use(logger.New(logger.Config{ + Skip: func(c fiber.Ctx) bool { + // Skip logging HTTP 200 requests + return c.Response().StatusCode() == fiber.StatusOK + }, +})) +``` + +```go +app.Use(logger.New(logger.Config{ + Skip: func(c fiber.Ctx) bool { + // Only log errors, similar to an error.log + return c.Response().StatusCode() < 400 + }, +})) +``` + +
+ ### Filesystem We've decided to remove filesystem middleware to clear up the confusion between static and filesystem middleware. diff --git a/middleware/logger/config.go b/middleware/logger/config.go index 4826151e..2df814eb 100644 --- a/middleware/logger/config.go +++ b/middleware/logger/config.go @@ -10,16 +10,21 @@ import ( // Config defines the config for middleware. type Config struct { - // Output is a writer where logs are written + // Stream is a writer where logs are written // // Default: os.Stdout - Output io.Writer + Stream io.Writer // Next defines a function to skip this middleware when returned true. // // Optional. Default: nil Next func(c fiber.Ctx) bool + // Skip is a function to determine if logging is skipped or written to Stream. + // + // Optional. Default: nil + Skip func(c fiber.Ctx) bool + // Done is a function that is called after the log string for a request is written to Output, // and pass the log string as parameter. // @@ -98,12 +103,13 @@ type LogFunc func(output Buffer, c fiber.Ctx, data *Data, extraParam string) (in // ConfigDefault is the default config var ConfigDefault = Config{ Next: nil, + Skip: nil, Done: nil, Format: defaultFormat, TimeFormat: "15:04:05", TimeZone: "Local", TimeInterval: 500 * time.Millisecond, - Output: os.Stdout, + Stream: os.Stdout, BeforeHandlerFunc: beforeHandlerFunc, LoggerFunc: defaultLoggerInstance, enableColors: true, @@ -126,6 +132,9 @@ func configDefault(config ...Config) Config { if cfg.Next == nil { cfg.Next = ConfigDefault.Next } + if cfg.Skip == nil { + cfg.Skip = ConfigDefault.Skip + } if cfg.Done == nil { cfg.Done = ConfigDefault.Done } @@ -141,8 +150,8 @@ func configDefault(config ...Config) Config { if int(cfg.TimeInterval) <= 0 { cfg.TimeInterval = ConfigDefault.TimeInterval } - if cfg.Output == nil { - cfg.Output = ConfigDefault.Output + if cfg.Stream == nil { + cfg.Stream = ConfigDefault.Stream } if cfg.BeforeHandlerFunc == nil { @@ -154,7 +163,7 @@ func configDefault(config ...Config) Config { } // Enable colors if no custom format or output is given - if !cfg.DisableColors && cfg.Output == ConfigDefault.Output { + if !cfg.DisableColors && cfg.Stream == ConfigDefault.Stream { cfg.enableColors = true } diff --git a/middleware/logger/default_logger.go b/middleware/logger/default_logger.go index 369b2c85..e4de79bf 100644 --- a/middleware/logger/default_logger.go +++ b/middleware/logger/default_logger.go @@ -15,6 +15,12 @@ import ( // default logger for fiber func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error { + // Check if Skip is defined and call it. + // Now, if Skip(c) == true, we SKIP logging: + if cfg.Skip != nil && cfg.Skip(c) { + return nil // Skip logging if Skip returns true + } + // Alias colors colors := c.App().Config().ColorScheme @@ -91,7 +97,7 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error { } // Write buffer to output - writeLog(cfg.Output, buf.Bytes()) + writeLog(cfg.Stream, buf.Bytes()) if cfg.Done != nil { cfg.Done(c, buf.Bytes()) @@ -125,7 +131,7 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error { buf.WriteString(err.Error()) } - writeLog(cfg.Output, buf.Bytes()) + writeLog(cfg.Stream, buf.Bytes()) if cfg.Done != nil { cfg.Done(c, buf.Bytes()) @@ -141,9 +147,9 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error { func beforeHandlerFunc(cfg Config) { // If colors are enabled, check terminal compatibility if cfg.enableColors { - cfg.Output = colorable.NewColorableStdout() + cfg.Stream = colorable.NewColorableStdout() if os.Getenv("TERM") == "dumb" || os.Getenv("NO_COLOR") == "1" || (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) { - cfg.Output = colorable.NewNonColorable(os.Stdout) + cfg.Stream = colorable.NewNonColorable(os.Stdout) } } } diff --git a/middleware/logger/logger_test.go b/middleware/logger/logger_test.go index d459f22c..011a0ead 100644 --- a/middleware/logger/logger_test.go +++ b/middleware/logger/logger_test.go @@ -71,7 +71,7 @@ func Test_Logger(t *testing.T) { app.Use(New(Config{ Format: "${error}", - Output: buf, + Stream: buf, })) app.Get("/", func(_ fiber.Ctx) error { @@ -94,7 +94,7 @@ func Test_Logger_locals(t *testing.T) { app.Use(New(Config{ Format: "${locals:demo}", - Output: buf, + Stream: buf, })) app.Get("/", func(c fiber.Ctx) error { @@ -171,6 +171,147 @@ func Test_Logger_Done(t *testing.T) { require.Positive(t, buf.Len(), 0) } +// Test_Logger_Filter tests the Filter functionality of the logger middleware. +// It verifies that logs are written or skipped based on the filter condition. +func Test_Logger_Filter(t *testing.T) { + t.Parallel() + + t.Run("Test Not Found", func(t *testing.T) { + t.Parallel() + app := fiber.New() + + logOutput := bytes.Buffer{} + + // Return true to skip logging for all requests != 404 + app.Use(New(Config{ + Skip: func(c fiber.Ctx) bool { + return c.Response().StatusCode() != fiber.StatusNotFound + }, + Stream: &logOutput, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/nonexistent", nil)) + require.NoError(t, err) + require.Equal(t, fiber.StatusNotFound, resp.StatusCode) + + // Expect logs for the 404 request + require.Contains(t, logOutput.String(), "404") + }) + + t.Run("Test OK", func(t *testing.T) { + t.Parallel() + app := fiber.New() + + logOutput := bytes.Buffer{} + + // Return true to skip logging for all requests == 200 + app.Use(New(Config{ + Skip: func(c fiber.Ctx) bool { + return c.Response().StatusCode() == fiber.StatusOK + }, + Stream: &logOutput, + })) + + app.Get("/", func(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + + // We skip logging for status == 200, so "200" should not appear + require.NotContains(t, logOutput.String(), "200") + }) + + t.Run("Always Skip", func(t *testing.T) { + t.Parallel() + app := fiber.New() + + logOutput := bytes.Buffer{} + + // Filter always returns true => skip all logs + app.Use(New(Config{ + Skip: func(_ fiber.Ctx) bool { + return true // always skip + }, + Stream: &logOutput, + })) + + app.Get("/something", func(c fiber.Ctx) error { + return c.Status(fiber.StatusTeapot).SendString("I'm a teapot") + }) + + _, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/something", nil)) + require.NoError(t, err) + + // Expect NO logs + require.Empty(t, logOutput.String()) + }) + + t.Run("Never Skip", func(t *testing.T) { + t.Parallel() + app := fiber.New() + + logOutput := bytes.Buffer{} + + // Filter always returns false => never skip logs + app.Use(New(Config{ + Skip: func(_ fiber.Ctx) bool { + return false // never skip + }, + Stream: &logOutput, + })) + + app.Get("/always", func(c fiber.Ctx) error { + return c.Status(fiber.StatusTeapot).SendString("Teapot again") + }) + + _, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/always", nil)) + require.NoError(t, err) + + // Expect some logging - check any substring + require.Contains(t, logOutput.String(), strconv.Itoa(fiber.StatusTeapot)) + }) + + t.Run("Skip /healthz", func(t *testing.T) { + t.Parallel() + app := fiber.New() + + logOutput := bytes.Buffer{} + + // Filter returns true (skip logs) if the request path is /healthz + app.Use(New(Config{ + Skip: func(c fiber.Ctx) bool { + return c.Path() == "/healthz" + }, + Stream: &logOutput, + })) + + // Normal route + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("Hello World!") + }) + // Health route + app.Get("/healthz", func(c fiber.Ctx) error { + return c.SendString("OK") + }) + + // Request to "/" -> should be logged + _, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + require.NoError(t, err) + require.Contains(t, logOutput.String(), "200") + + // Reset output buffer + logOutput.Reset() + + // Request to "/healthz" -> should be skipped + _, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/healthz", nil)) + require.NoError(t, err) + require.Empty(t, logOutput.String()) + }) +} + // go test -run Test_Logger_ErrorTimeZone func Test_Logger_ErrorTimeZone(t *testing.T) { t.Parallel() @@ -234,7 +375,7 @@ func Test_Logger_LoggerToWriter(t *testing.T) { app.Use("/"+level, New(Config{ Format: "${error}", - Output: LoggerToWriter(logger, tc. + Stream: LoggerToWriter(logger, tc. level), })) @@ -276,7 +417,7 @@ func Test_Logger_ErrorOutput_WithoutColor(t *testing.T) { app := fiber.New() app.Use(New(Config{ - Output: o, + Stream: o, DisableColors: true, })) @@ -293,7 +434,7 @@ func Test_Logger_ErrorOutput(t *testing.T) { app := fiber.New() app.Use(New(Config{ - Output: o, + Stream: o, })) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) @@ -312,7 +453,7 @@ func Test_Logger_All(t *testing.T) { app.Use(New(Config{ Format: "${pid}${reqHeaders}${referer}${scheme}${protocol}${ip}${ips}${host}${url}${ua}${body}${route}${black}${red}${green}${yellow}${blue}${magenta}${cyan}${white}${reset}${error}${reqHeader:test}${query:test}${form:test}${cookie:test}${non}", - Output: buf, + Stream: buf, })) // Alias colors @@ -358,7 +499,7 @@ func Test_Logger_WithLatency(t *testing.T) { app := fiber.New() logger := New(Config{ - Output: buff, + Stream: buff, Format: "${latency}", }) app.Use(logger) @@ -403,7 +544,7 @@ func Test_Logger_WithLatency_DefaultFormat(t *testing.T) { app := fiber.New() logger := New(Config{ - Output: buff, + Stream: buff, }) app.Use(logger) @@ -453,7 +594,7 @@ func Test_Query_Params(t *testing.T) { app.Use(New(Config{ Format: "${queryParams}", - Output: buf, + Stream: buf, })) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?foo=bar&baz=moz", nil)) @@ -474,7 +615,7 @@ func Test_Response_Body(t *testing.T) { app.Use(New(Config{ Format: "${resBody}", - Output: buf, + Stream: buf, })) app.Get("/", func(c fiber.Ctx) error { @@ -508,7 +649,7 @@ func Test_Request_Body(t *testing.T) { app.Use(New(Config{ Format: "${bytesReceived} ${bytesSent} ${status}", - Output: buf, + Stream: buf, })) app.Post("/", func(c fiber.Ctx) error { @@ -536,7 +677,7 @@ func Test_Logger_AppendUint(t *testing.T) { app.Use(New(Config{ Format: "${bytesReceived} ${bytesSent} ${status}", - Output: buf, + Stream: buf, })) app.Get("/", func(c fiber.Ctx) error { @@ -611,7 +752,7 @@ func Test_Response_Header(t *testing.T) { })) app.Use(New(Config{ Format: "${respHeader:X-Request-ID}", - Output: buf, + Stream: buf, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello fiber!") @@ -634,7 +775,7 @@ func Test_Req_Header(t *testing.T) { app.Use(New(Config{ Format: "${reqHeader:test}", - Output: buf, + Stream: buf, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello fiber!") @@ -658,7 +799,7 @@ func Test_ReqHeader_Header(t *testing.T) { app.Use(New(Config{ Format: "${reqHeader:test}", - Output: buf, + Stream: buf, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello fiber!") @@ -689,7 +830,7 @@ func Test_CustomTags(t *testing.T) { return output.WriteString(customTag) }, }, - Output: buf, + Stream: buf, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello fiber!") @@ -713,7 +854,7 @@ func Test_Logger_ByteSent_Streaming(t *testing.T) { app.Use(New(Config{ Format: "${bytesReceived} ${bytesSent} ${status}", - Output: buf, + Stream: buf, })) app.Get("/", func(c fiber.Ctx) error { @@ -759,7 +900,7 @@ func Test_Logger_EnableColors(t *testing.T) { app := fiber.New() app.Use(New(Config{ - Output: o, + Stream: o, })) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) @@ -782,7 +923,7 @@ func Benchmark_Logger(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${bytesReceived} ${bytesSent} ${status}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { c.Set("test", "test") @@ -794,7 +935,7 @@ func Benchmark_Logger(b *testing.B) { b.Run("DefaultFormat", func(bb *testing.B) { app := fiber.New() app.Use(New(Config{ - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello, World!") @@ -805,7 +946,7 @@ func Benchmark_Logger(b *testing.B) { b.Run("DefaultFormatDisableColors", func(bb *testing.B) { app := fiber.New() app.Use(New(Config{ - Output: io.Discard, + Stream: io.Discard, DisableColors: true, })) app.Get("/", func(c fiber.Ctx) error { @@ -819,7 +960,7 @@ func Benchmark_Logger(b *testing.B) { logger := fiberlog.DefaultLogger() logger.SetOutput(io.Discard) app.Use(New(Config{ - Output: LoggerToWriter(logger, fiberlog.LevelDebug), + Stream: LoggerToWriter(logger, fiberlog.LevelDebug), })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello, World!") @@ -831,7 +972,7 @@ func Benchmark_Logger(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${bytesReceived} ${bytesSent} ${status} ${reqHeader:test}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { c.Set("test", "test") @@ -844,7 +985,7 @@ func Benchmark_Logger(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${locals:demo}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { c.Locals("demo", "johndoe") @@ -857,7 +998,7 @@ func Benchmark_Logger(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${locals:demo}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/int", func(c fiber.Ctx) error { c.Locals("demo", 55) @@ -874,7 +1015,7 @@ func Benchmark_Logger(b *testing.B) { io.Discard.Write(logString) //nolint:errcheck // ignore error } }, - Output: io.Discard, + Stream: io.Discard, })) app.Get("/logging", func(ctx fiber.Ctx) error { return ctx.SendStatus(fiber.StatusOK) @@ -886,7 +1027,7 @@ func Benchmark_Logger(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${pid}${reqHeaders}${referer}${scheme}${protocol}${ip}${ips}${host}${url}${ua}${body}${route}${black}${red}${green}${yellow}${blue}${magenta}${cyan}${white}${reset}${error}${reqHeader:test}${query:test}${form:test}${cookie:test}${non}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello, World!") @@ -898,7 +1039,7 @@ func Benchmark_Logger(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${bytesReceived} ${bytesSent} ${status}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { c.Set("Connection", "keep-alive") @@ -927,7 +1068,7 @@ func Benchmark_Logger(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${resBody}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Sample response body") @@ -950,7 +1091,7 @@ func Benchmark_Logger_Parallel(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${bytesReceived} ${bytesSent} ${status}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { c.Set("test", "test") @@ -962,7 +1103,7 @@ func Benchmark_Logger_Parallel(b *testing.B) { b.Run("DefaultFormat", func(bb *testing.B) { app := fiber.New() app.Use(New(Config{ - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello, World!") @@ -975,7 +1116,7 @@ func Benchmark_Logger_Parallel(b *testing.B) { logger := fiberlog.DefaultLogger() logger.SetOutput(io.Discard) app.Use(New(Config{ - Output: LoggerToWriter(logger, fiberlog.LevelDebug), + Stream: LoggerToWriter(logger, fiberlog.LevelDebug), })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello, World!") @@ -986,7 +1127,7 @@ func Benchmark_Logger_Parallel(b *testing.B) { b.Run("DefaultFormatDisableColors", func(bb *testing.B) { app := fiber.New() app.Use(New(Config{ - Output: io.Discard, + Stream: io.Discard, DisableColors: true, })) app.Get("/", func(c fiber.Ctx) error { @@ -999,7 +1140,7 @@ func Benchmark_Logger_Parallel(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${bytesReceived} ${bytesSent} ${status} ${reqHeader:test}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { c.Set("test", "test") @@ -1012,7 +1153,7 @@ func Benchmark_Logger_Parallel(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${locals:demo}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { c.Locals("demo", "johndoe") @@ -1025,7 +1166,7 @@ func Benchmark_Logger_Parallel(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${locals:demo}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/int", func(c fiber.Ctx) error { c.Locals("demo", 55) @@ -1042,7 +1183,7 @@ func Benchmark_Logger_Parallel(b *testing.B) { io.Discard.Write(logString) //nolint:errcheck // ignore error } }, - Output: io.Discard, + Stream: io.Discard, })) app.Get("/logging", func(ctx fiber.Ctx) error { return ctx.SendStatus(fiber.StatusOK) @@ -1054,7 +1195,7 @@ func Benchmark_Logger_Parallel(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${pid}${reqHeaders}${referer}${scheme}${protocol}${ip}${ips}${host}${url}${ua}${body}${route}${black}${red}${green}${yellow}${blue}${magenta}${cyan}${white}${reset}${error}${reqHeader:test}${query:test}${form:test}${cookie:test}${non}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello, World!") @@ -1066,7 +1207,7 @@ func Benchmark_Logger_Parallel(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${bytesReceived} ${bytesSent} ${status}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { c.Set("Connection", "keep-alive") @@ -1095,7 +1236,7 @@ func Benchmark_Logger_Parallel(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${resBody}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Sample response body") From eb7b00b4fb10d780ecf37486baea1b4b98d7a3bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Thu, 13 Mar 2025 01:44:57 +0800 Subject: [PATCH 32/53] fix: default value comment --- middleware/helmet/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middleware/helmet/config.go b/middleware/helmet/config.go index c7fa3cab..c9367d81 100644 --- a/middleware/helmet/config.go +++ b/middleware/helmet/config.go @@ -28,7 +28,7 @@ type Config struct { ContentSecurityPolicy string // ReferrerPolicy - // Optional. Default value "ReferrerPolicy". + // Optional. Default value "no-referrer". ReferrerPolicy string // Permissions-Policy From b56a141d59cedd041c55cde66e696f4d8dde6bfb Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Fri, 14 Mar 2025 02:51:34 -0400 Subject: [PATCH 33/53] docs: Update helmet.md default values (#3350) Update helmet.md --- docs/middleware/helmet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/middleware/helmet.md b/docs/middleware/helmet.md index bc101350..369a4c2c 100644 --- a/docs/middleware/helmet.md +++ b/docs/middleware/helmet.md @@ -54,7 +54,7 @@ curl -I http://localhost:3000 | ContentSecurityPolicy | `string` | ContentSecurityPolicy | "" | | CSPReportOnly | `bool` | CSPReportOnly | false | | HSTSPreloadEnabled | `bool` | HSTSPreloadEnabled | false | -| ReferrerPolicy | `string` | ReferrerPolicy | "ReferrerPolicy" | +| ReferrerPolicy | `string` | ReferrerPolicy | "no-referrer" | | PermissionPolicy | `string` | Permissions-Policy | "" | | CrossOriginEmbedderPolicy | `string` | Cross-Origin-Embedder-Policy | "require-corp" | | CrossOriginOpenerPolicy | `string` | Cross-Origin-Opener-Policy | "same-origin" | From 395c8fafa96719cfcce808876c4b11914ca34efc Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Mon, 17 Mar 2025 03:29:51 -0400 Subject: [PATCH 34/53] =?UTF-8?q?=F0=9F=A7=B9=20chore:=20Fix=20linter=20wo?= =?UTF-8?q?rkflow=20failures=20(#3354)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix linter workflow failures * Bump golang.org/x/net to v0.36.0 * Try skipping golangci-lint cache * Update linter.yml * fix: directives and spaces * fix: better configuration * fix: golangci-lint install * Update golangci-lint version * Enable SA4023 * Update golangci-lint config * Remove duplicated rule --------- Co-authored-by: Fernandez Ludovic --- .github/workflows/linter.yml | 5 +- .golangci.yml | 14 +++-- Makefile | 2 +- app.go | 2 +- app_test.go | 4 +- bind_test.go | 4 +- client/helper_test.go | 2 +- client/hooks.go | 2 +- ctx_test.go | 76 +++++++++++++------------- go.mod | 2 +- go.sum | 2 + helpers.go | 8 +-- helpers_test.go | 2 +- internal/storage/memory/memory_test.go | 16 +++--- listen.go | 34 +++++------- log/default.go | 4 +- middleware/adaptor/adaptor_test.go | 4 +- middleware/logger/default_logger.go | 2 +- middleware/logger/logger_test.go | 6 +- middleware/proxy/proxy_test.go | 15 ++++- path.go | 2 +- redirect_test.go | 30 +++++----- router.go | 2 +- router_test.go | 4 +- 24 files changed, 129 insertions(+), 115 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index beed2126..2f7f8c84 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -37,4 +37,7 @@ jobs: uses: golangci/golangci-lint-action@v6 with: # NOTE: Keep this in sync with the version from .golangci.yml - version: v1.62.2 + version: v1.64.7 + # NOTE(ldez): temporary workaround + install-mode: goinstall + diff --git a/.golangci.yml b/.golangci.yml index 8b8d27b8..357676f7 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -7,7 +7,6 @@ run: output: sort-results: true - uniq-by-line: false linters-settings: depguard: @@ -187,7 +186,7 @@ linters-settings: - name: unchecked-type-assertion disabled: true # TODO: Do not disable - name: unhandled-error - arguments: ['bytes\.Buffer\.Write'] + disabled: true stylecheck: checks: @@ -250,7 +249,10 @@ issues: max-issues-per-linter: 0 max-same-issues: 0 exclude-dirs: - - internal # TODO: Do not ignore interal packages + - internal # TODO: Do not ignore internal packages + exclude-files: + - '_msgp\.go' + - '_msgp_test\.go' exclude-rules: - linters: - err113 @@ -263,7 +265,10 @@ issues: linters: - bodyclose - err113 - # fix: true + - source: 'fmt.Fprintf?' + linters: + - errcheck + - revive linters: enable: @@ -358,7 +363,6 @@ linters: - stylecheck # - tagalign # TODO: Enable - tagliatelle - - tenv - testableexamples - testifylint # - testpackage # TODO: Enable diff --git a/Makefile b/Makefile index 669b3fbe..7e67dbc2 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ markdown: ## lint: 🚨 Run lint checks .PHONY: lint lint: - go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2 run ./... + golangci-lint run ## test: 🚦 Execute all tests .PHONY: test diff --git a/app.go b/app.go index ec55f06a..84059e79 100644 --- a/app.go +++ b/app.go @@ -1024,7 +1024,7 @@ func (app *App) Test(req *http.Request, config ...TestConfig) (*http.Response, e select { case err = <-channel: case <-time.After(cfg.Timeout): - conn.Close() //nolint:errcheck, revive // It is fine to ignore the error here + conn.Close() //nolint:errcheck // It is fine to ignore the error here if cfg.FailOnTimeout { return nil, os.ErrDeadlineExceeded } diff --git a/app_test.go b/app_test.go index 97d48fce..84606e6b 100644 --- a/app_test.go +++ b/app_test.go @@ -403,7 +403,7 @@ func Test_App_serverErrorHandler_Internal_Error(t *testing.T) { t.Parallel() app := New() msg := "test err" - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed app.serverErrorHandler(c.fasthttp, errors.New(msg)) require.Equal(t, string(c.fasthttp.Response.Body()), msg) @@ -413,7 +413,7 @@ func Test_App_serverErrorHandler_Internal_Error(t *testing.T) { func Test_App_serverErrorHandler_Network_Error(t *testing.T) { t.Parallel() app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed app.serverErrorHandler(c.fasthttp, &net.DNSError{ Err: "test error", diff --git a/bind_test.go b/bind_test.go index b01086e6..89839e13 100644 --- a/bind_test.go +++ b/bind_test.go @@ -1378,7 +1378,7 @@ func Benchmark_Bind_URI(b *testing.B) { var err error app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed c.route = &Route{ Params: []string{ @@ -1415,7 +1415,7 @@ func Benchmark_Bind_URI_Map(b *testing.B) { var err error app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed c.route = &Route{ Params: []string{ diff --git a/client/helper_test.go b/client/helper_test.go index f25d0af1..1b644cbd 100644 --- a/client/helper_test.go +++ b/client/helper_test.go @@ -131,7 +131,7 @@ func testRequestFail(t *testing.T, handler fiber.Handler, wrapAgent func(agent * } } -func testClient(t *testing.T, handler fiber.Handler, wrapAgent func(agent *Client), excepted string, count ...int) { //nolint: unparam // maybe needed +func testClient(t *testing.T, handler fiber.Handler, wrapAgent func(agent *Client), excepted string, count ...int) { //nolint:unparam // maybe needed t.Helper() app, ln, start := createHelperServer(t) diff --git a/client/hooks.go b/client/hooks.go index ca6f5d6c..3d86930d 100644 --- a/client/hooks.go +++ b/client/hooks.go @@ -199,7 +199,7 @@ func parserRequestBody(c *Client, req *Request) error { case filesBody: return parserRequestBodyFile(req) case rawBody: - if body, ok := req.body.([]byte); ok { + if body, ok := req.body.([]byte); ok { //nolint:revive // ignore simplicity req.RawRequest.SetBody(body) } else { return ErrBodyType diff --git a/ctx_test.go b/ctx_test.go index ee272d24..5040f4f8 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -192,7 +192,7 @@ func Test_Ctx_AcceptsCharsets(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Ctx_AcceptsCharsets -benchmem -count=4 func Benchmark_Ctx_AcceptsCharsets(b *testing.B) { app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed c.Request().Header.Set("Accept-Charset", "utf-8, iso-8859-1;q=0.5") var res string @@ -218,7 +218,7 @@ func Test_Ctx_AcceptsEncodings(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Ctx_AcceptsEncodings -benchmem -count=4 func Benchmark_Ctx_AcceptsEncodings(b *testing.B) { app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed c.Request().Header.Set(HeaderAcceptEncoding, "deflate, gzip;q=1.0, *;q=0.5") var res string @@ -243,7 +243,7 @@ func Test_Ctx_AcceptsLanguages(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Ctx_AcceptsLanguages -benchmem -count=4 func Benchmark_Ctx_AcceptsLanguages(b *testing.B) { app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed c.Request().Header.Set(HeaderAcceptLanguage, "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5") var res string @@ -304,7 +304,7 @@ func Test_Ctx_Append(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Ctx_Append -benchmem -count=4 func Benchmark_Ctx_Append(b *testing.B) { app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed b.ReportAllocs() b.ResetTimer() @@ -337,7 +337,7 @@ func Test_Ctx_Attachment(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Ctx_Attachment -benchmem -count=4 func Benchmark_Ctx_Attachment(b *testing.B) { app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed b.ReportAllocs() b.ResetTimer() @@ -363,7 +363,7 @@ func Test_Ctx_BaseURL(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Ctx_BaseURL -benchmem func Benchmark_Ctx_BaseURL(b *testing.B) { app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed c.Request().SetHost("google.com:1337") c.Request().URI().SetPath("/haha/oke/lol") @@ -380,7 +380,7 @@ func Benchmark_Ctx_BaseURL(b *testing.B) { func Test_Ctx_Body(t *testing.T) { t.Parallel() app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed c.Request().SetBody([]byte("john=doe")) require.Equal(t, []byte("john=doe"), c.Body()) @@ -390,7 +390,7 @@ func Test_Ctx_Body(t *testing.T) { func Test_Ctx_BodyRaw(t *testing.T) { t.Parallel() app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed c.Request().SetBodyRaw([]byte("john=doe")) require.Equal(t, []byte("john=doe"), c.BodyRaw()) @@ -400,7 +400,7 @@ func Test_Ctx_BodyRaw(t *testing.T) { func Test_Ctx_BodyRaw_Immutable(t *testing.T) { t.Parallel() app := New(Config{Immutable: true}) - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed c.Request().SetBodyRaw([]byte("john=doe")) require.Equal(t, []byte("john=doe"), c.BodyRaw()) @@ -411,7 +411,7 @@ func Benchmark_Ctx_Body(b *testing.B) { const input = "john=doe" app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed c.Request().SetBody([]byte(input)) b.ReportAllocs() @@ -428,7 +428,7 @@ func Benchmark_Ctx_BodyRaw(b *testing.B) { const input = "john=doe" app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed c.Request().SetBodyRaw([]byte(input)) b.ReportAllocs() @@ -445,7 +445,7 @@ func Benchmark_Ctx_BodyRaw_Immutable(b *testing.B) { const input = "john=doe" app := New(Config{Immutable: true}) - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed c.Request().SetBodyRaw([]byte(input)) b.ReportAllocs() @@ -462,7 +462,7 @@ func Test_Ctx_Body_Immutable(t *testing.T) { t.Parallel() app := New() app.config.Immutable = true - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed c.Request().SetBody([]byte("john=doe")) require.Equal(t, []byte("john=doe"), c.Body()) @@ -474,7 +474,7 @@ func Benchmark_Ctx_Body_Immutable(b *testing.B) { app := New() app.config.Immutable = true - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed c.Request().SetBody([]byte(input)) b.ReportAllocs() @@ -527,7 +527,7 @@ func Test_Ctx_Body_With_Compression(t *testing.T) { t.Run(tCase.name, func(t *testing.T) { t.Parallel() app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed c.Request().Header.Set("Content-Encoding", tCase.contentEncoding) if strings.Contains(tCase.contentEncoding, "gzip") { @@ -720,7 +720,7 @@ func Test_Ctx_Body_With_Compression_Immutable(t *testing.T) { t.Parallel() app := New() app.config.Immutable = true - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed c.Request().Header.Set("Content-Encoding", tCase.contentEncoding) if strings.Contains(tCase.contentEncoding, "gzip") { @@ -897,7 +897,7 @@ func Test_Ctx_Context(t *testing.T) { t.Parallel() testKey := struct{}{} testValue := "Test Value" - ctx := context.WithValue(context.Background(), testKey, testValue) //nolint: staticcheck // not needed for tests + ctx := context.WithValue(context.Background(), testKey, testValue) //nolint:staticcheck // not needed for tests require.Equal(t, testValue, ctx.Value(testKey)) }) } @@ -910,7 +910,7 @@ func Test_Ctx_SetContext(t *testing.T) { testKey := struct{}{} testValue := "Test Value" - ctx := context.WithValue(context.Background(), testKey, testValue) //nolint: staticcheck // not needed for tests + ctx := context.WithValue(context.Background(), testKey, testValue) //nolint:staticcheck // not needed for tests c.SetContext(ctx) require.Equal(t, testValue, c.Context().Value(testKey)) } @@ -930,7 +930,7 @@ func Test_Ctx_Context_Multiple_Requests(t *testing.T) { } input := utils.CopyString(Query(c, "input", "NO_VALUE")) - ctx = context.WithValue(ctx, testKey, fmt.Sprintf("%s_%s", testValue, input)) //nolint: staticcheck // not needed for tests + ctx = context.WithValue(ctx, testKey, fmt.Sprintf("%s_%s", testValue, input)) //nolint:staticcheck // not needed for tests c.SetContext(ctx) return c.Status(StatusOK).SendString(fmt.Sprintf("resp_%s_returned", input)) @@ -1013,7 +1013,7 @@ func Test_Ctx_Cookie(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Ctx_Cookie -benchmem -count=4 func Benchmark_Ctx_Cookie(b *testing.B) { app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed b.ReportAllocs() b.ResetTimer() @@ -1544,12 +1544,12 @@ func Test_Ctx_Binders(t *testing.T) { t.Run("URI", func(t *testing.T) { t.Skip("URI is not ready for v3") //nolint:gocritic // TODO: uncomment - //t.Parallel() - //withValues(t, func(c Ctx, testStruct *TestStruct) error { + // t.Parallel() + // withValues(t, func(c Ctx, testStruct *TestStruct) error { // c.Route().Params = []string{"name", "name2", "class", "class2"} // c.Params().value = [30]string{"foo", "bar", "111", "222"} // return c.Bind().URI(testStruct) - //}) + // }) }) t.Run("ReqHeader", func(t *testing.T) { t.Parallel() @@ -2566,7 +2566,7 @@ func Test_Ctx_Params_Case_Sensitive(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Ctx_Params -benchmem -count=4 func Benchmark_Ctx_Params(b *testing.B) { app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed c.route = &Route{ Params: []string{ @@ -3746,7 +3746,7 @@ func Benchmark_Ctx_CBOR(b *testing.B) { func Benchmark_Ctx_JSON_Ctype(b *testing.B) { app := New() // TODO: Check extra allocs because of the interface stuff - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed type SomeStruct struct { Name string Age uint8 @@ -3813,7 +3813,7 @@ func Test_Ctx_JSONP(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Ctx_JSONP -benchmem -count=4 func Benchmark_Ctx_JSONP(b *testing.B) { app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed type SomeStruct struct { Name string @@ -3838,7 +3838,7 @@ func Benchmark_Ctx_JSONP(b *testing.B) { func Test_Ctx_XML(t *testing.T) { t.Parallel() app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed require.Error(t, c.JSON(complex(1, 1))) @@ -3897,7 +3897,7 @@ func Test_Ctx_XML(t *testing.T) { // go test -run=^$ -bench=Benchmark_Ctx_XML -benchmem -count=4 func Benchmark_Ctx_XML(b *testing.B) { app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed type SomeStruct struct { Name string `xml:"Name"` Age uint8 `xml:"Age"` @@ -3936,7 +3936,7 @@ func Test_Ctx_Links(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Ctx_Links -benchmem -count=4 func Benchmark_Ctx_Links(b *testing.B) { app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed b.ReportAllocs() b.ResetTimer() @@ -4363,7 +4363,7 @@ func Benchmark_Ctx_Render_Engine(b *testing.B) { // go test -v -run=^$ -bench=Benchmark_Ctx_Get_Location_From_Route -benchmem -count=4 func Benchmark_Ctx_Get_Location_From_Route(b *testing.B) { app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed app.Get("/user/:name", func(c Ctx) error { return c.SendString(c.Params("name")) @@ -4578,14 +4578,14 @@ func Test_Ctx_SendStreamWriter(t *testing.T) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) err := c.SendStreamWriter(func(w *bufio.Writer) { - w.WriteString("Don't crash please") //nolint:errcheck, revive // It is fine to ignore the error + w.WriteString("Don't crash please") //nolint:errcheck // It is fine to ignore the error }) require.NoError(t, err) require.Equal(t, "Don't crash please", string(c.Response().Body())) err = c.SendStreamWriter(func(w *bufio.Writer) { for lineNum := 1; lineNum <= 5; lineNum++ { - fmt.Fprintf(w, "Line %d\n", lineNum) //nolint:errcheck, revive // It is fine to ignore the error + fmt.Fprintf(w, "Line %d\n", lineNum) if err := w.Flush(); err != nil { t.Errorf("unexpected error: %s", err) return @@ -4607,7 +4607,7 @@ func Test_Ctx_SendStreamWriter_Interrupted(t *testing.T) { app.Get("/", func(c Ctx) error { return c.SendStreamWriter(func(w *bufio.Writer) { for lineNum := 1; lineNum <= 5; lineNum++ { - fmt.Fprintf(w, "Line %d\n", lineNum) //nolint:errcheck // It is fine to ignore the error + fmt.Fprintf(w, "Line %d\n", lineNum) if err := w.Flush(); err != nil { if lineNum < 3 { @@ -4728,7 +4728,7 @@ func Benchmark_Ctx_Type(b *testing.B) { // go test -v -run=^$ -bench=Benchmark_Ctx_Type_Charset -benchmem -count=4 func Benchmark_Ctx_Type_Charset(b *testing.B) { app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed b.ReportAllocs() b.ResetTimer() @@ -4753,7 +4753,7 @@ func Test_Ctx_Vary(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Ctx_Vary -benchmem -count=4 func Benchmark_Ctx_Vary(b *testing.B) { app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed b.ReportAllocs() b.ResetTimer() @@ -4806,7 +4806,7 @@ func Test_Ctx_Writef(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Ctx_Writef -benchmem -count=4 func Benchmark_Ctx_Writef(b *testing.B) { app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed world := "World!" b.ReportAllocs() @@ -4951,11 +4951,11 @@ func Test_Ctx_BodyStreamWriter(t *testing.T) { ctx := &fasthttp.RequestCtx{} ctx.SetBodyStreamWriter(func(w *bufio.Writer) { - fmt.Fprintf(w, "body writer line 1\n") //nolint: errcheck // It is fine to ignore the error + fmt.Fprintf(w, "body writer line 1\n") if err := w.Flush(); err != nil { t.Errorf("unexpected error: %s", err) } - fmt.Fprintf(w, "body writer line 2\n") //nolint: errcheck // It is fine to ignore the error + fmt.Fprintf(w, "body writer line 2\n") }) require.True(t, ctx.IsBodyStream()) diff --git a/go.mod b/go.mod index 5367e203..f4cd17c1 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect - golang.org/x/net v0.35.0 // indirect + golang.org/x/net v0.37.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index e586535a..71824581 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,8 @@ golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= diff --git a/helpers.go b/helpers.go index a452e13c..584553a7 100644 --- a/helpers.go +++ b/helpers.go @@ -51,16 +51,16 @@ func getTLSConfig(ln net.Listener) *tls.Config { } // Copy value from pointer - if val := reflect.Indirect(pointer); val.Type() != nil { + if val := reflect.Indirect(pointer); val.IsValid() { // Get private field from value - if field := val.FieldByName("config"); field.Type() != nil { + if field := val.FieldByName("config"); field.IsValid() { // Copy value from pointer field (unsafe) newval := reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())) //nolint:gosec // Probably the only way to extract the *tls.Config from a net.Listener. TODO: Verify there really is no easier way without using unsafe. - if newval.Type() == nil { + if !newval.IsValid() { return nil } // Get element from pointer - if elem := newval.Elem(); elem.Type() != nil { + if elem := newval.Elem(); elem.IsValid() { // Cast value to *tls.Config c, ok := elem.Interface().(*tls.Config) if !ok { diff --git a/helpers_test.go b/helpers_test.go index 5e56bade..12d6b60f 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -566,7 +566,7 @@ func Test_Utils_TestConn_Closed_Write(t *testing.T) { require.NoError(t, err) // Close early, write should fail - conn.Close() //nolint:errcheck, revive // It is fine to ignore the error here + conn.Close() //nolint:errcheck // It is fine to ignore the error here _, err = conn.Write([]byte("Response 2\n")) require.ErrorIs(t, err, errTestConnClosed) diff --git a/internal/storage/memory/memory_test.go b/internal/storage/memory/memory_test.go index 347e9f5f..1952ddcf 100644 --- a/internal/storage/memory/memory_test.go +++ b/internal/storage/memory/memory_test.go @@ -209,7 +209,7 @@ func Benchmark_Memory_Set(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = testStore.Set("john", []byte("doe"), 0) //nolint: errcheck // error not needed for benchmark + _ = testStore.Set("john", []byte("doe"), 0) //nolint:errcheck // error not needed for benchmark } } @@ -220,7 +220,7 @@ func Benchmark_Memory_Set_Parallel(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { - _ = testStore.Set("john", []byte("doe"), 0) //nolint: errcheck // error not needed for benchmark + _ = testStore.Set("john", []byte("doe"), 0) //nolint:errcheck // error not needed for benchmark } }) } @@ -259,7 +259,7 @@ func Benchmark_Memory_Get(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = testStore.Get("john") //nolint: errcheck // error not needed for benchmark + _, _ = testStore.Get("john") //nolint:errcheck // error not needed for benchmark } } @@ -273,7 +273,7 @@ func Benchmark_Memory_Get_Parallel(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { - _, _ = testStore.Get("john") //nolint: errcheck // error not needed for benchmark + _, _ = testStore.Get("john") //nolint:errcheck // error not needed for benchmark } }) } @@ -315,8 +315,8 @@ func Benchmark_Memory_SetAndDelete(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = testStore.Set("john", []byte("doe"), 0) //nolint: errcheck // error not needed for benchmark - _ = testStore.Delete("john") //nolint: errcheck // error not needed for benchmark + _ = testStore.Set("john", []byte("doe"), 0) //nolint:errcheck // error not needed for benchmark + _ = testStore.Delete("john") //nolint:errcheck // error not needed for benchmark } } @@ -327,8 +327,8 @@ func Benchmark_Memory_SetAndDelete_Parallel(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { - _ = testStore.Set("john", []byte("doe"), 0) //nolint: errcheck // error not needed for benchmark - _ = testStore.Delete("john") //nolint: errcheck // error not needed for benchmark + _ = testStore.Set("john", []byte("doe"), 0) //nolint:errcheck // error not needed for benchmark + _ = testStore.Delete("john") //nolint:errcheck // error not needed for benchmark } }) } diff --git a/listen.go b/listen.go index f33c9daf..83ed655b 100644 --- a/listen.go +++ b/listen.go @@ -328,7 +328,7 @@ func (*App) prepareListenData(addr string, isTLS bool, cfg ListenConfig) ListenD } // startupMessage prepares the startup message with the handler number, port, address and other information -func (app *App) startupMessage(addr string, isTLS bool, pids string, cfg ListenConfig) { //nolint: revive // Accepting a bool param named isTLS if fine here +func (app *App) startupMessage(addr string, isTLS bool, pids string, cfg ListenConfig) { //nolint:revive // Accepting a bool param named isTLS if fine here // ignore child processes if IsChild() { return @@ -366,38 +366,35 @@ func (app *App) startupMessage(addr string, isTLS bool, pids string, cfg ListenC out = colorable.NewNonColorable(os.Stdout) } - fmt.Fprintf(out, "%s\n", fmt.Sprintf(figletFiberText, colors.Red+"v"+Version+colors.Reset)) //nolint:errcheck,revive // ignore error - fmt.Fprintf(out, strings.Repeat("-", 50)+"\n") //nolint:errcheck,revive,govet // ignore error + fmt.Fprintf(out, "%s\n", fmt.Sprintf(figletFiberText, colors.Red+"v"+Version+colors.Reset)) + fmt.Fprintf(out, strings.Repeat("-", 50)+"\n") if host == "0.0.0.0" { - //nolint:errcheck,revive // ignore error fmt.Fprintf(out, "%sINFO%s Server started on: \t%s%s://127.0.0.1:%s%s (bound on host 0.0.0.0 and port %s)\n", colors.Green, colors.Reset, colors.Blue, scheme, port, colors.Reset, port) } else { - //nolint:errcheck,revive // ignore error fmt.Fprintf(out, "%sINFO%s Server started on: \t%s%s%s\n", colors.Green, colors.Reset, colors.Blue, fmt.Sprintf("%s://%s:%s", scheme, host, port), colors.Reset) } if app.config.AppName != "" { - fmt.Fprintf(out, "%sINFO%s Application name: \t\t%s%s%s\n", colors.Green, colors.Reset, colors.Blue, app.config.AppName, colors.Reset) //nolint:errcheck,revive // ignore error + fmt.Fprintf(out, "%sINFO%s Application name: \t\t%s%s%s\n", colors.Green, colors.Reset, colors.Blue, app.config.AppName, colors.Reset) } - //nolint:errcheck,revive // ignore error fmt.Fprintf(out, "%sINFO%s Total handlers count: \t%s%s%s\n", colors.Green, colors.Reset, colors.Blue, strconv.Itoa(int(app.handlersCount)), colors.Reset) if isPrefork == "Enabled" { - fmt.Fprintf(out, "%sINFO%s Prefork: \t\t\t%s%s%s\n", colors.Green, colors.Reset, colors.Blue, isPrefork, colors.Reset) //nolint:errcheck,revive // ignore error + fmt.Fprintf(out, "%sINFO%s Prefork: \t\t\t%s%s%s\n", colors.Green, colors.Reset, colors.Blue, isPrefork, colors.Reset) } else { - fmt.Fprintf(out, "%sINFO%s Prefork: \t\t\t%s%s%s\n", colors.Green, colors.Reset, colors.Red, isPrefork, colors.Reset) //nolint:errcheck,revive // ignore error + fmt.Fprintf(out, "%sINFO%s Prefork: \t\t\t%s%s%s\n", colors.Green, colors.Reset, colors.Red, isPrefork, colors.Reset) } - fmt.Fprintf(out, "%sINFO%s PID: \t\t\t%s%v%s\n", colors.Green, colors.Reset, colors.Blue, os.Getpid(), colors.Reset) //nolint:errcheck,revive // ignore error - fmt.Fprintf(out, "%sINFO%s Total process count: \t%s%s%s\n", colors.Green, colors.Reset, colors.Blue, procs, colors.Reset) //nolint:errcheck,revive // ignore error + fmt.Fprintf(out, "%sINFO%s PID: \t\t\t%s%v%s\n", colors.Green, colors.Reset, colors.Blue, os.Getpid(), colors.Reset) + fmt.Fprintf(out, "%sINFO%s Total process count: \t%s%s%s\n", colors.Green, colors.Reset, colors.Blue, procs, colors.Reset) if cfg.EnablePrefork { // Turn the `pids` variable (in the form ",a,b,c,d,e,f,etc") into a slice of PIDs @@ -408,7 +405,7 @@ func (app *App) startupMessage(addr string, isTLS bool, pids string, cfg ListenC } } - fmt.Fprintf(out, "%sINFO%s Child PIDs: \t\t%s", colors.Green, colors.Reset, colors.Blue) //nolint:errcheck,revive // ignore error + fmt.Fprintf(out, "%sINFO%s Child PIDs: \t\t%s", colors.Green, colors.Reset, colors.Blue) totalPids := len(pidSlice) rowTotalPidCount := 10 @@ -421,17 +418,17 @@ func (app *App) startupMessage(addr string, isTLS bool, pids string, cfg ListenC } for n, pid := range pidSlice[start:end] { - fmt.Fprintf(out, "%s", pid) //nolint:errcheck,revive // ignore error + fmt.Fprintf(out, "%s", pid) if n+1 != len(pidSlice[start:end]) { - fmt.Fprintf(out, ", ") //nolint:errcheck,revive // ignore error + fmt.Fprintf(out, ", ") } } - fmt.Fprintf(out, "\n%s", colors.Reset) //nolint:errcheck,revive // ignore error + fmt.Fprintf(out, "\n%s", colors.Reset) } } // add new Line as spacer - fmt.Fprintf(out, "\n%s", colors.Reset) //nolint:errcheck,revive // ignore error + fmt.Fprintf(out, "\n%s", colors.Reset) } // printRoutesMessage print all routes with method, path, name and handlers @@ -473,11 +470,10 @@ func (app *App) printRoutesMessage() { return routes[i].path < routes[j].path }) - fmt.Fprintf(w, "%smethod\t%s| %spath\t%s| %sname\t%s| %shandlers\t%s\n", colors.Blue, colors.White, colors.Green, colors.White, colors.Cyan, colors.White, colors.Yellow, colors.Reset) //nolint:errcheck,revive // ignore error - fmt.Fprintf(w, "%s------\t%s| %s----\t%s| %s----\t%s| %s--------\t%s\n", colors.Blue, colors.White, colors.Green, colors.White, colors.Cyan, colors.White, colors.Yellow, colors.Reset) //nolint:errcheck,revive // ignore error + fmt.Fprintf(w, "%smethod\t%s| %spath\t%s| %sname\t%s| %shandlers\t%s\n", colors.Blue, colors.White, colors.Green, colors.White, colors.Cyan, colors.White, colors.Yellow, colors.Reset) + fmt.Fprintf(w, "%s------\t%s| %s----\t%s| %s----\t%s| %s--------\t%s\n", colors.Blue, colors.White, colors.Green, colors.White, colors.Cyan, colors.White, colors.Yellow, colors.Reset) for _, route := range routes { - //nolint:errcheck,revive // ignore error fmt.Fprintf(w, "%s%s\t%s| %s%s\t%s| %s%s\t%s| %s%s%s\n", colors.Blue, route.method, colors.White, colors.Green, route.path, colors.White, colors.Cyan, route.name, colors.White, colors.Yellow, route.handlers, colors.Reset) } diff --git a/log/default.go b/log/default.go index 6de940a4..f376a1a0 100644 --- a/log/default.go +++ b/log/default.go @@ -53,9 +53,9 @@ func (l *defaultLogger) privateLogf(lv Level, format string, fmtArgs []any) { buf.WriteString(level) if len(fmtArgs) > 0 { - _, _ = fmt.Fprintf(buf, format, fmtArgs...) //nolint: errcheck // It is fine to ignore the error + _, _ = fmt.Fprintf(buf, format, fmtArgs...) } else { - _, _ = fmt.Fprint(buf, fmtArgs...) //nolint: errcheck // It is fine to ignore the error + _, _ = fmt.Fprint(buf, fmtArgs...) } _ = l.stdlog.Output(l.depth, buf.String()) //nolint:errcheck // It is fine to ignore the error diff --git a/middleware/adaptor/adaptor_test.go b/middleware/adaptor/adaptor_test.go index 67c306fe..7bede0a8 100644 --- a/middleware/adaptor/adaptor_test.go +++ b/middleware/adaptor/adaptor_test.go @@ -1,4 +1,4 @@ -//nolint:contextcheck, revive // Much easier to just ignore memory leaks in tests +//nolint:contextcheck,revive // Much easier to just ignore memory leaks in tests package adaptor import ( @@ -68,7 +68,7 @@ func Test_HTTPHandler(t *testing.T) { w.Header().Set("Header1", "value1") w.Header().Set("Header2", "value2") w.WriteHeader(http.StatusBadRequest) - fmt.Fprintf(w, "request body is %q", body) //nolint:errcheck // not needed + fmt.Fprintf(w, "request body is %q", body) } fiberH := HTTPHandlerFunc(http.HandlerFunc(nethttpH)) fiberH = setFiberContextValueMiddleware(fiberH, expectedContextKey, expectedContextValue) diff --git a/middleware/logger/default_logger.go b/middleware/logger/default_logger.go index e4de79bf..c70a3e0e 100644 --- a/middleware/logger/default_logger.go +++ b/middleware/logger/default_logger.go @@ -166,7 +166,7 @@ func writeLog(w io.Writer, msg []byte) { // Write error to output if _, err := w.Write([]byte(err.Error())); err != nil { // There is something wrong with the given io.Writer - _, _ = fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err) //nolint: errcheck // It is fine to ignore the error + _, _ = fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err) } } } diff --git a/middleware/logger/logger_test.go b/middleware/logger/logger_test.go index 011a0ead..eb1b6a44 100644 --- a/middleware/logger/logger_test.go +++ b/middleware/logger/logger_test.go @@ -865,7 +865,7 @@ func Test_Logger_ByteSent_Streaming(t *testing.T) { for { i++ msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) - fmt.Fprintf(w, "data: Message: %s\n\n", msg) //nolint:errcheck // ignore error + fmt.Fprintf(w, "data: Message: %s\n\n", msg) err := w.Flush() if err != nil { break @@ -1049,7 +1049,7 @@ func Benchmark_Logger(b *testing.B) { for { i++ msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) - fmt.Fprintf(w, "data: Message: %s\n\n", msg) //nolint:errcheck // ignore error + fmt.Fprintf(w, "data: Message: %s\n\n", msg) err := w.Flush() if err != nil { break @@ -1217,7 +1217,7 @@ func Benchmark_Logger_Parallel(b *testing.B) { for { i++ msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) - fmt.Fprintf(w, "data: Message: %s\n\n", msg) //nolint:errcheck // ignore error + fmt.Fprintf(w, "data: Message: %s\n\n", msg) err := w.Flush() if err != nil { break diff --git a/middleware/proxy/proxy_test.go b/middleware/proxy/proxy_test.go index a8108251..532af09e 100644 --- a/middleware/proxy/proxy_test.go +++ b/middleware/proxy/proxy_test.go @@ -506,7 +506,10 @@ func Test_Proxy_Do_WithRealURL(t *testing.T) { return Do(c, "https://www.google.com") }) - resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) + resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), fiber.TestConfig{ + Timeout: 2 * time.Second, + FailOnTimeout: true, + }) require.NoError(t, err1) require.Equal(t, fiber.StatusOK, resp.StatusCode) require.Equal(t, "/test", resp.Request.URL.String()) @@ -523,7 +526,10 @@ func Test_Proxy_Do_WithRedirect(t *testing.T) { return Do(c, "https://google.com") }) - resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) + resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), fiber.TestConfig{ + Timeout: 2 * time.Second, + FailOnTimeout: true, + }) require.NoError(t, err1) body, err := io.ReadAll(resp.Body) require.NoError(t, err) @@ -558,7 +564,10 @@ func Test_Proxy_DoRedirects_TooManyRedirects(t *testing.T) { return DoRedirects(c, "http://google.com", 0) }) - resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) + resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), fiber.TestConfig{ + Timeout: 2 * time.Second, + FailOnTimeout: true, + }) require.NoError(t, err1) body, err := io.ReadAll(resp.Body) require.NoError(t, err) diff --git a/path.go b/path.go index 3876943d..b188a41c 100644 --- a/path.go +++ b/path.go @@ -487,7 +487,7 @@ func splitNonEscaped(s, sep string) []string { } // getMatch parses the passed url and tries to match it against the route segments and determine the parameter positions -func (parser *routeParser) getMatch(detectionPath, path string, params *[maxParams]string, partialCheck bool) bool { //nolint: revive // Accepting a bool param is fine here +func (parser *routeParser) getMatch(detectionPath, path string, params *[maxParams]string, partialCheck bool) bool { //nolint:revive // Accepting a bool param is fine here var i, paramsIterator, partLen int for _, segment := range parser.segs { partLen = len(detectionPath) diff --git a/redirect_test.go b/redirect_test.go index 1570d05f..82cfb36b 100644 --- a/redirect_test.go +++ b/redirect_test.go @@ -178,7 +178,7 @@ func Test_Redirect_Back_WithFlashMessages(t *testing.T) { return c.SendString("user") }).Name("user") - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed err := c.Redirect().With("success", "1").With("message", "test").Back("/") require.NoError(t, err) @@ -225,7 +225,7 @@ func Test_Redirect_Route_WithFlashMessages(t *testing.T) { return c.SendString("user") }).Name("user") - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed err := c.Redirect().With("success", "1").With("message", "test").Route("user") @@ -259,7 +259,7 @@ func Test_Redirect_Route_WithOldInput(t *testing.T) { return c.SendString("user") }).Name("user") - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed c.Request().URI().SetQueryString("id=1&name=tom") err := c.Redirect().With("success", "1").With("message", "test").WithInput().Route("user") @@ -294,7 +294,7 @@ func Test_Redirect_Route_WithOldInput(t *testing.T) { return c.SendString("user") }).Name("user") - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed c.Request().Header.Set(HeaderContentType, MIMEApplicationForm) c.Request().SetBodyString("id=1&name=tom") @@ -330,7 +330,7 @@ func Test_Redirect_Route_WithOldInput(t *testing.T) { return c.SendString("user") }).Name("user") - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed body := &bytes.Buffer{} writer := multipart.NewWriter(body) @@ -376,7 +376,7 @@ func Test_Redirect_parseAndClearFlashMessages(t *testing.T) { return c.SendString("user") }).Name("user") - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed msgs := redirectionMsgs{ { @@ -464,7 +464,7 @@ func Benchmark_Redirect_Route(b *testing.B) { return c.JSON(c.Params("name")) }).Name("user") - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed b.ReportAllocs() b.ResetTimer() @@ -491,7 +491,7 @@ func Benchmark_Redirect_Route_WithQueries(b *testing.B) { return c.JSON(c.Params("name")) }).Name("user") - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed b.ReportAllocs() b.ResetTimer() @@ -523,7 +523,7 @@ func Benchmark_Redirect_Route_WithFlashMessages(b *testing.B) { return c.SendString("user") }).Name("user") - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed b.ReportAllocs() b.ResetTimer() @@ -576,7 +576,7 @@ func Benchmark_Redirect_parseAndClearFlashMessages(b *testing.B) { return c.SendString("user") }).Name("user") - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed val, err := testredirectionMsgs.MarshalMsg(nil) require.NoError(b, err) @@ -618,7 +618,7 @@ func Benchmark_Redirect_processFlashMessages(b *testing.B) { return c.SendString("user") }).Name("user") - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed c.Redirect().With("success", "1").With("message", "test") @@ -647,7 +647,7 @@ func Benchmark_Redirect_Messages(b *testing.B) { return c.SendString("user") }).Name("user") - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed val, err := testredirectionMsgs.MarshalMsg(nil) require.NoError(b, err) @@ -684,7 +684,7 @@ func Benchmark_Redirect_OldInputs(b *testing.B) { return c.SendString("user") }).Name("user") - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed val, err := testredirectionMsgs.MarshalMsg(nil) require.NoError(b, err) @@ -719,7 +719,7 @@ func Benchmark_Redirect_Message(b *testing.B) { return c.SendString("user") }).Name("user") - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed val, err := testredirectionMsgs.MarshalMsg(nil) require.NoError(b, err) @@ -750,7 +750,7 @@ func Benchmark_Redirect_OldInput(b *testing.B) { return c.SendString("user") }).Name("user") - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed val, err := testredirectionMsgs.MarshalMsg(nil) require.NoError(b, err) diff --git a/router.go b/router.go index a14d2edd..795529bd 100644 --- a/router.go +++ b/router.go @@ -108,7 +108,7 @@ func (r *Route) match(detectionPath, path string, params *[maxParams]string) boo return false } -func (app *App) nextCustom(c CustomCtx) (bool, error) { //nolint: unparam // bool param might be useful for testing +func (app *App) nextCustom(c CustomCtx) (bool, error) { //nolint:unparam // bool param might be useful for testing // Get stack length tree, ok := app.treeStack[c.getMethodINT()][c.getTreePath()] if !ok { diff --git a/router_test.go b/router_test.go index 0ac0c212..0e6019e2 100644 --- a/router_test.go +++ b/router_test.go @@ -600,7 +600,7 @@ func Benchmark_Router_Next(b *testing.B) { var res bool var err error - c := app.AcquireCtx(request).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c := app.AcquireCtx(request).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed b.ResetTimer() for n := 0; n < b.N; n++ { @@ -825,7 +825,7 @@ func Benchmark_Router_Github_API(b *testing.B) { for n := 0; n < b.N; n++ { c.URI().SetPath(routesFixture.TestRoutes[i].Path) - ctx := app.AcquireCtx(c).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + ctx := app.AcquireCtx(c).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed match, err = app.next(ctx) app.ReleaseCtx(ctx) From 87f3f0c8b658978cebe29f4099a02d1fa00532d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=2E=20Efe=20=C3=87etin?= Date: Wed, 19 Mar 2025 13:00:16 +0300 Subject: [PATCH 35/53] :bug: bug: fix client iterators when using break statement (#3357) * :bug: bug: fix client iterators when using break statement * fix linter --- client/request.go | 13 +++++++------ client/request_test.go | 19 ++++++++++++++++++- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/client/request.go b/client/request.go index 80a03b09..daa7e60b 100644 --- a/client/request.go +++ b/client/request.go @@ -298,11 +298,12 @@ func (r *Request) Cookie(key string) string { // Use maps.Collect() to gather them into a map if needed. func (r *Request) Cookies() iter.Seq2[string, string] { return func(yield func(string, string) bool) { - r.cookies.VisitAll(func(key, val string) { - if !yield(key, val) { + for k, v := range *r.cookies { + res := yield(k, v) + if !res { return } - }) + } } } @@ -343,11 +344,11 @@ func (r *Request) PathParam(key string) string { // Use maps.Collect() to gather them into a map if needed. func (r *Request) PathParams() iter.Seq2[string, string] { return func(yield func(string, string) bool) { - r.path.VisitAll(func(key, val string) { - if !yield(key, val) { + for k, v := range *r.path { + if !yield(k, v) { return } - }) + } } } diff --git a/client/request_test.go b/client/request_test.go index c13dbbd8..73bae29d 100644 --- a/client/request_test.go +++ b/client/request_test.go @@ -1,3 +1,4 @@ +//nolint:goconst // Much easier to just ignore memory leaks in tests package client import ( @@ -451,6 +452,14 @@ func Test_Request_Cookies(t *testing.T) { require.Equal(t, "bar", cookies["foo"]) require.Equal(t, "foo", cookies["bar"]) + require.NotPanics(t, func() { + for _, v := range req.Cookies() { + if v == "bar" { + break + } + } + }) + require.Len(t, cookies, 2) } @@ -564,6 +573,14 @@ func Test_Request_PathParams(t *testing.T) { require.Equal(t, "foo", pathParams["bar"]) require.Len(t, pathParams, 2) + + require.NotPanics(t, func() { + for _, v := range req.PathParams() { + if v == "bar" { + break + } + } + }) } func Benchmark_Request_PathParams(b *testing.B) { @@ -1579,7 +1596,7 @@ func Test_SetValWithStruct(t *testing.T) { require.True(t, func() bool { for _, v := range p.PeekMulti("TSlice") { - if string(v) == "bar" { //nolint:goconst // test + if string(v) == "bar" { return true } } From f6ac929fde9c5502d2b80abc0fe679d5e6fe5dc3 Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Thu, 20 Mar 2025 06:35:59 -0700 Subject: [PATCH 36/53] =?UTF-8?q?=F0=9F=A7=B9=20chore:=20Mark=20unused=20t?= =?UTF-8?q?ests=20with=20t.SkipNow=20(#3366)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🚨 test: uncomment test and mark it with t.SkipNow * fix lint issues --- middleware/csrf/csrf_test.go | 91 ++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 41 deletions(-) diff --git a/middleware/csrf/csrf_test.go b/middleware/csrf/csrf_test.go index 090082f4..7f586cd9 100644 --- a/middleware/csrf/csrf_test.go +++ b/middleware/csrf/csrf_test.go @@ -1331,56 +1331,65 @@ func Test_CSRF_Cookie_Injection_Exploit(t *testing.T) { } // TODO: use this test case and make the unsafe header value bug from https://github.com/gofiber/fiber/issues/2045 reproducible and permanently fixed/tested by this testcase -// func Test_CSRF_UnsafeHeaderValue(t *testing.T) { -// t.Parallel() -// app := fiber.New() +func Test_CSRF_UnsafeHeaderValue(t *testing.T) { + t.SkipNow() + t.Parallel() + app := fiber.New() -// app.Use(New()) -// app.Get("/", func(c fiber.Ctx) error { -// return c.SendStatus(fiber.StatusOK) -// }) -// app.Get("/test", func(c fiber.Ctx) error { -// return c.SendStatus(fiber.StatusOK) -// }) -// app.Post("/", func(c fiber.Ctx) error { -// return c.SendStatus(fiber.StatusOK) -// }) + app.Use(New()) + app.Get("/", func(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + app.Get("/test", func(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + app.Post("/", func(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) -// resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) -// require.NoError(t, err) -// require.Equal(t, fiber.StatusOK, resp.StatusCode) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) -// var token string -// for _, c := range resp.Cookies() { -// if c.Name != ConfigDefault.CookieName { -// continue -// } -// token = c.Value -// break -// } + var token string + for _, c := range resp.Cookies() { + if c.Name != ConfigDefault.CookieName { + continue + } + token = c.Value + break + } -// fmt.Println("token", token) + t.Log("token", token) -// getReq := httptest.NewRequest(fiber.MethodGet, "/", nil) -// getReq.Header.Set(HeaderName, token) -// resp, err = app.Test(getReq) + getReq := httptest.NewRequest(fiber.MethodGet, "/", nil) + getReq.Header.Set(HeaderName, token) + resp, err = app.Test(getReq) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) -// getReq = httptest.NewRequest(fiber.MethodGet, "/test", nil) -// getReq.Header.Set("X-Requested-With", "XMLHttpRequest") -// getReq.Header.Set(fiber.HeaderCacheControl, "no") -// getReq.Header.Set(HeaderName, token) + getReq = httptest.NewRequest(fiber.MethodGet, "/test", nil) + getReq.Header.Set("X-Requested-With", "XMLHttpRequest") + getReq.Header.Set(fiber.HeaderCacheControl, "no") + getReq.Header.Set(HeaderName, token) -// resp, err = app.Test(getReq) + resp, err = app.Test(getReq) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) -// getReq.Header.Set(fiber.HeaderAccept, "*/*") -// getReq.Header.Del(HeaderName) -// resp, err = app.Test(getReq) + getReq.Header.Set(fiber.HeaderAccept, "*/*") + getReq.Header.Del(HeaderName) + resp, err = app.Test(getReq) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) -// postReq := httptest.NewRequest(fiber.MethodPost, "/", nil) -// postReq.Header.Set("X-Requested-With", "XMLHttpRequest") -// postReq.Header.Set(HeaderName, token) -// resp, err = app.Test(postReq) -// } + postReq := httptest.NewRequest(fiber.MethodPost, "/", nil) + postReq.Header.Set("X-Requested-With", "XMLHttpRequest") + postReq.Header.Set(HeaderName, token) + resp, err = app.Test(postReq) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) +} // go test -v -run=^$ -bench=Benchmark_Middleware_CSRF_Check -benchmem -count=4 func Benchmark_Middleware_CSRF_Check(b *testing.B) { From e947e03ed2d072b683aa69757109dee8cf565c08 Mon Sep 17 00:00:00 2001 From: Edvard <75655486+edvardsanta@users.noreply.github.com> Date: Fri, 21 Mar 2025 12:13:21 -0300 Subject: [PATCH 37/53] =?UTF-8?q?=F0=9F=94=A5=20feat(logger):=20Add=20pred?= =?UTF-8?q?efined=20log=20formats=20(#3359)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(logger): Add predefined log formats This commit introduces predefined log formats for the logger middleware, enhancing its flexibility and ease of use. Users can now specify formats like "common", "combined", and "json" in addition to the default format. Changes: - Added a `format.go` file to store predefined log format constants. - Updated `config.go` to include documentation for the `Format` configuration option, explaining the available placeholders and predefined formats. - Modified `logger.go` to utilize the predefined formats based on the `Format` configuration. - Added a new test case `Test_Logger_CLF` in `logger_test.go` to verify the "common" log format. * feat(logger): Use predefined formats and fix default format This commit updates the logger middleware to utilize the predefined log formats introduced in a previous commit. It also fixes the default format to use the `FormatDefault` constant. Changes: - Updated `config.go` to use `FormatDefault` constant for the default format. - Updated `default_logger.go` to use `FormatDefault` constant for the default format. - Added new test cases in `logger_test.go` to verify the "common", "combined" and "json" log formats. - Updated `format.go` to add newline character to the end of the default format. * feat(logger): Document and exemplify predefined formats * fix(logger): Improve test assertions based on golangci-lint * docs(logger): Improve documentation and formatting logger.md based on markdownlint-cli2 * docs(logger): Improve documentation based on markdownlint-cli2 * fix(logger): Improve combined and JSON format tests * feat(logger): Add ECS log format * feat(logger): Add CustomFormat option This commit introduces a `CustomFormat` option to the `Config` struct, allowing users to specify a predefined format (like "common", "combined", "json", or "ecs") * feat(logger): Add ECS log format to examples and config * docs(logger): Update examples in whats_new.md * feat(logger): Remove CustomFormat option and renamed Format consts - Removed `CustomFormat` field from `Config`. - Removed `LoggerConfig` map. - Rename predefined formats constants. * docs(logger): Update documentation and examples after format refactor --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> --- docs/middleware/logger.md | 78 ++++++++++++------ docs/whats_new.md | 16 ++++ middleware/logger/config.go | 23 ++++-- middleware/logger/default_logger.go | 2 +- middleware/logger/format.go | 14 ++++ middleware/logger/logger.go | 1 - middleware/logger/logger_test.go | 118 ++++++++++++++++++++++++++++ 7 files changed, 220 insertions(+), 32 deletions(-) create mode 100644 middleware/logger/format.go diff --git a/docs/middleware/logger.md b/docs/middleware/logger.md index af16f384..c4c60cbc 100644 --- a/docs/middleware/logger.md +++ b/docs/middleware/logger.md @@ -79,7 +79,7 @@ app.Use(logger.New(logger.Config{ TimeZone: "Asia/Shanghai", Done: func(c fiber.Ctx, logString []byte) { if c.Response().StatusCode() != fiber.StatusOK { - reporter.SendToSlack(logString) + reporter.SendToSlack(logString) } }, })) @@ -88,6 +88,23 @@ app.Use(logger.New(logger.Config{ app.Use(logger.New(logger.Config{ DisableColors: true, })) + +// Use predefined formats +app.Use(logger.New(logger.Config{ + Format: logger.FormatCommon, +})) + +app.Use(logger.New(logger.Config{ + Format: logger.FormatCombined, +})) + +app.Use(logger.New(logger.Config{ + Format: logger.FormatJSON, +})) + +app.Use(logger.New(logger.Config{ + Format: logger.FormatECS, +})) ``` ### Use Logger Middleware with Other Loggers @@ -136,37 +153,50 @@ Writing to os.File is goroutine-safe, but if you are using a custom Stream that ### Config -| Property | Type | Description | Default | -|:-----------------|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------| -| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | -| Skip | `func(fiber.Ctx) bool` | Skip is a function to determine if logging is skipped or written to Stream. | `nil` | -| Done | `func(fiber.Ctx, []byte)` | Done is a function that is called after the log string for a request is written to Stream, and pass the log string as parameter. | `nil` | -| CustomTags | `map[string]LogFunc` | tagFunctions defines the custom tag action. | `map[string]LogFunc` | -| Format | `string` | Format defines the logging tags. | `[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n` | -| TimeFormat | `string` | TimeFormat defines the time format for log timestamps. | `15:04:05` | -| TimeZone | `string` | TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc | `"Local"` | -| TimeInterval | `time.Duration` | TimeInterval is the delay before the timestamp is updated. | `500 * time.Millisecond` | -| Stream | `io.Writer` | Stream is a writer where logs are written. | `os.Stdout` | -| LoggerFunc | `func(c fiber.Ctx, data *Data, cfg Config) error` | Custom logger function for integration with logging libraries (Zerolog, Zap, Logrus, etc). Defaults to Fiber's default logger if not defined. | `see default_logger.go defaultLoggerInstance` | -| DisableColors | `bool` | DisableColors defines if the logs output should be colorized. | `false` | +| Property | Type | Description | Default | +| :------------ | :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------- | +| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| Skip | `func(fiber.Ctx) bool` | Skip is a function to determine if logging is skipped or written to Stream. | `nil` | +| Done | `func(fiber.Ctx, []byte)` | Done is a function that is called after the log string for a request is written to Stream, and pass the log string as parameter. | `nil` | +| CustomTags | `map[string]LogFunc` | tagFunctions defines the custom tag action. | `map[string]LogFunc` | +| `Format` | `string` | Defines the logging tags. See more in [Predefined Formats](#predefined-formats), or create your own using [Tags](#constants). | `[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n` (same as `DefaultFormat`) | +| TimeFormat | `string` | TimeFormat defines the time format for log timestamps. | `15:04:05` | +| TimeZone | `string` | TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc | `"Local"` | +| TimeInterval | `time.Duration` | TimeInterval is the delay before the timestamp is updated. | `500 * time.Millisecond` | +| Stream | `io.Writer` | Stream is a writer where logs are written. | `os.Stdout` | +| LoggerFunc | `func(c fiber.Ctx, data *Data, cfg Config) error` | Custom logger function for integration with logging libraries (Zerolog, Zap, Logrus, etc). Defaults to Fiber's default logger if not defined. | `see default_logger.go defaultLoggerInstance` | +| DisableColors | `bool` | DisableColors defines if the logs output should be colorized. | `false` | ## Default Config ```go var ConfigDefault = Config{ - Next: nil, - Skip nil, - Done: nil, - Format: "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n", - TimeFormat: "15:04:05", - TimeZone: "Local", - TimeInterval: 500 * time.Millisecond, - Stream: os.Stdout, - DisableColors: false, - LoggerFunc: defaultLoggerInstance, + Next: nil, + Skip: nil, + Done: nil, + Format: DefaultFormat, + TimeFormat: "15:04:05", + TimeZone: "Local", + TimeInterval: 500 * time.Millisecond, + Stream: os.Stdout, + BeforeHandlerFunc: beforeHandlerFunc, + LoggerFunc: defaultLoggerInstance, + enableColors: true, } ``` +## Predefined Formats + +Logger provides predefined formats that you can use by name or directly by specifying the format string. + +| **Format Constant** | **Format String** | **Description** | +|---------------------|--------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------| +| `DefaultFormat` | `"[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n"` | Fiber's default logger format. | +| `CommonFormat` | `"${ip} - - [${time}] "${method} ${url} ${protocol}" ${status} ${bytesSent}\n"` | Common Log Format (CLF) used in web server logs. | +| `CombinedFormat` | `"${ip} - - [${time}] "${method} ${url} ${protocol}" ${status} ${bytesSent} "${referer}" "${ua}"\n"` | CLF format plus the `referer` and `user agent` fields. | +| `JSONFormat` | `"{time: ${time}, ip: ${ip}, method: ${method}, url: ${url}, status: ${status}, bytesSent: ${bytesSent}}\n"` | JSON format for structured logging. | +| `ECSFormat` | `"{\"@timestamp\":\"${time}\",\"ecs\":{\"version\":\"1.6.0\"},\"client\":{\"ip\":\"${ip}\"},\"http\":{\"request\":{\"method\":\"${method}\",\"url\":\"${url}\",\"protocol\":\"${protocol}\"},\"response\":{\"status_code\":${status},\"body\":{\"bytes\":${bytesSent}}}},\"log\":{\"level\":\"INFO\",\"logger\":\"fiber\"},\"message\":\"${method} ${url} responded with ${status}\"}\n"` | Elastic Common Schema (ECS) format for structured logging. | + ## Constants ```go diff --git a/docs/whats_new.md b/docs/whats_new.md index 8528dc41..bc569d3e 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -937,6 +937,22 @@ app.Use(logger.New(logger.Config{ +#### Predefined Formats + +Logger provides predefined formats that you can use by name or directly by specifying the format string. +
+ +Example Usage + +```go +app.Use(logger.New(logger.Config{ + Format: logger.FormatCombined, +})) +``` + +See more in [Logger](./middleware/logger.md#predefined-formats) +
+ ### Filesystem We've decided to remove filesystem middleware to clear up the confusion between static and filesystem middleware. diff --git a/middleware/logger/config.go b/middleware/logger/config.go index 2df814eb..d543acfa 100644 --- a/middleware/logger/config.go +++ b/middleware/logger/config.go @@ -50,9 +50,23 @@ type Config struct { timeZoneLocation *time.Location - // Format defines the logging tags + // Format defines the logging format for the middleware. // - // Optional. Default: [${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error} + // You can customize the log output by defining a format string with placeholders + // such as: ${time}, ${ip}, ${status}, ${method}, ${path}, ${latency}, ${error}, etc. + // The full list of available placeholders can be found in 'tags.go' or at + // 'https://docs.gofiber.io/api/middleware/logger/#constants'. + // + // Fiber provides predefined logging formats that can be used directly: + // + // - DefaultFormat → Uses the default log format: "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}" + // - CommonFormat → Uses the Apache Common Log Format (CLF): "${ip} - - [${time}] \"${method} ${url} ${protocol}\" ${status} ${bytesSent}\n" + // - CombinedFormat → Uses the Apache Combined Log Format: "${ip} - - [${time}] \"${method} ${url} ${protocol}\" ${status} ${bytesSent} \"${referer}\" \"${ua}\"\n" + // - JSONFormat → Uses the JSON log format: "{\"time\":\"${time}\",\"ip\":\"${ip}\",\"method\":\"${method}\",\"url\":\"${url}\",\"status\":${status},\"bytesSent\":${bytesSent}}\n" + // - ECSFormat → Uses the Elastic Common Schema (ECS) log format: {\"@timestamp\":\"${time}\",\"ecs\":{\"version\":\"1.6.0\"},\"client\":{\"ip\":\"${ip}\"},\"http\":{\"request\":{\"method\":\"${method}\",\"url\":\"${url}\",\"protocol\":\"${protocol}\"},\"response\":{\"status_code\":${status},\"body\":{\"bytes\":${bytesSent}}}},\"log\":{\"level\":\"INFO\",\"logger\":\"fiber\"},\"message\":\"${method} ${url} responded with ${status}\"}" + // If both `Format` and `CustomFormat` are provided, the `CustomFormat` will be used, and the `Format` field will be ignored. + // If no format is specified, the default format is used: + // "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}" Format string // TimeFormat https://programming.guide/go/format-parse-string-time-date-example.html @@ -105,7 +119,7 @@ var ConfigDefault = Config{ Next: nil, Skip: nil, Done: nil, - Format: defaultFormat, + Format: DefaultFormat, TimeFormat: "15:04:05", TimeZone: "Local", TimeInterval: 500 * time.Millisecond, @@ -115,9 +129,6 @@ var ConfigDefault = Config{ enableColors: true, } -// default logging format for Fiber's default logger -var defaultFormat = "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n" - // Helper function to set default values func configDefault(config ...Config) Config { // Return default config if nothing provided diff --git a/middleware/logger/default_logger.go b/middleware/logger/default_logger.go index c70a3e0e..a2cbfa1f 100644 --- a/middleware/logger/default_logger.go +++ b/middleware/logger/default_logger.go @@ -28,7 +28,7 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error { buf := bytebufferpool.Get() // Default output when no custom Format or io.Writer is given - if cfg.Format == defaultFormat { + if cfg.Format == DefaultFormat { // Format error if exist formatErr := "" if cfg.enableColors { diff --git a/middleware/logger/format.go b/middleware/logger/format.go new file mode 100644 index 00000000..901c2409 --- /dev/null +++ b/middleware/logger/format.go @@ -0,0 +1,14 @@ +package logger + +const ( + // Fiber's default logger + DefaultFormat = "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n" + // Apache Common Log Format (CLF) + CommonFormat = "${ip} - - [${time}] \"${method} ${url} ${protocol}\" ${status} ${bytesSent}\n" + // Apache Combined Log Format + CombinedFormat = "${ip} - - [${time}] \"${method} ${url} ${protocol}\" ${status} ${bytesSent} \"${referer}\" \"${ua}\"\n" + // JSON log formats + JSONFormat = "{\"time\":\"${time}\",\"ip\":\"${ip}\",\"method\":\"${method}\",\"url\":\"${url}\",\"status\":${status},\"bytesSent\":${bytesSent}}\n" + // Elastic Common Schema (ECS) Log Format + ECSFormat = "{\"@timestamp\":\"${time}\",\"ecs\":{\"version\":\"1.6.0\"},\"client\":{\"ip\":\"${ip}\"},\"http\":{\"request\":{\"method\":\"${method}\",\"url\":\"${url}\",\"protocol\":\"${protocol}\"},\"response\":{\"status_code\":${status},\"body\":{\"bytes\":${bytesSent}}}},\"log\":{\"level\":\"INFO\",\"logger\":\"fiber\"},\"message\":\"${method} ${url} responded with ${status}\"}\n" +) diff --git a/middleware/logger/logger.go b/middleware/logger/logger.go index 793c16c2..7d4befc9 100644 --- a/middleware/logger/logger.go +++ b/middleware/logger/logger.go @@ -40,7 +40,6 @@ func New(config ...Config) fiber.Handler { } }() } - // Set PID once pid := strconv.Itoa(os.Getpid()) diff --git a/middleware/logger/logger_test.go b/middleware/logger/logger_test.go index eb1b6a44..edce174c 100644 --- a/middleware/logger/logger_test.go +++ b/middleware/logger/logger_test.go @@ -467,6 +467,124 @@ func Test_Logger_All(t *testing.T) { require.Equal(t, expected, buf.String()) } +func Test_Logger_CLF_Format(t *testing.T) { + t.Parallel() + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app := fiber.New() + + app.Use(New(Config{ + Format: CommonFormat, + Stream: buf, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?foo=bar", nil)) + require.NoError(t, err) + require.Equal(t, fiber.StatusNotFound, resp.StatusCode) + + expected := fmt.Sprintf("0.0.0.0 - - [%s] \"%s %s %s\" %d %d\n", + time.Now().Format("15:04:05"), + fiber.MethodGet, "/?foo=bar", "HTTP/1.1", + fiber.StatusNotFound, + 0) + logResponse := buf.String() + require.Equal(t, expected, logResponse) +} + +func Test_Logger_Combined_CLF_Format(t *testing.T) { + t.Parallel() + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app := fiber.New() + + app.Use(New(Config{ + Format: CombinedFormat, + Stream: buf, + })) + const expectedUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36" + const expectedReferer = "http://example.com" + req := httptest.NewRequest(fiber.MethodGet, "/?foo=bar", nil) + req.Header.Set("Referer", expectedReferer) + req.Header.Set("User-Agent", expectedUA) + resp, err := app.Test(req) + require.NoError(t, err) + require.Equal(t, fiber.StatusNotFound, resp.StatusCode) + + expected := fmt.Sprintf("0.0.0.0 - - [%s] %q %d %d %q %q\n", + time.Now().Format("15:04:05"), + fmt.Sprintf("%s %s %s", fiber.MethodGet, "/?foo=bar", "HTTP/1.1"), + fiber.StatusNotFound, + 0, + expectedReferer, + expectedUA) + logResponse := buf.String() + require.Equal(t, expected, logResponse) +} + +func Test_Logger_Json_Format(t *testing.T) { + t.Parallel() + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app := fiber.New() + + app.Use(New(Config{ + Format: JSONFormat, + Stream: buf, + })) + + req := httptest.NewRequest(fiber.MethodGet, "/?foo=bar", nil) + resp, err := app.Test(req) + require.NoError(t, err) + require.Equal(t, fiber.StatusNotFound, resp.StatusCode) + + expected := fmt.Sprintf( + "{\"time\":%q,\"ip\":%q,\"method\":%q,\"url\":%q,\"status\":%d,\"bytesSent\":%d}\n", + time.Now().Format("15:04:05"), + "0.0.0.0", + fiber.MethodGet, + "/?foo=bar", + fiber.StatusNotFound, + 0, + ) + logResponse := buf.String() + require.Equal(t, expected, logResponse) +} + +func Test_Logger_ECS_Format(t *testing.T) { + t.Parallel() + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app := fiber.New() + + app.Use(New(Config{ + Format: ECSFormat, + Stream: buf, + })) + + req := httptest.NewRequest(fiber.MethodGet, "/?foo=bar", nil) + resp, err := app.Test(req) + require.NoError(t, err) + require.Equal(t, fiber.StatusNotFound, resp.StatusCode) + + expected := fmt.Sprintf( + "{\"@timestamp\":%q,\"ecs\":{\"version\":\"1.6.0\"},\"client\":{\"ip\":%q},\"http\":{\"request\":{\"method\":%q,\"url\":%q,\"protocol\":%q},\"response\":{\"status_code\":%d,\"body\":{\"bytes\":%d}}},\"log\":{\"level\":\"INFO\",\"logger\":\"fiber\"},\"message\":%q}\n", + time.Now().Format("15:04:05"), + "0.0.0.0", + fiber.MethodGet, + "/?foo=bar", + "HTTP/1.1", + fiber.StatusNotFound, + 0, + fmt.Sprintf("%s %s responded with %d", fiber.MethodGet, "/?foo=bar", fiber.StatusNotFound), + ) + logResponse := buf.String() + require.Equal(t, expected, logResponse) +} + func getLatencyTimeUnits() []struct { unit string div time.Duration From ef40c04ede816d9ce2071e3a0034b3a706789520 Mon Sep 17 00:00:00 2001 From: Kashiwa <13825170+ksw2000@users.noreply.github.com> Date: Sun, 23 Mar 2025 23:58:43 +0800 Subject: [PATCH 38/53] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20reduce?= =?UTF-8?q?=20DefaultCtx=20from=20768=20bytes=20to=20736=20bytes=20(#3353)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ Refactor: reduce DefaultCtx from 768 bytes to 736 bytes * ♻️ Refactor: add comments --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> --- ctx.go | 69 +++++++++++++++++++++++++++---------------------------- router.go | 2 +- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/ctx.go b/ctx.go index b2d13c0a..efd6006d 100644 --- a/ctx.go +++ b/ctx.go @@ -49,28 +49,26 @@ const userContextKey contextKey = 0 // __local_user_context__ // //go:generate ifacemaker --file ctx.go --struct DefaultCtx --iface Ctx --pkg fiber --output ctx_interface_gen.go --not-exported true --iface-comment "Ctx represents the Context which hold the HTTP request and response.\nIt has methods for the request query string, parameters, body, HTTP headers and so on." type DefaultCtx struct { - app *App // Reference to *App - route *Route // Reference to *Route - fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx - bind *Bind // Default bind reference - redirect *Redirect // Default redirect reference - req *DefaultReq // Default request api reference - res *DefaultRes // Default response api reference - values [maxParams]string // Route parameter values - viewBindMap sync.Map // Default view map to bind template engine - method string // HTTP method - baseURI string // HTTP base uri - path string // HTTP path with the modifications by the configuration -> string copy from pathBuffer - detectionPath string // Route detection path -> string copy from detectionPathBuffer - treePath string // Path for the search in the tree - pathOriginal string // Original HTTP path - pathBuffer []byte // HTTP path buffer - detectionPathBuffer []byte // HTTP detectionPath buffer - flashMessages redirectionMsgs // Flash messages - indexRoute int // Index of the current route - indexHandler int // Index of the current handler - methodINT int // HTTP method INT equivalent - matched bool // Non use route matched + app *App // Reference to *App + route *Route // Reference to *Route + fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx + bind *Bind // Default bind reference + redirect *Redirect // Default redirect reference + req *DefaultReq // Default request api reference + res *DefaultRes // Default response api reference + values [maxParams]string // Route parameter values + viewBindMap sync.Map // Default view map to bind template engine + method string // HTTP method + baseURI string // HTTP base uri + treePath string // Path for the search in the tree + pathOriginal string // Original HTTP path + flashMessages redirectionMsgs // Flash messages + path []byte // HTTP path with the modifications by the configuration + detectionPath []byte // Route detection path + indexRoute int // Index of the current route + indexHandler int // Index of the current handler + methodINT int // HTTP method INT equivalent + matched bool // Non use route matched } // SendFile defines configuration options when to transfer file with SendFile. @@ -1123,8 +1121,9 @@ func Params[V GenericType](c Ctx, key string, defaultValue ...V) V { // Path returns the path part of the request URL. // Optionally, you could override the path. +// Make copies or use the Immutable setting to use the value outside the Handler. func (c *DefaultCtx) Path(override ...string) string { - if len(override) != 0 && c.path != override[0] { + if len(override) != 0 && string(c.path) != override[0] { // Set new path to context c.pathOriginal = override[0] @@ -1133,7 +1132,7 @@ func (c *DefaultCtx) Path(override ...string) string { // Prettify path c.configDependentPaths() } - return c.path + return c.app.getString(c.path) } // Scheme contains the request protocol string: http or https for TLS requests. @@ -1832,32 +1831,32 @@ func (c *DefaultCtx) XHR() bool { // configDependentPaths set paths for route recognition and prepared paths for the user, // here the features for caseSensitive, decoded paths, strict paths are evaluated func (c *DefaultCtx) configDependentPaths() { - c.pathBuffer = append(c.pathBuffer[0:0], c.pathOriginal...) + c.path = append(c.path[:0], c.pathOriginal...) // If UnescapePath enabled, we decode the path and save it for the framework user if c.app.config.UnescapePath { - c.pathBuffer = fasthttp.AppendUnquotedArg(c.pathBuffer[:0], c.pathBuffer) + c.path = fasthttp.AppendUnquotedArg(c.path[:0], c.path) } - c.path = c.app.getString(c.pathBuffer) // another path is specified which is for routing recognition only // use the path that was changed by the previous configuration flags - c.detectionPathBuffer = append(c.detectionPathBuffer[0:0], c.pathBuffer...) + c.detectionPath = append(c.detectionPath[:0], c.path...) // If CaseSensitive is disabled, we lowercase the original path if !c.app.config.CaseSensitive { - c.detectionPathBuffer = utils.ToLowerBytes(c.detectionPathBuffer) + c.detectionPath = utils.ToLowerBytes(c.detectionPath) } // If StrictRouting is disabled, we strip all trailing slashes - if !c.app.config.StrictRouting && len(c.detectionPathBuffer) > 1 && c.detectionPathBuffer[len(c.detectionPathBuffer)-1] == '/' { - c.detectionPathBuffer = utils.TrimRight(c.detectionPathBuffer, '/') + if !c.app.config.StrictRouting && len(c.detectionPath) > 1 && c.detectionPath[len(c.detectionPath)-1] == '/' { + c.detectionPath = utils.TrimRight(c.detectionPath, '/') } - c.detectionPath = c.app.getString(c.detectionPathBuffer) // Define the path for dividing routes into areas for fast tree detection, so that fewer routes need to be traversed, // since the first three characters area select a list of routes - c.treePath = c.treePath[0:0] + c.treePath = "" const maxDetectionPaths = 3 if len(c.detectionPath) >= maxDetectionPaths { - c.treePath = c.detectionPath[:maxDetectionPaths] + // c.treePath is only used by Fiber and is not exposed to the user + // so we can use utils.UnsafeString instead of c.app.getString + c.treePath = utils.UnsafeString(c.detectionPath[:maxDetectionPaths]) } } @@ -1963,7 +1962,7 @@ func (c *DefaultCtx) getTreePath() string { } func (c *DefaultCtx) getDetectionPath() string { - return c.detectionPath + return c.app.getString(c.detectionPath) } func (c *DefaultCtx) getPathOriginal() string { diff --git a/router.go b/router.go index 795529bd..be289ca5 100644 --- a/router.go +++ b/router.go @@ -180,7 +180,7 @@ func (app *App) next(c *DefaultCtx) (bool, error) { } // Check if it matches the request path - match = route.match(c.detectionPath, c.path, &c.values) + match = route.match(utils.UnsafeString(c.detectionPath), utils.UnsafeString(c.path), &c.values) if !match { // No match, next route continue From 7606c618d3e4950e9be66c3cbaff8aa457aa78f3 Mon Sep 17 00:00:00 2001 From: Mazyar Yousefiniyae shad Date: Tue, 25 Mar 2025 10:55:56 +0330 Subject: [PATCH 39/53] =?UTF-8?q?=F0=9F=93=9A=20Doc:=20Add=20more=20valida?= =?UTF-8?q?tion=20examples=20(#3369)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add examples on valudator guid * ref: return prev validation comment --- docs/guide/validation.md | 71 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/docs/guide/validation.md b/docs/guide/validation.md index 7226347f..fd007a62 100644 --- a/docs/guide/validation.md +++ b/docs/guide/validation.md @@ -8,8 +8,7 @@ sidebar_position: 5 Fiber provides the [Bind](../api/bind.md#validation) function to validate and bind [request data](../api/bind.md#binders) to a struct. -```go title="Example" - +```go title="Basic Example" import "github.com/go-playground/validator/v10" type structValidator struct { @@ -42,3 +41,71 @@ app.Post("/", func(c fiber.Ctx) error { return c.JSON(user) }) ``` + +```go title="Advanced Validation Example" +type User struct { + Name string `json:"name" validate:"required,min=3,max=32"` + Email string `json:"email" validate:"required,email"` + Age int `json:"age" validate:"gte=0,lte=100"` + Password string `json:"password" validate:"required,min=8"` + Website string `json:"website" validate:"url"` +} + +// Custom validation error messages +type UserWithCustomMessages struct { + Name string `json:"name" validate:"required,min=3,max=32" message:"Name is required and must be between 3 and 32 characters"` + Email string `json:"email" validate:"required,email" message:"Valid email is required"` + Age int `json:"age" validate:"gte=0,lte=100" message:"Age must be between 0 and 100"` +} + +app.Post("/user", func(c fiber.Ctx) error { + user := new(User) + + if err := c.Bind().Body(user); err != nil { + // Handle validation errors + if validationErrors, ok := err.(validator.ValidationErrors); ok { + for _, e := range validationErrors { + // e.Field() - field name + // e.Tag() - validation tag + // e.Value() - invalid value + // e.Param() - validation parameter + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "field": e.Field(), + "error": e.Error(), + }) + } + } + return err + } + + return c.JSON(user) +}) +``` + +```go title="Custom Validator Example" +// Custom validator for password strength +type PasswordValidator struct { + validate *validator.Validate +} + +func (v *PasswordValidator) Validate(out any) error { + if err := v.validate.Struct(out); err != nil { + return err + } + + // Custom password validation logic + if user, ok := out.(*User); ok { + if len(user.Password) < 8 { + return errors.New("password must be at least 8 characters") + } + // Add more password validation rules here + } + + return nil +} + +// Usage +app := fiber.New(fiber.Config{ + StructValidator: &PasswordValidator{validate: validator.New()}, +}) +``` From 4bf292945df965cb5fcf0e36cbe7fd2abc288084 Mon Sep 17 00:00:00 2001 From: Kashiwa <13825170+ksw2000@users.noreply.github.com> Date: Tue, 25 Mar 2025 15:30:41 +0800 Subject: [PATCH 40/53] =?UTF-8?q?=E2=99=BB=EF=B8=8FRefactor:=20reduce=20De?= =?UTF-8?q?faultCtx=20from=20736=20to=20728=20bytes=20(#3368)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.go | 4 ++-- ctx.go | 22 ++++++++++++---------- ctx_interface.go | 2 +- ctx_interface_gen.go | 2 +- helpers.go | 8 ++++---- router.go | 30 ++++++++++++++---------------- 6 files changed, 34 insertions(+), 34 deletions(-) diff --git a/app.go b/app.go index 84059e79..d4d89d92 100644 --- a/app.go +++ b/app.go @@ -109,7 +109,7 @@ type App struct { // Route stack divided by HTTP methods stack [][]*Route // Route stack divided by HTTP methods and route prefixes - treeStack []map[string][]*Route + treeStack []map[int][]*Route // custom binders customBinders []CustomBinder // customConstraints is a list of external constraints @@ -581,7 +581,7 @@ func New(config ...Config) *App { // Create router stack app.stack = make([][]*Route, len(app.config.RequestMethods)) - app.treeStack = make([]map[string][]*Route, len(app.config.RequestMethods)) + app.treeStack = make([]map[int][]*Route, len(app.config.RequestMethods)) // Override colors app.config.ColorScheme = defaultColors(app.config.ColorScheme) diff --git a/ctx.go b/ctx.go index efd6006d..b959a48f 100644 --- a/ctx.go +++ b/ctx.go @@ -33,8 +33,11 @@ const ( schemeHTTPS = "https" ) -// maxParams defines the maximum number of parameters per route. -const maxParams = 30 +const ( + // maxParams defines the maximum number of parameters per route. + maxParams = 30 + maxDetectionPaths = 3 +) // The contextKey type is unexported to prevent collisions with context keys defined in // other packages. @@ -60,11 +63,11 @@ type DefaultCtx struct { viewBindMap sync.Map // Default view map to bind template engine method string // HTTP method baseURI string // HTTP base uri - treePath string // Path for the search in the tree pathOriginal string // Original HTTP path flashMessages redirectionMsgs // Flash messages path []byte // HTTP path with the modifications by the configuration detectionPath []byte // Route detection path + treePathHash int // Hash of the path for the search in the tree indexRoute int // Index of the current route indexHandler int // Index of the current handler methodINT int // HTTP method INT equivalent @@ -1851,12 +1854,11 @@ func (c *DefaultCtx) configDependentPaths() { // Define the path for dividing routes into areas for fast tree detection, so that fewer routes need to be traversed, // since the first three characters area select a list of routes - c.treePath = "" - const maxDetectionPaths = 3 + c.treePathHash = 0 if len(c.detectionPath) >= maxDetectionPaths { - // c.treePath is only used by Fiber and is not exposed to the user - // so we can use utils.UnsafeString instead of c.app.getString - c.treePath = utils.UnsafeString(c.detectionPath[:maxDetectionPaths]) + c.treePathHash = int(c.detectionPath[0])<<16 | + int(c.detectionPath[1])<<8 | + int(c.detectionPath[2]) } } @@ -1957,8 +1959,8 @@ func (c *DefaultCtx) getIndexRoute() int { return c.indexRoute } -func (c *DefaultCtx) getTreePath() string { - return c.treePath +func (c *DefaultCtx) getTreePathHash() int { + return c.treePathHash } func (c *DefaultCtx) getDetectionPath() string { diff --git a/ctx_interface.go b/ctx_interface.go index 32e8ee39..6ef33847 100644 --- a/ctx_interface.go +++ b/ctx_interface.go @@ -19,7 +19,7 @@ type CustomCtx interface { // Methods to use with next stack. getMethodINT() int getIndexRoute() int - getTreePath() string + getTreePathHash() int getDetectionPath() string getPathOriginal() string getValues() *[maxParams]string diff --git a/ctx_interface_gen.go b/ctx_interface_gen.go index fffe218d..a4d7db3d 100644 --- a/ctx_interface_gen.go +++ b/ctx_interface_gen.go @@ -347,7 +347,7 @@ type Ctx interface { // Methods to use with next stack. getMethodINT() int getIndexRoute() int - getTreePath() string + getTreePathHash() int getDetectionPath() string getPathOriginal() string getValues() *[maxParams]string diff --git a/helpers.go b/helpers.go index 584553a7..3f1685b1 100644 --- a/helpers.go +++ b/helpers.go @@ -113,9 +113,9 @@ func (app *App) methodExist(c *DefaultCtx) bool { // Reset stack index c.setIndexRoute(-1) - tree, ok := c.App().treeStack[i][c.getTreePath()] + tree, ok := c.App().treeStack[i][c.treePathHash] if !ok { - tree = c.App().treeStack[i][""] + tree = c.App().treeStack[i][0] } // Get stack length lenr := len(tree) - 1 @@ -157,9 +157,9 @@ func (app *App) methodExistCustom(c CustomCtx) bool { // Reset stack index c.setIndexRoute(-1) - tree, ok := c.App().treeStack[i][c.getTreePath()] + tree, ok := c.App().treeStack[i][c.getTreePathHash()] if !ok { - tree = c.App().treeStack[i][""] + tree = c.App().treeStack[i][0] } // Get stack length lenr := len(tree) - 1 diff --git a/router.go b/router.go index be289ca5..0aec6509 100644 --- a/router.go +++ b/router.go @@ -110,9 +110,9 @@ func (r *Route) match(detectionPath, path string, params *[maxParams]string) boo func (app *App) nextCustom(c CustomCtx) (bool, error) { //nolint:unparam // bool param might be useful for testing // Get stack length - tree, ok := app.treeStack[c.getMethodINT()][c.getTreePath()] + tree, ok := app.treeStack[c.getMethodINT()][c.getTreePathHash()] if !ok { - tree = app.treeStack[c.getMethodINT()][""] + tree = app.treeStack[c.getMethodINT()][0] } lenr := len(tree) - 1 @@ -158,9 +158,9 @@ func (app *App) nextCustom(c CustomCtx) (bool, error) { //nolint:unparam // bool func (app *App) next(c *DefaultCtx) (bool, error) { // Get stack length - tree, ok := app.treeStack[c.methodINT][c.treePath] + tree, ok := app.treeStack[c.methodINT][c.treePathHash] if !ok { - tree = app.treeStack[c.methodINT][""] + tree = app.treeStack[c.methodINT][0] } lenTree := len(tree) - 1 @@ -454,30 +454,28 @@ func (app *App) buildTree() *App { // loop all the methods and stacks and create the prefix tree for m := range app.config.RequestMethods { - tsMap := make(map[string][]*Route) + tsMap := make(map[int][]*Route) for _, route := range app.stack[m] { - treePath := "" - if len(route.routeParser.segs) > 0 && len(route.routeParser.segs[0].Const) >= 3 { - treePath = route.routeParser.segs[0].Const[:3] + treePathHash := 0 + if len(route.routeParser.segs) > 0 && len(route.routeParser.segs[0].Const) >= maxDetectionPaths { + treePathHash = int(route.routeParser.segs[0].Const[0])<<16 | + int(route.routeParser.segs[0].Const[1])<<8 | + int(route.routeParser.segs[0].Const[2]) } // create tree stack - tsMap[treePath] = append(tsMap[treePath], route) + tsMap[treePathHash] = append(tsMap[treePathHash], route) } - app.treeStack[m] = tsMap - } - // loop the methods and tree stacks and add global stack and sort everything - for m := range app.config.RequestMethods { - tsMap := app.treeStack[m] for treePart := range tsMap { - if treePart != "" { + if treePart != 0 { // merge global tree routes in current tree stack - tsMap[treePart] = uniqueRouteStack(append(tsMap[treePart], tsMap[""]...)) + tsMap[treePart] = uniqueRouteStack(append(tsMap[treePart], tsMap[0]...)) } // sort tree slices with the positions slc := tsMap[treePart] sort.Slice(slc, func(i, j int) bool { return slc[i].pos < slc[j].pos }) } + app.treeStack[m] = tsMap } app.routesRefreshed = false From dab20c9df585f67fd7b1d8edc1edd6d7233c94fc Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Wed, 26 Mar 2025 08:09:43 -0400 Subject: [PATCH 41/53] =?UTF-8?q?=F0=9F=A7=B9=20chore:=20Add=20Immutable?= =?UTF-8?q?=20benchmarks=20for=20default=20case=20(#3374)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add benchmarks for default case with Immutable --- router_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/router_test.go b/router_test.go index 0e6019e2..ecc53dbb 100644 --- a/router_test.go +++ b/router_test.go @@ -656,6 +656,50 @@ func Benchmark_Router_Next_Default_Parallel(b *testing.B) { }) } +// go test -v ./... -run=^$ -bench=Benchmark_Router_Next_Default_Immutable -benchmem -count=4 +func Benchmark_Router_Next_Default_Immutable(b *testing.B) { + app := New(Config{Immutable: true}) + app.Get("/", func(_ Ctx) error { + return nil + }) + + h := app.Handler() + + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(MethodGet) + fctx.Request.SetRequestURI("/") + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + h(fctx) + } +} + +// go test -benchmem -run=^$ -bench ^Benchmark_Router_Next_Default_Parallel_Immutable$ github.com/gofiber/fiber/v3 -count=1 +func Benchmark_Router_Next_Default_Parallel_Immutable(b *testing.B) { + app := New(Config{Immutable: true}) + app.Get("/", func(_ Ctx) error { + return nil + }) + + h := app.Handler() + + b.ReportAllocs() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(MethodGet) + fctx.Request.SetRequestURI("/") + + for pb.Next() { + h(fctx) + } + }) +} + // go test -v ./... -run=^$ -bench=Benchmark_Route_Match -benchmem -count=4 func Benchmark_Route_Match(b *testing.B) { var match bool From e90fe8afbc32245c085e7f643ba6579e495ae6a3 Mon Sep 17 00:00:00 2001 From: Kashiwa <13825170+ksw2000@users.noreply.github.com> Date: Wed, 26 Mar 2025 20:16:53 +0800 Subject: [PATCH 42/53] =?UTF-8?q?=E2=99=BB=EF=B8=8FRefactor:=20remove=20re?= =?UTF-8?q?dundant=20field=20`method`=20in=20`DefaultCtx`=20(#3372)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️Refactor: remove redundant field method in defaultCtx * ♻️Refactor: rename getMethodINT to getMethodInt --- app.go | 30 +++++++++++++++++++++--------- ctx.go | 26 +++++++++++--------------- ctx_interface.go | 2 +- ctx_interface_gen.go | 2 +- helpers.go | 35 ++++++++++++++++------------------- router.go | 12 ++++++------ 6 files changed, 56 insertions(+), 51 deletions(-) diff --git a/app.go b/app.go index d4d89d92..43ccaf9a 100644 --- a/app.go +++ b/app.go @@ -456,17 +456,29 @@ const ( DefaultWriteBufferSize = 4096 ) +const ( + methodGet = iota + methodHead + methodPost + methodPut + methodDelete + methodConnect + methodOptions + methodTrace + methodPatch +) + // HTTP methods enabled by default var DefaultMethods = []string{ - MethodGet, - MethodHead, - MethodPost, - MethodPut, - MethodDelete, - MethodConnect, - MethodOptions, - MethodTrace, - MethodPatch, + methodGet: MethodGet, + methodHead: MethodHead, + methodPost: MethodPost, + methodPut: MethodPut, + methodDelete: MethodDelete, + methodConnect: MethodConnect, + methodOptions: MethodOptions, + methodTrace: MethodTrace, + methodPatch: MethodPatch, } // DefaultErrorHandler that process return errors from handlers diff --git a/ctx.go b/ctx.go index b959a48f..1816185e 100644 --- a/ctx.go +++ b/ctx.go @@ -61,7 +61,6 @@ type DefaultCtx struct { res *DefaultRes // Default response api reference values [maxParams]string // Route parameter values viewBindMap sync.Map // Default view map to bind template engine - method string // HTTP method baseURI string // HTTP base uri pathOriginal string // Original HTTP path flashMessages redirectionMsgs // Flash messages @@ -70,7 +69,7 @@ type DefaultCtx struct { treePathHash int // Hash of the path for the search in the tree indexRoute int // Index of the current route indexHandler int // Index of the current handler - methodINT int // HTTP method INT equivalent + methodInt int // HTTP method INT equivalent matched bool // Non use route matched } @@ -1006,19 +1005,17 @@ func (c *DefaultCtx) Location(path string) { func (c *DefaultCtx) Method(override ...string) string { if len(override) == 0 { // Nothing to override, just return current method from context - return c.method + return c.app.method(c.methodInt) } method := utils.ToUpper(override[0]) - mINT := c.app.methodInt(method) - if mINT == -1 { + methodInt := c.app.methodInt(method) + if methodInt == -1 { // Provided override does not valid HTTP method, no override, return current method - return c.method + return c.app.method(c.methodInt) } - - c.method = method - c.methodINT = mINT - return c.method + c.methodInt = methodInt + return method } // MultipartForm parse form entries from binary. @@ -1486,7 +1483,7 @@ func (c *DefaultCtx) Route() *Route { return &Route{ path: c.pathOriginal, Path: c.pathOriginal, - Method: c.method, + Method: c.Method(), Handlers: make([]Handler, 0), Params: make([]string, 0), } @@ -1919,8 +1916,7 @@ func (c *DefaultCtx) Reset(fctx *fasthttp.RequestCtx) { // Set paths c.pathOriginal = c.app.getString(fctx.URI().PathOriginal()) // Set method - c.method = c.app.getString(fctx.Request.Header.Method()) - c.methodINT = c.app.methodInt(c.method) + c.methodInt = c.app.methodInt(utils.UnsafeString(fctx.Request.Header.Method())) // Attach *fasthttp.RequestCtx to ctx c.fasthttp = fctx // reset base uri @@ -1951,8 +1947,8 @@ func (c *DefaultCtx) getBody() []byte { } // Methods to use with next stack. -func (c *DefaultCtx) getMethodINT() int { - return c.methodINT +func (c *DefaultCtx) getMethodInt() int { + return c.methodInt } func (c *DefaultCtx) getIndexRoute() int { diff --git a/ctx_interface.go b/ctx_interface.go index 6ef33847..c2eb8bf4 100644 --- a/ctx_interface.go +++ b/ctx_interface.go @@ -17,7 +17,7 @@ type CustomCtx interface { Reset(fctx *fasthttp.RequestCtx) // Methods to use with next stack. - getMethodINT() int + getMethodInt() int getIndexRoute() int getTreePathHash() int getDetectionPath() string diff --git a/ctx_interface_gen.go b/ctx_interface_gen.go index a4d7db3d..df537e26 100644 --- a/ctx_interface_gen.go +++ b/ctx_interface_gen.go @@ -345,7 +345,7 @@ type Ctx interface { release() getBody() []byte // Methods to use with next stack. - getMethodINT() int + getMethodInt() int getIndexRoute() int getTreePathHash() int getDetectionPath() string diff --git a/helpers.go b/helpers.go index 3f1685b1..573aab3d 100644 --- a/helpers.go +++ b/helpers.go @@ -14,6 +14,7 @@ import ( "os" "path/filepath" "reflect" + "slices" "strconv" "strings" "sync" @@ -107,7 +108,7 @@ func (app *App) methodExist(c *DefaultCtx) bool { methods := app.config.RequestMethods for i := 0; i < len(methods); i++ { // Skip original method - if c.getMethodINT() == i { + if c.getMethodInt() == i { continue } // Reset stack index @@ -151,7 +152,7 @@ func (app *App) methodExistCustom(c CustomCtx) bool { methods := app.config.RequestMethods for i := 0; i < len(methods); i++ { // Skip original method - if c.getMethodINT() == i { + if c.getMethodInt() == i { continue } // Reset stack index @@ -652,39 +653,35 @@ func getBytesImmutable(s string) []byte { func (app *App) methodInt(s string) int { // For better performance if len(app.configured.RequestMethods) == 0 { - // TODO: Use iota instead switch s { case MethodGet: - return 0 + return methodGet case MethodHead: - return 1 + return methodHead case MethodPost: - return 2 + return methodPost case MethodPut: - return 3 + return methodPut case MethodDelete: - return 4 + return methodDelete case MethodConnect: - return 5 + return methodConnect case MethodOptions: - return 6 + return methodOptions case MethodTrace: - return 7 + return methodTrace case MethodPatch: - return 8 + return methodPatch default: return -1 } } - // For method customization - for i, v := range app.config.RequestMethods { - if s == v { - return i - } - } + return slices.Index(app.config.RequestMethods, s) +} - return -1 +func (app *App) method(methodInt int) string { + return app.config.RequestMethods[methodInt] } // IsMethodSafe reports whether the HTTP method is considered safe. diff --git a/router.go b/router.go index 0aec6509..ac5832bc 100644 --- a/router.go +++ b/router.go @@ -110,9 +110,9 @@ func (r *Route) match(detectionPath, path string, params *[maxParams]string) boo func (app *App) nextCustom(c CustomCtx) (bool, error) { //nolint:unparam // bool param might be useful for testing // Get stack length - tree, ok := app.treeStack[c.getMethodINT()][c.getTreePathHash()] + tree, ok := app.treeStack[c.getMethodInt()][c.getTreePathHash()] if !ok { - tree = app.treeStack[c.getMethodINT()][0] + tree = app.treeStack[c.getMethodInt()][0] } lenr := len(tree) - 1 @@ -158,9 +158,9 @@ func (app *App) nextCustom(c CustomCtx) (bool, error) { //nolint:unparam // bool func (app *App) next(c *DefaultCtx) (bool, error) { // Get stack length - tree, ok := app.treeStack[c.methodINT][c.treePathHash] + tree, ok := app.treeStack[c.methodInt][c.treePathHash] if !ok { - tree = app.treeStack[c.methodINT][0] + tree = app.treeStack[c.methodInt][0] } lenTree := len(tree) - 1 @@ -202,7 +202,7 @@ func (app *App) next(c *DefaultCtx) (bool, error) { } // If c.Next() does not match, return 404 - err := NewError(StatusNotFound, "Cannot "+c.method+" "+html.EscapeString(c.pathOriginal)) + err := NewError(StatusNotFound, "Cannot "+c.Method()+" "+html.EscapeString(c.pathOriginal)) if !c.matched && app.methodExist(c) { // If no match, scan stack again if other methods match the request // Moved from app.handler because middleware may break the route chain @@ -221,7 +221,7 @@ func (app *App) defaultRequestHandler(rctx *fasthttp.RequestCtx) { defer app.ReleaseCtx(ctx) // Check if the HTTP method is valid - if ctx.methodINT == -1 { + if ctx.methodInt == -1 { _ = ctx.SendStatus(StatusNotImplemented) //nolint:errcheck // Always return nil return } From 75281bd874f7f179c18adf3f55175b84409c8fb8 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Sun, 30 Mar 2025 05:46:52 -0400 Subject: [PATCH 43/53] =?UTF-8?q?=F0=9F=A7=B9=20chore:=20Simplify=20Health?= =?UTF-8?q?Check=20middleware=20(#3380)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Simplify middleware * Rename default endpoints --- docs/middleware/healthcheck.md | 32 ++++++++++------- docs/whats_new.md | 8 ++--- middleware/healthcheck/config.go | 12 +++---- middleware/healthcheck/healthcheck.go | 7 ++-- middleware/healthcheck/healthcheck_test.go | 42 +++++++++++----------- 5 files changed, 52 insertions(+), 49 deletions(-) diff --git a/docs/middleware/healthcheck.md b/docs/middleware/healthcheck.md index 2837c550..122f5768 100644 --- a/docs/middleware/healthcheck.md +++ b/docs/middleware/healthcheck.md @@ -27,7 +27,7 @@ Liveness, readiness and startup probes middleware for [Fiber](https://github.com ## Signatures ```go -func NewHealthChecker(config Config) fiber.Handler +func New(config Config) fiber.Handler ``` ## Examples @@ -41,38 +41,44 @@ import( ) ``` -After you initiate your [Fiber](https://github.com/gofiber/fiber) app, you can use the following possibilities: +After you initiate your [Fiber](https://github.com/gofiber/fiber) app, you can use the following options: ```go // Provide a minimal config for liveness check -app.Get(healthcheck.DefaultLivenessEndpoint, healthcheck.NewHealthChecker()) +app.Get(healthcheck.LivenessEndpoint, healthcheck.New()) + // Provide a minimal config for readiness check -app.Get(healthcheck.DefaultReadinessEndpoint, healthcheck.NewHealthChecker()) +app.Get(healthcheck.ReadinessEndpoint, healthcheck.New()) + // Provide a minimal config for startup check -app.Get(healthcheck.DefaultStartupEndpoint, healthcheck.NewHealthChecker()) +app.Get(healthcheck.StartupEndpoint, healthcheck.New()) + // Provide a minimal config for check with custom endpoint -app.Get("/live", healthcheck.NewHealthChecker()) +app.Get("/live", healthcheck.New()) // Or extend your config for customization -app.Get(healthcheck.DefaultLivenessEndpoint, healthcheck.NewHealthChecker(healthcheck.Config{ +app.Get(healthcheck.LivenessEndpoint, healthcheck.New(healthcheck.Config{ Probe: func(c fiber.Ctx) bool { return true }, })) + // And it works the same for readiness, just change the route -app.Get(healthcheck.DefaultReadinessEndpoint, healthcheck.NewHealthChecker(healthcheck.Config{ +app.Get(healthcheck.ReadinessEndpoint, healthcheck.New(healthcheck.Config{ Probe: func(c fiber.Ctx) bool { return true }, })) + // And it works the same for startup, just change the route -app.Get(healthcheck.DefaultStartupEndpoint, healthcheck.NewHealthChecker(healthcheck.Config{ +app.Get(healthcheck.StartupEndpoint, healthcheck.New(healthcheck.Config{ Probe: func(c fiber.Ctx) bool { return true }, })) + // With a custom route and custom probe -app.Get("/live", healthcheck.NewHealthChecker(healthcheck.Config{ +app.Get("/live", healthcheck.New(healthcheck.Config{ Probe: func(c fiber.Ctx) bool { return true }, @@ -81,7 +87,7 @@ app.Get("/live", healthcheck.NewHealthChecker(healthcheck.Config{ // It can also be used with app.All, although it will only respond to requests with the GET method // in case of calling the route with any method which isn't GET, the return will be 404 Not Found when app.All is used // and 405 Method Not Allowed when app.Get is used -app.All(healthcheck.DefaultReadinessEndpoint, healthcheck.NewHealthChecker(healthcheck.Config{ +app.All(healthcheck.ReadinessEndpoint, healthcheck.New(healthcheck.Config{ Probe: func(c fiber.Ctx) bool { return true }, @@ -108,7 +114,7 @@ type Config struct { // initialization and readiness checks // // Optional. Default: func(c fiber.Ctx) bool { return true } - Probe HealthChecker + Probe func(fiber.Ctx) bool } ``` @@ -117,7 +123,7 @@ type Config struct { The default configuration used by this middleware is defined as follows: ```go -func defaultProbe(fiber.Ctx) bool { return true } +func defaultProbe(_ fiber.Ctx) bool { return true } var ConfigDefault = Config{ Probe: defaultProbe, diff --git a/docs/whats_new.md b/docs/whats_new.md index bc569d3e..19f261ae 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -1564,25 +1564,25 @@ With the new version, each health check endpoint is configured separately, allow // after // Default liveness endpoint configuration -app.Get(healthcheck.DefaultLivenessEndpoint, healthcheck.NewHealthChecker(healthcheck.Config{ +app.Get(healthcheck.LivenessEndpoint, healthcheck.New(healthcheck.Config{ Probe: func(c fiber.Ctx) bool { return true }, })) // Default readiness endpoint configuration -app.Get(healthcheck.DefaultReadinessEndpoint, healthcheck.NewHealthChecker()) +app.Get(healthcheck.ReadinessEndpoint, healthcheck.New()) // New default startup endpoint configuration // Default endpoint is /startupz -app.Get(healthcheck.DefaultStartupEndpoint, healthcheck.NewHealthChecker(healthcheck.Config{ +app.Get(healthcheck.StartupEndpoint, healthcheck.New(healthcheck.Config{ Probe: func(c fiber.Ctx) bool { return serviceA.Ready() && serviceB.Ready() && ... }, })) // Custom liveness endpoint configuration -app.Get("/live", healthcheck.NewHealthChecker()) +app.Get("/live", healthcheck.New()) ``` #### Monitor diff --git a/middleware/healthcheck/config.go b/middleware/healthcheck/config.go index eba6d537..112f4039 100644 --- a/middleware/healthcheck/config.go +++ b/middleware/healthcheck/config.go @@ -18,18 +18,18 @@ type Config struct { // the application is in a state where it can handle requests (e.g., the server is up and running). // // Optional. Default: func(c fiber.Ctx) bool { return true } - Probe HealthChecker + Probe func(fiber.Ctx) bool } const ( - DefaultLivenessEndpoint = "/livez" - DefaultReadinessEndpoint = "/readyz" - DefaultStartupEndpoint = "/startupz" + LivenessEndpoint = "/livez" + ReadinessEndpoint = "/readyz" + StartupEndpoint = "/startupz" ) -func defaultProbe(fiber.Ctx) bool { return true } +func defaultProbe(_ fiber.Ctx) bool { return true } -func defaultConfigV3(config ...Config) Config { +func defaultConfig(config ...Config) Config { if len(config) < 1 { return Config{ Probe: defaultProbe, diff --git a/middleware/healthcheck/healthcheck.go b/middleware/healthcheck/healthcheck.go index 51a16d70..de2079ae 100644 --- a/middleware/healthcheck/healthcheck.go +++ b/middleware/healthcheck/healthcheck.go @@ -4,11 +4,8 @@ import ( "github.com/gofiber/fiber/v3" ) -// HealthChecker defines a function to check liveness or readiness of the application -type HealthChecker func(fiber.Ctx) bool - -func NewHealthChecker(config ...Config) fiber.Handler { - cfg := defaultConfigV3(config...) +func New(config ...Config) fiber.Handler { + cfg := defaultConfig(config...) return func(c fiber.Ctx) error { // Don't execute middleware if Next returns true diff --git a/middleware/healthcheck/healthcheck_test.go b/middleware/healthcheck/healthcheck_test.go index 07efa3de..bccfddde 100644 --- a/middleware/healthcheck/healthcheck_test.go +++ b/middleware/healthcheck/healthcheck_test.go @@ -34,9 +34,9 @@ func Test_HealthCheck_Strict_Routing_Default(t *testing.T) { StrictRouting: true, }) - app.Get(DefaultLivenessEndpoint, NewHealthChecker()) - app.Get(DefaultReadinessEndpoint, NewHealthChecker()) - app.Get(DefaultStartupEndpoint, NewHealthChecker()) + app.Get(LivenessEndpoint, New()) + app.Get(ReadinessEndpoint, New()) + app.Get(StartupEndpoint, New()) shouldGiveOK(t, app, "/readyz") shouldGiveOK(t, app, "/livez") @@ -53,9 +53,9 @@ func Test_HealthCheck_Default(t *testing.T) { t.Parallel() app := fiber.New() - app.Get(DefaultLivenessEndpoint, NewHealthChecker()) - app.Get(DefaultReadinessEndpoint, NewHealthChecker()) - app.Get(DefaultStartupEndpoint, NewHealthChecker()) + app.Get(LivenessEndpoint, New()) + app.Get(ReadinessEndpoint, New()) + app.Get(StartupEndpoint, New()) shouldGiveOK(t, app, "/readyz") shouldGiveOK(t, app, "/livez") @@ -73,12 +73,12 @@ func Test_HealthCheck_Custom(t *testing.T) { app := fiber.New() c1 := make(chan struct{}, 1) - app.Get("/live", NewHealthChecker(Config{ + app.Get("/live", New(Config{ Probe: func(_ fiber.Ctx) bool { return true }, })) - app.Get("/ready", NewHealthChecker(Config{ + app.Get("/ready", New(Config{ Probe: func(_ fiber.Ctx) bool { select { case <-c1: @@ -88,7 +88,7 @@ func Test_HealthCheck_Custom(t *testing.T) { } }, })) - app.Get(DefaultStartupEndpoint, NewHealthChecker(Config{ + app.Get(StartupEndpoint, New(Config{ Probe: func(_ fiber.Ctx) bool { return false }, @@ -123,12 +123,12 @@ func Test_HealthCheck_Custom_Nested(t *testing.T) { app := fiber.New() c1 := make(chan struct{}, 1) - app.Get("/probe/live", NewHealthChecker(Config{ + app.Get("/probe/live", New(Config{ Probe: func(_ fiber.Ctx) bool { return true }, })) - app.Get("/probe/ready", NewHealthChecker(Config{ + app.Get("/probe/ready", New(Config{ Probe: func(_ fiber.Ctx) bool { select { case <-c1: @@ -164,15 +164,15 @@ func Test_HealthCheck_Next(t *testing.T) { app := fiber.New() - checker := NewHealthChecker(Config{ + checker := New(Config{ Next: func(_ fiber.Ctx) bool { return true }, }) - app.Get(DefaultLivenessEndpoint, checker) - app.Get(DefaultReadinessEndpoint, checker) - app.Get(DefaultStartupEndpoint, checker) + app.Get(LivenessEndpoint, checker) + app.Get(ReadinessEndpoint, checker) + app.Get(StartupEndpoint, checker) // This should give not found since there are no other handlers to execute // so it's like the route isn't defined at all @@ -184,9 +184,9 @@ func Test_HealthCheck_Next(t *testing.T) { func Benchmark_HealthCheck(b *testing.B) { app := fiber.New() - app.Get(DefaultLivenessEndpoint, NewHealthChecker()) - app.Get(DefaultReadinessEndpoint, NewHealthChecker()) - app.Get(DefaultStartupEndpoint, NewHealthChecker()) + app.Get(LivenessEndpoint, New()) + app.Get(ReadinessEndpoint, New()) + app.Get(StartupEndpoint, New()) h := app.Handler() fctx := &fasthttp.RequestCtx{} @@ -206,9 +206,9 @@ func Benchmark_HealthCheck(b *testing.B) { func Benchmark_HealthCheck_Parallel(b *testing.B) { app := fiber.New() - app.Get(DefaultLivenessEndpoint, NewHealthChecker()) - app.Get(DefaultReadinessEndpoint, NewHealthChecker()) - app.Get(DefaultStartupEndpoint, NewHealthChecker()) + app.Get(LivenessEndpoint, New()) + app.Get(ReadinessEndpoint, New()) + app.Get(StartupEndpoint, New()) h := app.Handler() From d19e993597d88b7df3e83a5937f2248c57475e16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=2E=20Efe=20=C3=87etin?= Date: Mon, 31 Mar 2025 10:31:59 +0300 Subject: [PATCH 44/53] :sparkles: feat: Add support for application state management (#3360) * :sparkles: feat: add support for application state management * increase test coverage * fix linter * Fix typo * add GetStateWithDefault helper * add docs * update what's new * add has method * fix linter * update * Add missing helpers for golang built-in types * Fix lint issues * Fix unit-tests. Update documentation * Fix docs, add missing benchmarks * Fix tests file * Update default example and test * Apply suggestions from code review --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Co-authored-by: Juan Calderon-Perez Co-authored-by: RW --- app.go | 10 + app_test.go | 10 + docs/api/constants.md | 2 +- docs/api/state.md | 640 +++++++++++++++++++++++++++ docs/whats_new.md | 1 + state.go | 322 ++++++++++++++ state_test.go | 981 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 1965 insertions(+), 1 deletion(-) create mode 100644 docs/api/state.md create mode 100644 state.go create mode 100644 state_test.go diff --git a/app.go b/app.go index 43ccaf9a..ae16a618 100644 --- a/app.go +++ b/app.go @@ -106,6 +106,8 @@ type App struct { tlsHandler *TLSHandler // Mount fields mountFields *mountFields + // state management + state *State // Route stack divided by HTTP methods stack [][]*Route // Route stack divided by HTTP methods and route prefixes @@ -527,6 +529,9 @@ func New(config ...Config) *App { // Define mountFields app.mountFields = newMountFields(app) + // Define state + app.state = newState() + // Override config if provided if len(config) > 0 { app.config = config[0] @@ -964,6 +969,11 @@ func (app *App) Hooks() *Hooks { return app.hooks } +// State returns the state struct to store global data in order to share it between handlers. +func (app *App) State() *State { + return app.state +} + var ErrTestGotEmptyResponse = errors.New("test: got empty response") // TestConfig is a struct holding Test settings diff --git a/app_test.go b/app_test.go index 84606e6b..b5d5ed46 100644 --- a/app_test.go +++ b/app_test.go @@ -1890,6 +1890,16 @@ func Test_Route_Naming_Issue_2671_2685(t *testing.T) { require.Equal(t, "/simple-route", sRoute2.Path) } +func Test_App_State(t *testing.T) { + t.Parallel() + app := New() + + app.State().Set("key", "value") + str, ok := app.State().GetString("key") + require.True(t, ok) + require.Equal(t, "value", str) +} + // go test -v -run=^$ -bench=Benchmark_Communication_Flow -benchmem -count=4 func Benchmark_Communication_Flow(b *testing.B) { app := New() diff --git a/docs/api/constants.md b/docs/api/constants.md index 53bbb25c..70366de1 100644 --- a/docs/api/constants.md +++ b/docs/api/constants.md @@ -2,7 +2,7 @@ id: constants title: 📋 Constants description: Some constants for Fiber. -sidebar_position: 8 +sidebar_position: 9 --- ### HTTP methods were copied from net/http diff --git a/docs/api/state.md b/docs/api/state.md new file mode 100644 index 00000000..b22b9675 --- /dev/null +++ b/docs/api/state.md @@ -0,0 +1,640 @@ +# State Management + +This document details the state management functionality provided by Fiber, a thread-safe global key–value store used to store application dependencies and runtime data. The implementation is based on Go's `sync.Map`, ensuring concurrency safety. + +Below is the detailed description of all public methods and usage examples. + +## State Type + +`State` is a key–value store built on top of `sync.Map`. It allows storage and retrieval of dependencies and configurations in a Fiber application as well as thread–safe access to runtime data. + +### Definition + +```go +// State is a key–value store for Fiber's app, used as a global storage for the app's dependencies. +// It is a thread–safe implementation of a map[string]any, using sync.Map. +type State struct { + dependencies sync.Map +} +``` + +## Methods on State + +### Set + +Set adds or updates a key–value pair in the State. + +```go +// Set adds or updates a key–value pair in the State. +func (s *State) Set(key string, value any) +``` + +**Usage Example:** + +```go +app.State().Set("appName", "My Fiber App") +``` + +### Get + +Get retrieves a value from the State. + +```go title="Signature" +func (s *State) Get(key string) (any, bool) +``` + +**Usage Example:** + +```go +value, ok := app.State().Get("appName") +if ok { + fmt.Println("App Name:", value) +} +``` + +### MustGet + +MustGet retrieves a value from the State and panics if the key is not found. + +```go title="Signature" +func (s *State) MustGet(key string) any +``` + +**Usage Example:** + +```go +appName := app.State().MustGet("appName") +fmt.Println("App Name:", appName) +``` + +### Has + +Has checks if a key exists in the State. + +```go title="Signature"s +func (s *State) Has(key string) bool +``` + +**Usage Example:** + +```go +if app.State().Has("appName") { + fmt.Println("App Name is set.") +} +``` + +### Delete + +Delete removes a key–value pair from the State. + +```go title="Signature" +func (s *State) Delete(key string) +``` + +**Usage Example:** + +```go +app.State().Delete("obsoleteKey") +``` + +### Reset + +Reset removes all keys from the State. + +```go title="Signature" +func (s *State) Reset() +``` + +**Usage Example:** + +```go +app.State().Reset() +``` + +### Keys + +Keys returns a slice containing all keys present in the State. + +```go title="Signature" +func (s *State) Keys() []string +``` + +**Usage Example:** + +```go +keys := app.State().Keys() +fmt.Println("State Keys:", keys) +``` + +### Len + +Len returns the number of keys in the State. + +```go +// Len returns the number of keys in the State. +func (s *State) Len() int +``` + +**Usage Example:** + +```go +fmt.Printf("Total State Entries: %d\n", app.State().Len()) +``` + +### GetString + +GetString retrieves a string value from the State. It returns the string and a boolean indicating a successful type assertion. + +```go title="Signature" +func (s *State) GetString(key string) (string, bool) +``` + +**Usage Example:** + +```go +if appName, ok := app.State().GetString("appName"); ok { + fmt.Println("App Name:", appName) +} +``` + +### GetInt + +GetInt retrieves an integer value from the State. It returns the int and a boolean indicating a successful type assertion. + +```go title="Signature" +func (s *State) GetInt(key string) (int, bool) +``` + +**Usage Example:** + +```go +if count, ok := app.State().GetInt("userCount"); ok { + fmt.Printf("User Count: %d\n", count) +} +``` + +### GetBool + +GetBool retrieves a boolean value from the State. It returns the bool and a boolean indicating a successful type assertion. + +```go title="Signature" +func (s *State) GetBool(key string) (value, bool) +``` + +**Usage Example:** + +```go +if debug, ok := app.State().GetBool("debugMode"); ok { + fmt.Printf("Debug Mode: %v\n", debug) +} +``` + +### GetFloat64 + +GetFloat64 retrieves a float64 value from the State. It returns the float64 and a boolean indicating a successful type assertion. + +```go title="Signature" +func (s *State) GetFloat64(key string) (float64, bool) +``` + +**Usage Example:** + +```go title="Signature" +if ratio, ok := app.State().GetFloat64("scalingFactor"); ok { + fmt.Printf("Scaling Factor: %f\n", ratio) +} +``` + +### GetUint + +GetUint retrieves a `uint` value from the State. + +```go title="Signature" +func (s *State) GetUint(key string) (uint, bool) +``` + +**Usage Example:** + +```go +if val, ok := app.State().GetUint("maxConnections"); ok { + fmt.Printf("Max Connections: %d\n", val) +} +``` + +### GetInt8 + +GetInt8 retrieves an `int8` value from the State. + +```go title="Signature" +func (s *State) GetInt8(key string) (int8, bool) +``` + +**Usage Example:** + +```go +if val, ok := app.State().GetInt8("threshold"); ok { + fmt.Printf("Threshold: %d\n", val) +} +``` + +### GetInt16 + +GetInt16 retrieves an `int16` value from the State. + +```go title="Signature" +func (s *State) GetInt16(key string) (int16, bool) +``` + +**Usage Example:** + +```go +if val, ok := app.State().GetInt16("minValue"); ok { + fmt.Printf("Minimum Value: %d\n", val) +} +``` + +### GetInt32 + +GetInt32 retrieves an `int32` value from the State. + +```go title="Signature" +func (s *State) GetInt32(key string) (int32, bool) +``` + +**Usage Example:** + +```go +if val, ok := app.State().GetInt32("portNumber"); ok { + fmt.Printf("Port Number: %d\n", val) +} +``` + +### GetInt64 + +GetInt64 retrieves an `int64` value from the State. + +```go title="Signature" +func (s *State) GetInt64(key string) (int64, bool) +``` + +**Usage Example:** + +```go +if val, ok := app.State().GetInt64("fileSize"); ok { + fmt.Printf("File Size: %d\n", val) +} +``` + +### GetUint8 + +GetUint8 retrieves a `uint8` value from the State. + +```go title="Signature" +func (s *State) GetUint8(key string) (uint8, bool) +``` + +**Usage Example:** + +```go +if val, ok := app.State().GetUint8("byteValue"); ok { + fmt.Printf("Byte Value: %d\n", val) +} +``` + +### GetUint16 + +GetUint16 retrieves a `uint16` value from the State. + +```go title="Signature" +func (s *State) GetUint16(key string) (uint16, bool) +``` + +**Usage Example:** + +```go +if val, ok := app.State().GetUint16("limit"); ok { + fmt.Printf("Limit: %d\n", val) +} +``` + +### GetUint32 + +GetUint32 retrieves a `uint32` value from the State. + +```go title="Signature" +func (s *State) GetUint32(key string) (uint32, bool) +``` + +**Usage Example:** + +```go +if val, ok := app.State().GetUint32("timeout"); ok { + fmt.Printf("Timeout: %d\n", val) +} +``` + +### GetUint64 + +GetUint64 retrieves a `uint64` value from the State. + +```go title="Signature" +func (s *State) GetUint64(key string) (uint64, bool) +``` + +**Usage Example:** + +```go +if val, ok := app.State().GetUint64("maxSize"); ok { + fmt.Printf("Max Size: %d\n", val) +} +``` + +### GetUintptr + +GetUintptr retrieves a `uintptr` value from the State. + +```go title="Signature" +func (s *State) GetUintptr(key string) (uintptr, bool) +``` + +**Usage Example:** + +```go +if val, ok := app.State().GetUintptr("pointerValue"); ok { + fmt.Printf("Pointer Value: %d\n", val) +} +``` + +### GetFloat32 + +GetFloat32 retrieves a `float32` value from the State. + +```go title="Signature" +func (s *State) GetFloat32(key string) (float32, bool) +``` + +**Usage Example:** + +```go +if val, ok := app.State().GetFloat32("scalingFactor32"); ok { + fmt.Printf("Scaling Factor (float32): %f\n", val) +} +``` + +### GetComplex64 + +GetComplex64 retrieves a `complex64` value from the State. + +```go title="Signature" +func (s *State) GetComplex64(key string) (complex64, bool) +``` + +**Usage Example:** + +```go +if val, ok := app.State().GetComplex64("complexVal"); ok { + fmt.Printf("Complex Value (complex64): %v\n", val) +} +``` + +### GetComplex128 + +GetComplex128 retrieves a `complex128` value from the State. + +```go title="Signature" +func (s *State) GetComplex128(key string) (complex128, bool) +``` + +**Usage Example:** + +```go +if val, ok := app.State().GetComplex128("complexVal128"); ok { + fmt.Printf("Complex Value (complex128): %v\n", val) +} +``` + +## Generic Functions + +Fiber provides generic functions to retrieve state values with type safety and fallback options. + +### GetState + +GetState retrieves a value from the State and casts it to the desired type. It returns the cast value and a boolean indicating if the cast was successful. + +```go title="Signature" +func GetState[T any](s *State, key string) (T, bool) +``` + +**Usage Example:** + +```go +// Retrieve an integer value safely. +userCount, ok := GetState[int](app.State(), "userCount") +if ok { + fmt.Printf("User Count: %d\n", userCount) +} +``` + +### MustGetState + +MustGetState retrieves a value from the State and casts it to the desired type. It panics if the key is not found or if the type assertion fails. + +```go title="Signature" +func MustGetState[T any](s *State, key string) T +``` + +**Usage Example:** + +```go +// Retrieve the value or panic if it is not present. +config := MustGetState[string](app.State(), "configFile") +fmt.Println("Config File:", config) +``` + +### GetStateWithDefault + +GetStateWithDefault retrieves a value from the State, casting it to the desired type. If the key is not present, it returns the provided default value. + +```go title="Signature" +func GetStateWithDefault[T any](s *State, key string, defaultVal T) T +``` + +**Usage Example:** + +```go +// Retrieve a value with a default fallback. +requestCount := GetStateWithDefault[int](app.State(), "requestCount", 0) +fmt.Printf("Request Count: %d\n", requestCount) +``` + +## Comprehensive Examples + +### Example: Request Counter + +This example demonstrates how to track the number of requests using the State. + +```go +package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v3" +) + +func main() { + app := fiber.New() + + // Initialize state with a counter. + app.State().Set("requestCount", 0) + + // Middleware: Increase counter for every request. + app.Use(func(c fiber.Ctx) error { + count, _ := c.App().State().GetInt("requestCount") + app.State().Set("requestCount", count+1) + return c.Next() + }) + + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("Hello World!") + }) + + app.Get("/stats", func(c fiber.Ctx) error { + count, _ := c.App().State().Get("requestCount") + return c.SendString(fmt.Sprintf("Total requests: %d", count)) + }) + + app.Listen(":3000") +} +``` + +### Example: Environment–Specific Configuration + +This example shows how to configure different settings based on the environment. + +```go +package main + +import ( + "os" + + "github.com/gofiber/fiber/v3" +) + +func main() { + app := fiber.New() + + // Determine environment. + environment := os.Getenv("ENV") + if environment == "" { + environment = "development" + } + app.State().Set("environment", environment) + + // Set environment-specific configurations. + if environment == "development" { + app.State().Set("apiUrl", "http://localhost:8080/api") + app.State().Set("debug", true) + } else { + app.State().Set("apiUrl", "https://api.production.com") + app.State().Set("debug", false) + } + + app.Get("/config", func(c fiber.Ctx) error { + config := map[string]any{ + "environment": environment, + "apiUrl": fiber.GetStateWithDefault(c.App().State(), "apiUrl", ""), + "debug": fiber.GetStateWithDefault(c.App().State(), "debug", false), + } + return c.JSON(config) + }) + + app.Listen(":3000") +} +``` + +### Example: Dependency Injection with State Management + +This example demonstrates how to use the State for dependency injection in a Fiber application. + +```go +package main + +import ( + "context" + "fmt" + "log" + + "github.com/gofiber/fiber/v3" + "github.com/redis/go-redis/v9" +) + +type User struct { + ID int `query:"id"` + Name string `query:"name"` + Email string `query:"email"` +} + +func main() { + app := fiber.New() + ctx := context.Background() + + // Initialize Redis client. + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", + DB: 0, + }) + + // Check the Redis connection. + if err := rdb.Ping(ctx).Err(); err != nil { + log.Fatalf("Could not connect to Redis: %v", err) + } + + // Inject the Redis client into Fiber's State for dependency injection. + app.State().Set("redis", rdb) + + app.Get("/user/create", func(c fiber.Ctx) error { + var user User + if err := c.Bind().Query(&user); err != nil { + return c.Status(fiber.StatusBadRequest).SendString(err.Error()) + } + + // Save the user to the database. + rdb, ok := fiber.GetState[*redis.Client](c.App().State(), "redis") + if !ok { + return c.Status(fiber.StatusInternalServerError).SendString("Redis client not found") + } + + // Save the user to the database. + key := fmt.Sprintf("user:%d", user.ID) + err := rdb.HSet(ctx, key, "name", user.Name, "email", user.Email).Err() + if err != nil { + return c.Status(fiber.StatusInternalServerError).SendString(err.Error()) + } + + return c.JSON(user) + }) + + app.Get("/user/:id", func(c fiber.Ctx) error { + id := c.Params("id") + + rdb, ok := fiber.GetState[*redis.Client](c.App().State(), "redis") + if !ok { + return c.Status(fiber.StatusInternalServerError).SendString("Redis client not found") + } + + key := fmt.Sprintf("user:%s", id) + user, err := rdb.HGetAll(ctx, key).Result() + if err == redis.Nil { + return c.Status(fiber.StatusNotFound).SendString("User not found") + } else if err != nil { + return c.Status(fiber.StatusInternalServerError).SendString(err.Error()) + } + + return c.JSON(user) + }) + + app.Listen(":3000") +} +``` diff --git a/docs/whats_new.md b/docs/whats_new.md index 19f261ae..4bf28927 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -59,6 +59,7 @@ We have made several changes to the Fiber app, including: - **RegisterCustomBinder**: Allows for the registration of custom binders. - **RegisterCustomConstraint**: Allows for the registration of custom constraints. - **NewCtxFunc**: Introduces a new context function. +- **State**: Provides a global state for the application, which can be used to store and retrieve data across the application. Check out the [State](./api/state) method for further details. ### Removed Methods diff --git a/state.go b/state.go new file mode 100644 index 00000000..43687603 --- /dev/null +++ b/state.go @@ -0,0 +1,322 @@ +package fiber + +import ( + "sync" +) + +// State is a key-value store for Fiber's app in order to be used as a global storage for the app's dependencies. +// It's a thread-safe implementation of a map[string]any, using sync.Map. +type State struct { + dependencies sync.Map +} + +// NewState creates a new instance of State. +func newState() *State { + return &State{ + dependencies: sync.Map{}, + } +} + +// Set sets a key-value pair in the State. +func (s *State) Set(key string, value any) { + s.dependencies.Store(key, value) +} + +// Get retrieves a value from the State. +func (s *State) Get(key string) (any, bool) { + return s.dependencies.Load(key) +} + +// MustGet retrieves a value from the State and panics if the key is not found. +func (s *State) MustGet(key string) any { + if dep, ok := s.Get(key); ok { + return dep + } + + panic("state: dependency not found!") +} + +// Has checks if a key is present in the State. +// It returns a boolean indicating if the key is present. +func (s *State) Has(key string) bool { + _, ok := s.Get(key) + return ok +} + +// Delete removes a key-value pair from the State. +func (s *State) Delete(key string) { + s.dependencies.Delete(key) +} + +// Reset resets the State by removing all keys. +func (s *State) Reset() { + s.dependencies.Clear() +} + +// Keys returns a slice containing all keys present in the State. +func (s *State) Keys() []string { + keys := make([]string, 0) + s.dependencies.Range(func(key, _ any) bool { + keyStr, ok := key.(string) + if !ok { + return false + } + + keys = append(keys, keyStr) + return true + }) + + return keys +} + +// Len returns the number of keys in the State. +func (s *State) Len() int { + length := 0 + s.dependencies.Range(func(_, _ any) bool { + length++ + return true + }) + + return length +} + +// GetState retrieves a value from the State and casts it to the desired type. +// It returns the casted value and a boolean indicating if the cast was successful. +func GetState[T any](s *State, key string) (T, bool) { + dep, ok := s.Get(key) + + if ok { + depT, okCast := dep.(T) + return depT, okCast + } + + var zeroVal T + return zeroVal, false +} + +// MustGetState retrieves a value from the State and casts it to the desired type. +// It panics if the key is not found or if the type assertion fails. +func MustGetState[T any](s *State, key string) T { + dep, ok := GetState[T](s, key) + if !ok { + panic("state: dependency not found!") + } + + return dep +} + +// GetStateWithDefault retrieves a value from the State, +// casting it to the desired type. If the key is not present, +// it returns the provided default value. +func GetStateWithDefault[T any](s *State, key string, defaultVal T) T { + dep, ok := GetState[T](s, key) + if !ok { + return defaultVal + } + + return dep +} + +// GetString retrieves a string value from the State. +// It returns the string and a boolean indicating successful type assertion. +func (s *State) GetString(key string) (string, bool) { + dep, ok := s.Get(key) + if ok { + depString, okCast := dep.(string) + return depString, okCast + } + + return "", false +} + +// GetInt retrieves an integer value from the State. +// It returns the int and a boolean indicating successful type assertion. +func (s *State) GetInt(key string) (int, bool) { + dep, ok := s.Get(key) + if ok { + depInt, okCast := dep.(int) + return depInt, okCast + } + + return 0, false +} + +// GetBool retrieves a boolean value from the State. +// It returns the bool and a boolean indicating successful type assertion. +func (s *State) GetBool(key string) (value, ok bool) { //nolint:nonamedreturns // Better idea to use named returns here + dep, ok := s.Get(key) + if ok { + depBool, okCast := dep.(bool) + return depBool, okCast + } + + return false, false +} + +// GetFloat64 retrieves a float64 value from the State. +// It returns the float64 and a boolean indicating successful type assertion. +func (s *State) GetFloat64(key string) (float64, bool) { + dep, ok := s.Get(key) + if ok { + depFloat64, okCast := dep.(float64) + return depFloat64, okCast + } + + return 0, false +} + +// GetUint retrieves a uint value from the State. +// It returns the float64 and a boolean indicating successful type assertion. +func (s *State) GetUint(key string) (uint, bool) { + dep, ok := s.Get(key) + if ok { + if depUint, okCast := dep.(uint); okCast { + return depUint, true + } + } + return 0, false +} + +// GetInt8 retrieves an int8 value from the State. +// It returns the float64 and a boolean indicating successful type assertion. +func (s *State) GetInt8(key string) (int8, bool) { + dep, ok := s.Get(key) + if ok { + if depInt8, okCast := dep.(int8); okCast { + return depInt8, true + } + } + return 0, false +} + +// GetInt16 retrieves an int16 value from the State. +// It returns the float64 and a boolean indicating successful type assertion. +func (s *State) GetInt16(key string) (int16, bool) { + dep, ok := s.Get(key) + if ok { + if depInt16, okCast := dep.(int16); okCast { + return depInt16, true + } + } + return 0, false +} + +// GetInt32 retrieves an int32 value from the State. +// It returns the float64 and a boolean indicating successful type assertion. +func (s *State) GetInt32(key string) (int32, bool) { + dep, ok := s.Get(key) + if ok { + if depInt32, okCast := dep.(int32); okCast { + return depInt32, true + } + } + return 0, false +} + +// GetInt64 retrieves an int64 value from the State. +// It returns the float64 and a boolean indicating successful type assertion. +func (s *State) GetInt64(key string) (int64, bool) { + dep, ok := s.Get(key) + if ok { + if depInt64, okCast := dep.(int64); okCast { + return depInt64, true + } + } + return 0, false +} + +// GetUint8 retrieves a uint8 value from the State. +// It returns the float64 and a boolean indicating successful type assertion. +func (s *State) GetUint8(key string) (uint8, bool) { + dep, ok := s.Get(key) + if ok { + if depUint8, okCast := dep.(uint8); okCast { + return depUint8, true + } + } + return 0, false +} + +// GetUint16 retrieves a uint16 value from the State. +// It returns the float64 and a boolean indicating successful type assertion. +func (s *State) GetUint16(key string) (uint16, bool) { + dep, ok := s.Get(key) + if ok { + if depUint16, okCast := dep.(uint16); okCast { + return depUint16, true + } + } + return 0, false +} + +// GetUint32 retrieves a uint32 value from the State. +// It returns the float64 and a boolean indicating successful type assertion. +func (s *State) GetUint32(key string) (uint32, bool) { + dep, ok := s.Get(key) + if ok { + if depUint32, okCast := dep.(uint32); okCast { + return depUint32, true + } + } + return 0, false +} + +// GetUint64 retrieves a uint64 value from the State. +// It returns the float64 and a boolean indicating successful type assertion. +func (s *State) GetUint64(key string) (uint64, bool) { + dep, ok := s.Get(key) + if ok { + if depUint64, okCast := dep.(uint64); okCast { + return depUint64, true + } + } + return 0, false +} + +// GetUintptr retrieves a uintptr value from the State. +// It returns the float64 and a boolean indicating successful type assertion. +func (s *State) GetUintptr(key string) (uintptr, bool) { + dep, ok := s.Get(key) + if ok { + if depUintptr, okCast := dep.(uintptr); okCast { + return depUintptr, true + } + } + return 0, false +} + +// GetFloat32 retrieves a float32 value from the State. +// It returns the float64 and a boolean indicating successful type assertion. +func (s *State) GetFloat32(key string) (float32, bool) { + dep, ok := s.Get(key) + if ok { + if depFloat32, okCast := dep.(float32); okCast { + return depFloat32, true + } + } + return 0, false +} + +// GetComplex64 retrieves a complex64 value from the State. +// It returns the float64 and a boolean indicating successful type assertion. +func (s *State) GetComplex64(key string) (complex64, bool) { + dep, ok := s.Get(key) + if ok { + if depComplex64, okCast := dep.(complex64); okCast { + return depComplex64, true + } + } + return 0, false +} + +// GetComplex128 retrieves a complex128 value from the State. +// It returns the float64 and a boolean indicating successful type assertion. +func (s *State) GetComplex128(key string) (complex128, bool) { + dep, ok := s.Get(key) + if ok { + if depComplex128, okCast := dep.(complex128); okCast { + return depComplex128, true + } + } + return 0, false +} diff --git a/state_test.go b/state_test.go new file mode 100644 index 00000000..e96ea0dd --- /dev/null +++ b/state_test.go @@ -0,0 +1,981 @@ +package fiber + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestState_SetAndGet_WithApp(t *testing.T) { + t.Parallel() + // Create app + app := New() + + // test setting and getting a value + app.State().Set("foo", "bar") + val, ok := app.State().Get("foo") + require.True(t, ok) + require.Equal(t, "bar", val) + + // test key not found + _, ok = app.State().Get("unknown") + require.False(t, ok) +} + +func TestState_SetAndGet(t *testing.T) { + t.Parallel() + st := newState() + + // test setting and getting a value + st.Set("foo", "bar") + val, ok := st.Get("foo") + require.True(t, ok) + require.Equal(t, "bar", val) + + // test key not found + _, ok = st.Get("unknown") + require.False(t, ok) +} + +func TestState_GetString(t *testing.T) { + t.Parallel() + st := newState() + + st.Set("str", "hello") + s, ok := st.GetString("str") + require.True(t, ok) + require.Equal(t, "hello", s) + + // wrong type should return false + st.Set("num", 123) + s, ok = st.GetString("num") + require.False(t, ok) + require.Equal(t, "", s) + + // missing key should return false + s, ok = st.GetString("missing") + require.False(t, ok) + require.Equal(t, "", s) +} + +func TestState_GetInt(t *testing.T) { + t.Parallel() + st := newState() + + st.Set("num", 456) + i, ok := st.GetInt("num") + require.True(t, ok) + require.Equal(t, 456, i) + + // wrong type should return zero value + st.Set("str", "abc") + i, ok = st.GetInt("str") + require.False(t, ok) + require.Equal(t, 0, i) + + // missing key should return zero value + i, ok = st.GetInt("missing") + require.False(t, ok) + require.Equal(t, 0, i) +} + +func TestState_GetBool(t *testing.T) { + t.Parallel() + st := newState() + + st.Set("flag", true) + b, ok := st.GetBool("flag") + require.True(t, ok) + require.True(t, b) + + // wrong type + st.Set("num", 1) + b, ok = st.GetBool("num") + require.False(t, ok) + require.False(t, b) + + // missing key should return false + b, ok = st.GetBool("missing") + require.False(t, ok) + require.False(t, b) +} + +func TestState_GetFloat64(t *testing.T) { + t.Parallel() + st := newState() + + st.Set("pi", 3.14) + f, ok := st.GetFloat64("pi") + require.True(t, ok) + require.InDelta(t, 3.14, f, 0.0001) + + // wrong type should return zero value + st.Set("int", 10) + f, ok = st.GetFloat64("int") + require.False(t, ok) + require.InDelta(t, 0.0, f, 0.0001) + + // missing key should return zero value + f, ok = st.GetFloat64("missing") + require.False(t, ok) + require.InDelta(t, 0.0, f, 0.0001) +} + +func TestState_GetUint(t *testing.T) { + t.Parallel() + st := newState() + + st.Set("uint", uint(100)) + u, ok := st.GetUint("uint") + require.True(t, ok) + require.Equal(t, uint(100), u) + + st.Set("wrong", "not uint") + u, ok = st.GetUint("wrong") + require.False(t, ok) + require.Equal(t, uint(0), u) + + u, ok = st.GetUint("missing") + require.False(t, ok) + require.Equal(t, uint(0), u) +} + +func TestState_GetInt8(t *testing.T) { + t.Parallel() + st := newState() + + st.Set("int8", int8(10)) + i, ok := st.GetInt8("int8") + require.True(t, ok) + require.Equal(t, int8(10), i) + + st.Set("wrong", "not int8") + i, ok = st.GetInt8("wrong") + require.False(t, ok) + require.Equal(t, int8(0), i) + + i, ok = st.GetInt8("missing") + require.False(t, ok) + require.Equal(t, int8(0), i) +} + +func TestState_GetInt16(t *testing.T) { + t.Parallel() + st := newState() + + st.Set("int16", int16(200)) + i, ok := st.GetInt16("int16") + require.True(t, ok) + require.Equal(t, int16(200), i) + + st.Set("wrong", "not int16") + i, ok = st.GetInt16("wrong") + require.False(t, ok) + require.Equal(t, int16(0), i) + + i, ok = st.GetInt16("missing") + require.False(t, ok) + require.Equal(t, int16(0), i) +} + +func TestState_GetInt32(t *testing.T) { + t.Parallel() + st := newState() + + st.Set("int32", int32(3000)) + i, ok := st.GetInt32("int32") + require.True(t, ok) + require.Equal(t, int32(3000), i) + + st.Set("wrong", "not int32") + i, ok = st.GetInt32("wrong") + require.False(t, ok) + require.Equal(t, int32(0), i) + + i, ok = st.GetInt32("missing") + require.False(t, ok) + require.Equal(t, int32(0), i) +} + +func TestState_GetInt64(t *testing.T) { + t.Parallel() + st := newState() + + st.Set("int64", int64(4000)) + i, ok := st.GetInt64("int64") + require.True(t, ok) + require.Equal(t, int64(4000), i) + + st.Set("wrong", "not int64") + i, ok = st.GetInt64("wrong") + require.False(t, ok) + require.Equal(t, int64(0), i) + + i, ok = st.GetInt64("missing") + require.False(t, ok) + require.Equal(t, int64(0), i) +} + +func TestState_GetUint8(t *testing.T) { + t.Parallel() + st := newState() + + st.Set("uint8", uint8(20)) + u, ok := st.GetUint8("uint8") + require.True(t, ok) + require.Equal(t, uint8(20), u) + + st.Set("wrong", "not uint8") + u, ok = st.GetUint8("wrong") + require.False(t, ok) + require.Equal(t, uint8(0), u) + + u, ok = st.GetUint8("missing") + require.False(t, ok) + require.Equal(t, uint8(0), u) +} + +func TestState_GetUint16(t *testing.T) { + t.Parallel() + st := newState() + + st.Set("uint16", uint16(300)) + u, ok := st.GetUint16("uint16") + require.True(t, ok) + require.Equal(t, uint16(300), u) + + st.Set("wrong", "not uint16") + u, ok = st.GetUint16("wrong") + require.False(t, ok) + require.Equal(t, uint16(0), u) + + u, ok = st.GetUint16("missing") + require.False(t, ok) + require.Equal(t, uint16(0), u) +} + +func TestState_GetUint32(t *testing.T) { + t.Parallel() + st := newState() + + st.Set("uint32", uint32(400000)) + u, ok := st.GetUint32("uint32") + require.True(t, ok) + require.Equal(t, uint32(400000), u) + + st.Set("wrong", "not uint32") + u, ok = st.GetUint32("wrong") + require.False(t, ok) + require.Equal(t, uint32(0), u) + + u, ok = st.GetUint32("missing") + require.False(t, ok) + require.Equal(t, uint32(0), u) +} + +func TestState_GetUint64(t *testing.T) { + t.Parallel() + st := newState() + + st.Set("uint64", uint64(5000000)) + u, ok := st.GetUint64("uint64") + require.True(t, ok) + require.Equal(t, uint64(5000000), u) + + st.Set("wrong", "not uint64") + u, ok = st.GetUint64("wrong") + require.False(t, ok) + require.Equal(t, uint64(0), u) + + u, ok = st.GetUint64("missing") + require.False(t, ok) + require.Equal(t, uint64(0), u) +} + +func TestState_GetUintptr(t *testing.T) { + t.Parallel() + st := newState() + + var ptr uintptr = 12345 + st.Set("uintptr", ptr) + u, ok := st.GetUintptr("uintptr") + require.True(t, ok) + require.Equal(t, ptr, u) + + st.Set("wrong", "not uintptr") + u, ok = st.GetUintptr("wrong") + require.False(t, ok) + require.Equal(t, uintptr(0), u) + + u, ok = st.GetUintptr("missing") + require.False(t, ok) + require.Equal(t, uintptr(0), u) +} + +func TestState_GetFloat32(t *testing.T) { + t.Parallel() + st := newState() + + st.Set("float32", float32(3.14)) + f, ok := st.GetFloat32("float32") + require.True(t, ok) + require.InDelta(t, float32(3.14), f, 0.0001) + + st.Set("wrong", "not float32") + f, ok = st.GetFloat32("wrong") + require.False(t, ok) + require.InDelta(t, float32(0), f, 0.0001) + + f, ok = st.GetFloat32("missing") + require.False(t, ok) + require.InDelta(t, float32(0), f, 0.0001) +} + +func TestState_GetComplex64(t *testing.T) { + t.Parallel() + st := newState() + + var c complex64 = complex(2, 3) + st.Set("complex64", c) + cRes, ok := st.GetComplex64("complex64") + require.True(t, ok) + require.Equal(t, c, cRes) + + st.Set("wrong", "not complex64") + cRes, ok = st.GetComplex64("wrong") + require.False(t, ok) + require.Equal(t, complex64(0), cRes) + + cRes, ok = st.GetComplex64("missing") + require.False(t, ok) + require.Equal(t, complex64(0), cRes) +} + +func TestState_GetComplex128(t *testing.T) { + t.Parallel() + st := newState() + + c := complex(4, 5) + st.Set("complex128", c) + cRes, ok := st.GetComplex128("complex128") + require.True(t, ok) + require.Equal(t, c, cRes) + + st.Set("wrong", "not complex128") + cRes, ok = st.GetComplex128("wrong") + require.False(t, ok) + require.Equal(t, complex128(0), cRes) + + cRes, ok = st.GetComplex128("missing") + require.False(t, ok) + require.Equal(t, complex128(0), cRes) +} + +func TestState_MustGet(t *testing.T) { + t.Parallel() + st := newState() + + st.Set("exists", "value") + val := st.MustGet("exists") + require.Equal(t, "value", val) + + // must-get on missing key should panic + require.Panics(t, func() { + _ = st.MustGet("missing") + }) +} + +func TestState_Has(t *testing.T) { + t.Parallel() + + st := newState() + + st.Set("key", "value") + require.True(t, st.Has("key")) +} + +func TestState_Delete(t *testing.T) { + t.Parallel() + st := newState() + + st.Set("key", "value") + st.Delete("key") + _, ok := st.Get("key") + require.False(t, ok) +} + +func TestState_Reset(t *testing.T) { + t.Parallel() + st := newState() + + st.Set("a", 1) + st.Set("b", 2) + st.Reset() + require.Equal(t, 0, st.Len()) + require.Empty(t, st.Keys()) +} + +func TestState_Keys(t *testing.T) { + t.Parallel() + st := newState() + + keys := []string{"one", "two", "three"} + for _, k := range keys { + st.Set(k, k) + } + + returnedKeys := st.Keys() + require.ElementsMatch(t, keys, returnedKeys) +} + +func TestState_Len(t *testing.T) { + t.Parallel() + st := newState() + + require.Equal(t, 0, st.Len()) + + st.Set("a", "a") + require.Equal(t, 1, st.Len()) + + st.Set("b", "b") + require.Equal(t, 2, st.Len()) + + st.Delete("a") + require.Equal(t, 1, st.Len()) +} + +type testCase[T any] struct { //nolint:govet // It does not really matter for test + name string + key string + value any + expected T + ok bool +} + +func runGenericTest[T any](t *testing.T, getter func(*State, string) (T, bool), tests []testCase[T]) { + t.Helper() + + st := newState() + for _, tc := range tests { + st.Set(tc.key, tc.value) + got, ok := getter(st, tc.key) + require.Equal(t, tc.ok, ok, tc.name) + require.Equal(t, tc.expected, got, tc.name) + } +} + +func TestState_GetGeneric(t *testing.T) { + t.Parallel() + + runGenericTest[int](t, GetState[int], []testCase[int]{ + {"int correct conversion", "num", 42, 42, true}, + {"int wrong conversion from string", "str", "abc", 0, false}, + }) + + runGenericTest[string](t, GetState[string], []testCase[string]{ + {"string correct conversion", "strVal", "hello", "hello", true}, + {"string wrong conversion from int", "intVal", 100, "", false}, + }) + + runGenericTest[bool](t, GetState[bool], []testCase[bool]{ + {"bool correct conversion", "flag", true, true, true}, + {"bool wrong conversion from int", "intFlag", 1, false, false}, + }) + + runGenericTest[float64](t, GetState[float64], []testCase[float64]{ + {"float64 correct conversion", "pi", 3.14, 3.14, true}, + {"float64 wrong conversion from int", "intVal", 10, 0.0, false}, + }) +} + +func Test_MustGetStateGeneric(t *testing.T) { + t.Parallel() + st := newState() + + st.Set("flag", true) + flag := MustGetState[bool](st, "flag") + require.True(t, flag) + + // mismatched type should panic + require.Panics(t, func() { + _ = MustGetState[string](st, "flag") + }) + + // missing key should also panic + require.Panics(t, func() { + _ = MustGetState[string](st, "missing") + }) +} + +func Test_GetStateWithDefault(t *testing.T) { + t.Parallel() + st := newState() + + st.Set("flag", true) + flag := GetStateWithDefault(st, "flag", false) + require.True(t, flag) + + // mismatched type should return the default value + str := GetStateWithDefault(st, "flag", "default") + require.Equal(t, "default", str) + + // missing key should return the default value + flag = GetStateWithDefault(st, "missing", false) + require.False(t, flag) +} + +func BenchmarkState_Set(b *testing.B) { + b.ReportAllocs() + + st := newState() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := "key" + strconv.Itoa(i) + st.Set(key, i) + } +} + +func BenchmarkState_Get(b *testing.B) { + b.ReportAllocs() + + st := newState() + n := 1000 + // pre-populate the state + for i := 0; i < n; i++ { + key := "key" + strconv.Itoa(i) + st.Set(key, i) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := "key" + strconv.Itoa(i%n) + st.Get(key) + } +} + +func BenchmarkState_GetString(b *testing.B) { + b.ReportAllocs() + + st := newState() + n := 1000 + // pre-populate the state + for i := 0; i < n; i++ { + key := "key" + strconv.Itoa(i) + st.Set(key, strconv.Itoa(i)) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := "key" + strconv.Itoa(i%n) + st.GetString(key) + } +} + +func BenchmarkState_GetInt(b *testing.B) { + b.ReportAllocs() + + st := newState() + n := 1000 + // pre-populate the state + for i := 0; i < n; i++ { + key := "key" + strconv.Itoa(i) + st.Set(key, i) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := "key" + strconv.Itoa(i%n) + st.GetInt(key) + } +} + +func BenchmarkState_GetBool(b *testing.B) { + b.ReportAllocs() + + st := newState() + n := 1000 + // pre-populate the state + for i := 0; i < n; i++ { + key := "key" + strconv.Itoa(i) + st.Set(key, i%2 == 0) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := "key" + strconv.Itoa(i%n) + st.GetBool(key) + } +} + +func BenchmarkState_GetFloat64(b *testing.B) { + b.ReportAllocs() + + st := newState() + n := 1000 + // pre-populate the state + for i := 0; i < n; i++ { + key := "key" + strconv.Itoa(i) + st.Set(key, float64(i)) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := "key" + strconv.Itoa(i%n) + st.GetFloat64(key) + } +} + +func BenchmarkState_MustGet(b *testing.B) { + b.ReportAllocs() + + st := newState() + n := 1000 + // pre-populate the state + for i := 0; i < n; i++ { + key := "key" + strconv.Itoa(i) + st.Set(key, i) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := "key" + strconv.Itoa(i%n) + st.MustGet(key) + } +} + +func BenchmarkState_GetStateGeneric(b *testing.B) { + b.ReportAllocs() + + st := newState() + n := 1000 + // pre-populate the state + for i := 0; i < n; i++ { + key := "key" + strconv.Itoa(i) + st.Set(key, i) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := "key" + strconv.Itoa(i%n) + GetState[int](st, key) + } +} + +func BenchmarkState_MustGetStateGeneric(b *testing.B) { + b.ReportAllocs() + + st := newState() + n := 1000 + // pre-populate the state + for i := 0; i < n; i++ { + key := "key" + strconv.Itoa(i) + st.Set(key, i) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := "key" + strconv.Itoa(i%n) + MustGetState[int](st, key) + } +} + +func BenchmarkState_GetStateWithDefault(b *testing.B) { + b.ReportAllocs() + + st := newState() + n := 1000 + // pre-populate the state + for i := 0; i < n; i++ { + key := "key" + strconv.Itoa(i) + st.Set(key, i) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := "key" + strconv.Itoa(i%n) + GetStateWithDefault[int](st, key, 0) + } +} + +func BenchmarkState_Has(b *testing.B) { + b.ReportAllocs() + + st := newState() + n := 1000 + // pre-populate the state + for i := 0; i < n; i++ { + st.Set("key"+strconv.Itoa(i), i) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + st.Has("key" + strconv.Itoa(i%n)) + } +} + +func BenchmarkState_Delete(b *testing.B) { + b.ReportAllocs() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + st := newState() + st.Set("a", 1) + st.Delete("a") + } +} + +func BenchmarkState_Reset(b *testing.B) { + b.ReportAllocs() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + st := newState() + // add a fixed number of keys before clearing + for j := 0; j < 100; j++ { + st.Set("key"+strconv.Itoa(j), j) + } + st.Reset() + } +} + +func BenchmarkState_Keys(b *testing.B) { + b.ReportAllocs() + + st := newState() + n := 1000 + for i := 0; i < n; i++ { + st.Set("key"+strconv.Itoa(i), i) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = st.Keys() + } +} + +func BenchmarkState_Len(b *testing.B) { + b.ReportAllocs() + + st := newState() + n := 1000 + for i := 0; i < n; i++ { + st.Set("key"+strconv.Itoa(i), i) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = st.Len() + } +} + +func BenchmarkState_GetUint(b *testing.B) { + b.ReportAllocs() + st := newState() + n := 1000 + // Pre-populate the state with uint values. + for i := 0; i < n; i++ { + key := "key" + strconv.Itoa(i) + st.Set(key, uint(i)) //nolint:gosec // This is a test + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := "key" + strconv.Itoa(i%n) + st.GetUint(key) + } +} + +func BenchmarkState_GetInt8(b *testing.B) { + b.ReportAllocs() + st := newState() + n := 1000 + // Pre-populate the state with int8 values (using modulo to stay in range). + for i := 0; i < n; i++ { + key := "key" + strconv.Itoa(i) + st.Set(key, int8(i%128)) //nolint:gosec // This is a test + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := "key" + strconv.Itoa(i%n) + st.GetInt8(key) + } +} + +func BenchmarkState_GetInt16(b *testing.B) { + b.ReportAllocs() + st := newState() + n := 1000 + // Pre-populate the state with int16 values. + for i := 0; i < n; i++ { + key := "key" + strconv.Itoa(i) + st.Set(key, int16(i)) //nolint:gosec // This is a test + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := "key" + strconv.Itoa(i%n) + st.GetInt16(key) + } +} + +func BenchmarkState_GetInt32(b *testing.B) { + b.ReportAllocs() + st := newState() + n := 1000 + // Pre-populate the state with int32 values. + for i := 0; i < n; i++ { + key := "key" + strconv.Itoa(i) + st.Set(key, int32(i)) //nolint:gosec // This is a test + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := "key" + strconv.Itoa(i%n) + st.GetInt32(key) + } +} + +func BenchmarkState_GetInt64(b *testing.B) { + b.ReportAllocs() + st := newState() + n := 1000 + // Pre-populate the state with int64 values. + for i := 0; i < n; i++ { + key := "key" + strconv.Itoa(i) + st.Set(key, int64(i)) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := "key" + strconv.Itoa(i%n) + st.GetInt64(key) + } +} + +func BenchmarkState_GetUint8(b *testing.B) { + b.ReportAllocs() + st := newState() + n := 1000 + // Pre-populate the state with uint8 values. + for i := 0; i < n; i++ { + key := "key" + strconv.Itoa(i) + st.Set(key, uint8(i%256)) //nolint:gosec // This is a test + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := "key" + strconv.Itoa(i%n) + st.GetUint8(key) + } +} + +func BenchmarkState_GetUint16(b *testing.B) { + b.ReportAllocs() + st := newState() + n := 1000 + // Pre-populate the state with uint16 values. + for i := 0; i < n; i++ { + key := "key" + strconv.Itoa(i) + st.Set(key, uint16(i)) //nolint:gosec // This is a test + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := "key" + strconv.Itoa(i%n) + st.GetUint16(key) + } +} + +func BenchmarkState_GetUint32(b *testing.B) { + b.ReportAllocs() + st := newState() + n := 1000 + // Pre-populate the state with uint32 values. + for i := 0; i < n; i++ { + key := "key" + strconv.Itoa(i) + st.Set(key, uint32(i)) //nolint:gosec // This is a test + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := "key" + strconv.Itoa(i%n) + st.GetUint32(key) + } +} + +func BenchmarkState_GetUint64(b *testing.B) { + b.ReportAllocs() + st := newState() + n := 1000 + // Pre-populate the state with uint64 values. + for i := 0; i < n; i++ { + key := "key" + strconv.Itoa(i) + st.Set(key, uint64(i)) //nolint:gosec // This is a test + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := "key" + strconv.Itoa(i%n) + st.GetUint64(key) + } +} + +func BenchmarkState_GetUintptr(b *testing.B) { + b.ReportAllocs() + st := newState() + n := 1000 + // Pre-populate the state with uintptr values. + for i := 0; i < n; i++ { + key := "key" + strconv.Itoa(i) + st.Set(key, uintptr(i)) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := "key" + strconv.Itoa(i%n) + st.GetUintptr(key) + } +} + +func BenchmarkState_GetFloat32(b *testing.B) { + b.ReportAllocs() + st := newState() + n := 1000 + // Pre-populate the state with float32 values. + for i := 0; i < n; i++ { + key := "key" + strconv.Itoa(i) + st.Set(key, float32(i)) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := "key" + strconv.Itoa(i%n) + st.GetFloat32(key) + } +} + +func BenchmarkState_GetComplex64(b *testing.B) { + b.ReportAllocs() + st := newState() + n := 1000 + // Pre-populate the state with complex64 values. + for i := 0; i < n; i++ { + key := "key" + strconv.Itoa(i) + // Create a complex64 value with both real and imaginary parts. + st.Set(key, complex(float32(i), float32(i))) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := "key" + strconv.Itoa(i%n) + st.GetComplex64(key) + } +} + +func BenchmarkState_GetComplex128(b *testing.B) { + b.ReportAllocs() + st := newState() + n := 1000 + // Pre-populate the state with complex128 values. + for i := 0; i < n; i++ { + key := "key" + strconv.Itoa(i) + // Create a complex128 value with both real and imaginary parts. + st.Set(key, complex(float64(i), float64(i))) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := "key" + strconv.Itoa(i%n) + st.GetComplex128(key) + } +} From c2e39b75703219f6d1fcec11ab4b6f8aaa87c2f0 Mon Sep 17 00:00:00 2001 From: Kashiwa <13825170+ksw2000@users.noreply.github.com> Date: Mon, 31 Mar 2025 15:49:40 +0800 Subject: [PATCH 45/53] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20Add=20fi?= =?UTF-8?q?ndNextNonEscapedCharPosition=20for=20single-byte=20charset=20ca?= =?UTF-8?q?ses=20(#3378)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ♻️ Refactor: add findNextNonEscapedCharsetPosition to process a single-byte parameter ``` goos: linux goarch: amd64 pkg: github.com/gofiber/fiber/v3 cpu: AMD EPYC 9J14 96-Core Processor │ old.txt │ new.txt │ │ sec/op │ sec/op vs base │ _RoutePatternMatch//api/v1/const_|_match_|_/api/v1/const-16 160.4n ± 1% 159.0n ± 0% -0.84% (p=0.000 n=20) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1-16 151.6n ± 0% 150.8n ± 0% -0.53% (p=0.005 n=20) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/-16 151.7n ± 0% 150.6n ± 0% -0.73% (p=0.000 n=20) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/something-16 162.3n ± 0% 160.8n ± 0% -0.96% (p=0.000 n=20) _RoutePatternMatch//api/:param/fixedEnd_|_match_|_/api/abc/fixedEnd-16 452.9n ± 1% 435.8n ± 0% -3.79% (p=0.000 n=20) _RoutePatternMatch//api/:param/fixedEnd_|_not_match_|_/api/abc/def/fixedEnd-16 455.6n ± 1% 435.7n ± 0% -4.38% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity-16 524.4n ± 1% 507.6n ± 1% -3.19% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/-16 528.2n ± 0% 508.7n ± 0% -3.69% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/1-16 528.1n ± 0% 510.6n ± 0% -3.31% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v-16 500.3n ± 0% 489.0n ± 0% -2.27% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v2-16 502.1n ± 0% 489.9n ± 0% -2.44% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v1/-16 515.5n ± 0% 498.8n ± 0% -3.24% (p=0.000 n=20) geomean 339.4n 331.1n -2.46% │ old.txt │ new.txt │ │ B/op │ B/op vs base │ _RoutePatternMatch//api/v1/const_|_match_|_/api/v1/const-16 144.0 ± 0% 144.0 ± 0% ~ (p=1.000 n=20) ¹ _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1-16 136.0 ± 0% 136.0 ± 0% ~ (p=1.000 n=20) ¹ _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/-16 136.0 ± 0% 136.0 ± 0% ~ (p=1.000 n=20) ¹ _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/something-16 152.0 ± 0% 152.0 ± 0% ~ (p=1.000 n=20) ¹ _RoutePatternMatch//api/:param/fixedEnd_|_match_|_/api/abc/fixedEnd-16 368.0 ± 0% 368.0 ± 0% ~ (p=1.000 n=20) ¹ _RoutePatternMatch//api/:param/fixedEnd_|_not_match_|_/api/abc/def/fixedEnd-16 368.0 ± 0% 368.0 ± 0% ~ (p=1.000 n=20) ¹ _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity-16 432.0 ± 0% 432.0 ± 0% ~ (p=1.000 n=20) ¹ _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/-16 432.0 ± 0% 432.0 ± 0% ~ (p=1.000 n=20) ¹ _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/1-16 432.0 ± 0% 432.0 ± 0% ~ (p=1.000 n=20) ¹ _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v-16 424.0 ± 0% 424.0 ± 0% ~ (p=1.000 n=20) ¹ _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v2-16 424.0 ± 0% 424.0 ± 0% ~ (p=1.000 n=20) ¹ _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v1/-16 424.0 ± 0% 424.0 ± 0% ~ (p=1.000 n=20) ¹ geomean 288.8 288.8 +0.00% ¹ all samples are equal │ old.txt │ new.txt │ │ allocs/op │ allocs/op vs base │ _RoutePatternMatch//api/v1/const_|_match_|_/api/v1/const-16 4.000 ± 0% 4.000 ± 0% ~ (p=1.000 n=20) ¹ _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1-16 4.000 ± 0% 4.000 ± 0% ~ (p=1.000 n=20) ¹ _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/-16 4.000 ± 0% 4.000 ± 0% ~ (p=1.000 n=20) ¹ _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/something-16 4.000 ± 0% 4.000 ± 0% ~ (p=1.000 n=20) ¹ _RoutePatternMatch//api/:param/fixedEnd_|_match_|_/api/abc/fixedEnd-16 9.000 ± 0% 9.000 ± 0% ~ (p=1.000 n=20) ¹ _RoutePatternMatch//api/:param/fixedEnd_|_not_match_|_/api/abc/def/fixedEnd-16 9.000 ± 0% 9.000 ± 0% ~ (p=1.000 n=20) ¹ _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity-16 9.000 ± 0% 9.000 ± 0% ~ (p=1.000 n=20) ¹ _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/-16 9.000 ± 0% 9.000 ± 0% ~ (p=1.000 n=20) ¹ _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/1-16 9.000 ± 0% 9.000 ± 0% ~ (p=1.000 n=20) ¹ _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v-16 9.000 ± 0% 9.000 ± 0% ~ (p=1.000 n=20) ¹ _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v2-16 9.000 ± 0% 9.000 ± 0% ~ (p=1.000 n=20) ¹ _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v1/-16 9.000 ± 0% 9.000 ± 0% ~ (p=1.000 n=20) ¹ geomean 6.868 6.868 +0.00% ¹ all samples are equal ``` --- path.go | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/path.go b/path.go index b188a41c..8cfde73f 100644 --- a/path.go +++ b/path.go @@ -123,16 +123,6 @@ var ( parameterDelimiterChars = append([]byte{paramStarterChar, escapeChar}, routeDelimiter...) // list of chars to find the end of a parameter parameterEndChars = append([]byte{optionalParam}, parameterDelimiterChars...) - // list of parameter constraint start - parameterConstraintStartChars = []byte{paramConstraintStart} - // list of parameter constraint end - parameterConstraintEndChars = []byte{paramConstraintEnd} - // list of parameter separator - parameterConstraintSeparatorChars = []byte{paramConstraintSeparator} - // list of parameter constraint data start - parameterConstraintDataStartChars = []byte{paramConstraintDataStart} - // list of parameter constraint data separator - parameterConstraintDataSeparatorChars = []byte{paramConstraintDataSeparator} ) // RoutePatternMatch checks if a given path matches a Fiber route pattern. @@ -337,8 +327,8 @@ func (parser *routeParser) analyseParameterPart(pattern string, customConstraint // find constraint part if exists in the parameter part and remove it if parameterEndPosition > 0 { - parameterConstraintStart = findNextNonEscapedCharsetPosition(pattern[0:parameterEndPosition], parameterConstraintStartChars) - parameterConstraintEnd = strings.LastIndexByte(pattern[0:parameterEndPosition+1], paramConstraintEnd) + parameterConstraintStart = findNextNonEscapedCharPosition(pattern[:parameterEndPosition], paramConstraintStart) + parameterConstraintEnd = strings.LastIndexByte(pattern[:parameterEndPosition+1], paramConstraintEnd) } // cut params part @@ -351,11 +341,11 @@ func (parser *routeParser) analyseParameterPart(pattern string, customConstraint if hasConstraint := parameterConstraintStart != -1 && parameterConstraintEnd != -1; hasConstraint { constraintString := pattern[parameterConstraintStart+1 : parameterConstraintEnd] - userConstraints := splitNonEscaped(constraintString, string(parameterConstraintSeparatorChars)) + userConstraints := splitNonEscaped(constraintString, paramConstraintSeparator) constraints = make([]*Constraint, 0, len(userConstraints)) for _, c := range userConstraints { - start := findNextNonEscapedCharsetPosition(c, parameterConstraintDataStartChars) + start := findNextNonEscapedCharPosition(c, paramConstraintDataStart) end := strings.LastIndexByte(c, paramConstraintDataEnd) // Assign constraint @@ -368,7 +358,7 @@ func (parser *routeParser) analyseParameterPart(pattern string, customConstraint // remove escapes from data if constraint.ID != regexConstraint { - constraint.Data = splitNonEscaped(c[start+1:end], string(parameterConstraintDataSeparatorChars)) + constraint.Data = splitNonEscaped(c[start+1:end], paramConstraintDataSeparator) if len(constraint.Data) == 1 { constraint.Data[0] = RemoveEscapeChar(constraint.Data[0]) } else if len(constraint.Data) == 2 { // This is fine, we simply expect two parts @@ -432,11 +422,11 @@ func findNextCharsetPosition(search string, charset []byte) int { return nextPosition } -// findNextCharsetPositionConstraint search the next char position from the charset +// findNextCharsetPositionConstraint searches the next char position from the charset // unlike findNextCharsetPosition, it takes care of constraint start-end chars to parse route pattern func findNextCharsetPositionConstraint(search string, charset []byte) int { - constraintStart := findNextNonEscapedCharsetPosition(search, parameterConstraintStartChars) - constraintEnd := findNextNonEscapedCharsetPosition(search, parameterConstraintEndChars) + constraintStart := findNextNonEscapedCharPosition(search, paramConstraintStart) + constraintEnd := findNextNonEscapedCharPosition(search, paramConstraintEnd) nextPosition := -1 for _, char := range charset { @@ -452,7 +442,7 @@ func findNextCharsetPositionConstraint(search string, charset []byte) int { return nextPosition } -// findNextNonEscapedCharsetPosition search the next char position from the charset and skip the escaped characters +// findNextNonEscapedCharsetPosition searches the next char position from the charset and skips the escaped characters func findNextNonEscapedCharsetPosition(search string, charset []byte) int { pos := findNextCharsetPosition(search, charset) for pos > 0 && search[pos-1] == escapeChar { @@ -471,16 +461,26 @@ func findNextNonEscapedCharsetPosition(search string, charset []byte) int { return pos } +// findNextNonEscapedCharPosition searches the next char position and skips the escaped characters +func findNextNonEscapedCharPosition(search string, char byte) int { + for i := 0; i < len(search); i++ { + if search[i] == char && (i == 0 || search[i-1] != escapeChar) { + return i + } + } + return -1 +} + // splitNonEscaped slices s into all substrings separated by sep and returns a slice of the substrings between those separators // This function also takes a care of escape char when splitting. -func splitNonEscaped(s, sep string) []string { +func splitNonEscaped(s string, sep byte) []string { var result []string - i := findNextNonEscapedCharsetPosition(s, []byte(sep)) + i := findNextNonEscapedCharPosition(s, sep) for i > -1 { result = append(result, s[:i]) - s = s[i+len(sep):] - i = findNextNonEscapedCharsetPosition(s, []byte(sep)) + s = s[i+1:] + i = findNextNonEscapedCharPosition(s, sep) } return append(result, s) From d8f9548650f8629f5f86c54bffd8f9e29ef14b3b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 12:07:59 +0000 Subject: [PATCH 46/53] build(deps): bump github.com/fxamacker/cbor/v2 from 2.7.0 to 2.8.0 Bumps [github.com/fxamacker/cbor/v2](https://github.com/fxamacker/cbor) from 2.7.0 to 2.8.0. - [Release notes](https://github.com/fxamacker/cbor/releases) - [Commits](https://github.com/fxamacker/cbor/compare/v2.7.0...v2.8.0) --- updated-dependencies: - dependency-name: github.com/fxamacker/cbor/v2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 4 +++- go.sum | 6 ++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index f4cd17c1..dea86813 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/gofiber/fiber/v3 go 1.23.0 +toolchain go1.24.1 + require ( github.com/gofiber/schema v1.3.0 github.com/gofiber/utils/v2 v2.0.0-beta.7 @@ -18,7 +20,7 @@ require ( require ( github.com/andybalholm/brotli v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // direct + github.com/fxamacker/cbor/v2 v2.8.0 // direct github.com/klauspost/compress v1.17.11 // indirect github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index 71824581..81b9a920 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7X github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= +github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gofiber/schema v1.3.0 h1:K3F3wYzAY+aivfCCEHPufCthu5/13r/lzp1nuk6mr3Q= github.com/gofiber/schema v1.3.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c= github.com/gofiber/utils/v2 v2.0.0-beta.7 h1:NnHFrRHvhrufPABdWajcKZejz9HnCWmT/asoxRsiEbQ= @@ -34,8 +34,6 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From 2f794d9f88898c998e6bb611c9ce6f4c15223bc8 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Mon, 31 Mar 2025 08:14:18 -0400 Subject: [PATCH 47/53] Update go.mod --- go.mod | 1 - 1 file changed, 1 deletion(-) diff --git a/go.mod b/go.mod index dea86813..39f1a36d 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,6 @@ module github.com/gofiber/fiber/v3 go 1.23.0 -toolchain go1.24.1 require ( github.com/gofiber/schema v1.3.0 From dec28010e9cd6ba93c2e4439695229720ce0ea1e Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Mon, 31 Mar 2025 08:14:32 -0400 Subject: [PATCH 48/53] Update go.mod --- go.mod | 1 - 1 file changed, 1 deletion(-) diff --git a/go.mod b/go.mod index 39f1a36d..f5490bf1 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,6 @@ module github.com/gofiber/fiber/v3 go 1.23.0 - require ( github.com/gofiber/schema v1.3.0 github.com/gofiber/utils/v2 v2.0.0-beta.7 From bb12633c8ba8f085e9a568de49e505476d51b2d0 Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Mon, 31 Mar 2025 11:55:01 -0300 Subject: [PATCH 49/53] =?UTF-8?q?Revert=20"=F0=9F=94=A5=20feat:=20Add=20su?= =?UTF-8?q?pport=20for=20context.Context=20in=20keyauth=20middleware"=20(#?= =?UTF-8?q?3364)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert "🔥 feat: Add support for context.Context in keyauth middleware (#3287)" This reverts commit 4177ab4086a97648553f34bcff2ff81a137d31f3. Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> --- middleware/keyauth/keyauth.go | 22 ++------ middleware/keyauth/keyauth_test.go | 82 +++++++++--------------------- 2 files changed, 29 insertions(+), 75 deletions(-) diff --git a/middleware/keyauth/keyauth.go b/middleware/keyauth/keyauth.go index 54ecdbe5..e245ba42 100644 --- a/middleware/keyauth/keyauth.go +++ b/middleware/keyauth/keyauth.go @@ -2,7 +2,6 @@ package keyauth import ( - "context" "errors" "fmt" "net/url" @@ -60,10 +59,7 @@ func New(config ...Config) fiber.Handler { valid, err := cfg.Validator(c, key) if err == nil && valid { - // Store in both Locals and Context c.Locals(tokenKey, key) - ctx := context.WithValue(c.Context(), tokenKey, key) - c.SetContext(ctx) return cfg.SuccessHandler(c) } return cfg.ErrorHandler(c, err) @@ -72,20 +68,12 @@ func New(config ...Config) fiber.Handler { // TokenFromContext returns the bearer token from the request context. // returns an empty string if the token does not exist -func TokenFromContext(c any) string { - switch ctx := c.(type) { - case context.Context: - if token, ok := ctx.Value(tokenKey).(string); ok { - return token - } - case fiber.Ctx: - if token, ok := ctx.Locals(tokenKey).(string); ok { - return token - } - default: - panic("unsupported context type, expected fiber.Ctx or context.Context") +func TokenFromContext(c fiber.Ctx) string { + token, ok := c.Locals(tokenKey).(string) + if !ok { + return "" } - return "" + return token } // MultipleKeySourceLookup creates a CustomKeyLookup function that checks multiple sources until one is found diff --git a/middleware/keyauth/keyauth_test.go b/middleware/keyauth/keyauth_test.go index 27c4e5a0..72c9d3c1 100644 --- a/middleware/keyauth/keyauth_test.go +++ b/middleware/keyauth/keyauth_test.go @@ -503,67 +503,33 @@ func Test_TokenFromContext_None(t *testing.T) { } func Test_TokenFromContext(t *testing.T) { - // Test that TokenFromContext returns the correct token - t.Run("fiber.Ctx", func(t *testing.T) { - app := fiber.New() - app.Use(New(Config{ - KeyLookup: "header:Authorization", - AuthScheme: "Basic", - Validator: func(_ fiber.Ctx, key string) (bool, error) { - if key == CorrectKey { - return true, nil - } - return false, ErrMissingOrMalformedAPIKey - }, - })) - app.Get("/", func(c fiber.Ctx) error { - return c.SendString(TokenFromContext(c)) - }) - - req := httptest.NewRequest(fiber.MethodGet, "/", nil) - req.Header.Add("Authorization", "Basic "+CorrectKey) - res, err := app.Test(req) - require.NoError(t, err) - - body, err := io.ReadAll(res.Body) - require.NoError(t, err) - require.Equal(t, CorrectKey, string(body)) + app := fiber.New() + // Wire up keyauth middleware to set TokenFromContext now + app.Use(New(Config{ + KeyLookup: "header:Authorization", + AuthScheme: "Basic", + Validator: func(_ fiber.Ctx, key string) (bool, error) { + if key == CorrectKey { + return true, nil + } + return false, ErrMissingOrMalformedAPIKey + }, + })) + // Define a test handler that checks TokenFromContext + app.Get("/", func(c fiber.Ctx) error { + return c.SendString(TokenFromContext(c)) }) - t.Run("context.Context", func(t *testing.T) { - app := fiber.New() - app.Use(New(Config{ - KeyLookup: "header:Authorization", - AuthScheme: "Basic", - Validator: func(_ fiber.Ctx, key string) (bool, error) { - if key == CorrectKey { - return true, nil - } - return false, ErrMissingOrMalformedAPIKey - }, - })) - // Verify that TokenFromContext works with context.Context - app.Get("/", func(c fiber.Ctx) error { - ctx := c.Context() - token := TokenFromContext(ctx) - return c.SendString(token) - }) + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + req.Header.Add("Authorization", "Basic "+CorrectKey) + // Send + res, err := app.Test(req) + require.NoError(t, err) - req := httptest.NewRequest(fiber.MethodGet, "/", nil) - req.Header.Add("Authorization", "Basic "+CorrectKey) - res, err := app.Test(req) - require.NoError(t, err) - - body, err := io.ReadAll(res.Body) - require.NoError(t, err) - require.Equal(t, CorrectKey, string(body)) - }) - - t.Run("invalid context type", func(t *testing.T) { - require.Panics(t, func() { - _ = TokenFromContext("invalid") - }) - }) + // Read the response body into a string + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, CorrectKey, string(body)) } func Test_AuthSchemeToken(t *testing.T) { From c5c7f86d85d91718692433b7b031c9f78bc9a16e Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Tue, 1 Apr 2025 14:48:19 +0800 Subject: [PATCH 50/53] =?UTF-8?q?=F0=9F=94=A5=20Feature:=20Enhance=20Check?= =?UTF-8?q?Constraint=20method=20for=20improved=20error=20handling=20(#335?= =?UTF-8?q?6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔥 Feature: Enhance CheckConstraint method for improved error handling * Revert "🔥 Feature: Enhance CheckConstraint method for improved error handling" This reverts commit 68e8777b4cdb10702c3511a65ba76986a379a188. * Reapply "🔥 Feature: Enhance CheckConstraint method for improved error handling" This reverts commit 9e6c8e68df5ad43d869b6a7494e93b6fc41b4a40. * 🚨 Test: Add comprehensive tests for CheckConstraint method with various constraint scenarios * 🩹 Fix: lint error * 🩹 Fix: Update CheckConstraint method to return true for noConstraint and improve error handling * ♻️ Refactor: Remove unused CheckConstraint test cases and reorganize benchmark test cases for clarity * ♻️ Refactor: Remove outdated test cases from path_testcases_test.go and clean up CheckConstraint method in path.go * 📚 Doc: Update custom constraints section to clarify overriding behavior * 🔥 Feature: Enhance CheckConstraint method for improved error handling * Revert "🔥 Feature: Enhance CheckConstraint method for improved error handling" This reverts commit 68e8777b4cdb10702c3511a65ba76986a379a188. * Reapply "🔥 Feature: Enhance CheckConstraint method for improved error handling" This reverts commit 9e6c8e68df5ad43d869b6a7494e93b6fc41b4a40. * 🚨 Test: Add comprehensive tests for CheckConstraint method with various constraint scenarios * 🩹 Fix: lint error * 🩹 Fix: Update CheckConstraint method to return true for noConstraint and improve error handling * ♻️ Refactor: Remove unused CheckConstraint test cases and reorganize benchmark test cases for clarity * ♻️ Refactor: Remove outdated test cases from path_testcases_test.go and clean up CheckConstraint method in path.go * 📚 Doc: Update custom constraints section to clarify overriding behavior * 📚 Doc: Add caution note about custom constraints overriding built-in constraints in routing guide --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Co-authored-by: RW --- docs/guide/routing.md | 4 ++++ path.go | 39 +++++++++++++++++++++++++++------------ path_testcases_test.go | 8 ++++++++ 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/docs/guide/routing.md b/docs/guide/routing.md index 450932cc..8953dc08 100644 --- a/docs/guide/routing.md +++ b/docs/guide/routing.md @@ -250,6 +250,10 @@ app.Get("/:test?", func(c fiber.Ctx) error { Custom constraints can be added to Fiber using the `app.RegisterCustomConstraint` method. Your constraints have to be compatible with the `CustomConstraint` interface. +:::caution +Attention, custom constraints can now override built-in constraints. If a custom constraint has the same name as a built-in constraint, the custom constraint will be used instead. This allows for more flexibility in defining route parameter constraints. +::: + It is a good idea to add external constraints to your project once you want to add more specific rules to your routes. For example, you can add a constraint to check if a parameter is a valid ULID. diff --git a/path.go b/path.go index 8cfde73f..05589884 100644 --- a/path.go +++ b/path.go @@ -672,12 +672,25 @@ func getParamConstraintType(constraintPart string) TypeConstraint { } } -//nolint:errcheck // TODO: Properly check _all_ errors in here, log them & immediately return +// CheckConstraint validates if a param matches the given constraint +// Returns true if the param passes the constraint check, false otherwise +// +//nolint:errcheck // TODO: Properly check _all_ errors in here, log them or immediately return func (c *Constraint) CheckConstraint(param string) bool { - var err error - var num int + // First check if there's a custom constraint with the same name + // This allows custom constraints to override built-in constraints + for _, cc := range c.customConstraints { + if cc.Name() == c.Name { + return cc.Execute(param, c.Data...) + } + } - // check data exists + var ( + err error + num int + ) + + // Validate constraint has required data needOneData := []TypeConstraint{minLenConstraint, maxLenConstraint, lenConstraint, minConstraint, maxConstraint, datetimeConstraint, regexConstraint} needTwoData := []TypeConstraint{betweenLenConstraint, rangeConstraint} @@ -696,11 +709,7 @@ func (c *Constraint) CheckConstraint(param string) bool { // check constraints switch c.ID { case noConstraint: - for _, cc := range c.customConstraints { - if cc.Name() == c.Name { - return cc.Execute(param, c.Data...) - } - } + return true case intConstraint: _, err = strconv.Atoi(param) case boolConstraint: @@ -744,14 +753,14 @@ func (c *Constraint) CheckConstraint(param string) bool { data, _ := strconv.Atoi(c.Data[0]) num, err = strconv.Atoi(param) - if num < data { + if err != nil || num < data { return false } case maxConstraint: data, _ := strconv.Atoi(c.Data[0]) num, err = strconv.Atoi(param) - if num > data { + if err != nil || num > data { return false } case rangeConstraint: @@ -759,12 +768,18 @@ func (c *Constraint) CheckConstraint(param string) bool { data2, _ := strconv.Atoi(c.Data[1]) num, err = strconv.Atoi(param) - if num < data || num > data2 { + if err != nil || num < data || num > data2 { return false } case datetimeConstraint: _, err = time.Parse(c.Data[0], param) + if err != nil { + return false + } case regexConstraint: + if c.RegexCompiler == nil { + return false + } if match := c.RegexCompiler.MatchString(param); !match { return false } diff --git a/path_testcases_test.go b/path_testcases_test.go index d4c8dc8d..27746636 100644 --- a/path_testcases_test.go +++ b/path_testcases_test.go @@ -713,6 +713,14 @@ func init() { {url: "/api/v1/", params: []string{""}, match: true}, }, }, + // Add test case for RegexCompiler == nil + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/123", params: []string{"123"}, match: true}, + {url: "/api/v1/abc", params: nil, match: false}, + }, + }, }..., ) } From d7e6413aa97077fdb2f81e8615cca7af054fd6d4 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Tue, 1 Apr 2025 08:31:29 -0400 Subject: [PATCH 51/53] =?UTF-8?q?=F0=9F=93=92=20docs:=20Update=20docs=20fo?= =?UTF-8?q?r=20State=20Management=20(#3388)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update docs for State Management --- docs/api/state.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/api/state.md b/docs/api/state.md index b22b9675..ecce678b 100644 --- a/docs/api/state.md +++ b/docs/api/state.md @@ -1,8 +1,10 @@ -# State Management +--- +id: state +title: 🗂️ State Management +sidebar_position: 8 +--- -This document details the state management functionality provided by Fiber, a thread-safe global key–value store used to store application dependencies and runtime data. The implementation is based on Go's `sync.Map`, ensuring concurrency safety. - -Below is the detailed description of all public methods and usage examples. +The State Management provides a global key–value store for managing application dependencies and runtime data. This store is shared across the entire application and remains consistent between requests. It is implemented using Go’s `sync.Map` to ensure safe concurrent access. ## State Type From 94a9496558c432d16484c28a4e5b55062f1c1277 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Apr 2025 12:49:30 +0000 Subject: [PATCH 52/53] build(deps): bump github.com/gofiber/utils/v2 Bumps [github.com/gofiber/utils/v2](https://github.com/gofiber/utils) from 2.0.0-beta.7 to 2.0.0-beta.8. - [Release notes](https://github.com/gofiber/utils/releases) - [Commits](https://github.com/gofiber/utils/compare/v2.0.0-beta.7...v2.0.0-beta.8) --- updated-dependencies: - dependency-name: github.com/gofiber/utils/v2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 4 +++- go.sum | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f5490bf1..038e4a55 100644 --- a/go.mod +++ b/go.mod @@ -2,9 +2,11 @@ module github.com/gofiber/fiber/v3 go 1.23.0 +toolchain go1.24.1 + require ( github.com/gofiber/schema v1.3.0 - github.com/gofiber/utils/v2 v2.0.0-beta.7 + github.com/gofiber/utils/v2 v2.0.0-beta.8 github.com/google/uuid v1.6.0 github.com/mattn/go-colorable v0.1.14 github.com/mattn/go-isatty v0.0.20 diff --git a/go.sum b/go.sum index 81b9a920..fe166a5c 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vt github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gofiber/schema v1.3.0 h1:K3F3wYzAY+aivfCCEHPufCthu5/13r/lzp1nuk6mr3Q= github.com/gofiber/schema v1.3.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c= -github.com/gofiber/utils/v2 v2.0.0-beta.7 h1:NnHFrRHvhrufPABdWajcKZejz9HnCWmT/asoxRsiEbQ= -github.com/gofiber/utils/v2 v2.0.0-beta.7/go.mod h1:J/M03s+HMdZdvhAeyh76xT72IfVqBzuz/OJkrMa7cwU= +github.com/gofiber/utils/v2 v2.0.0-beta.8 h1:ZifwbHZqZO3YJsx1ZhDsWnPjaQ7C0YD20LHt+DQeXOU= +github.com/gofiber/utils/v2 v2.0.0-beta.8/go.mod h1:1lCBo9vEF4RFEtTgWntipnaScJZQiM8rrsYycLZ4n9c= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= From 4ad9a89e53f85492b1602e342870b6fcf4838497 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Tue, 1 Apr 2025 08:52:24 -0400 Subject: [PATCH 53/53] Update go.mod --- go.mod | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.mod b/go.mod index 038e4a55..201cd000 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,6 @@ module github.com/gofiber/fiber/v3 go 1.23.0 -toolchain go1.24.1 - require ( github.com/gofiber/schema v1.3.0 github.com/gofiber/utils/v2 v2.0.0-beta.8