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 01/15] 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 02/15] 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 03/15] =?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 04/15] 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 05/15] 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 06/15] =?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 07/15] =?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 08/15] =?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 09/15] =?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 10/15] =?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 11/15] 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 12/15] 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 13/15] =?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 14/15] 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 15/15] 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