From 70a06c5887992c24f9a89624abde66e0b6ed46b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:50:34 +0300 Subject: [PATCH 1/4] build(deps): bump github.com/tinylib/msgp from 1.2.4 to 1.2.5 (#3240) Bumps [github.com/tinylib/msgp](https://github.com/tinylib/msgp) from 1.2.4 to 1.2.5. - [Release notes](https://github.com/tinylib/msgp/releases) - [Commits](https://github.com/tinylib/msgp/compare/v1.2.4...v1.2.5) --- updated-dependencies: - dependency-name: github.com/tinylib/msgp dependency-type: direct:production update-type: version-update:semver-patch ... 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 f8fa9006e5..a17d0b1aee 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/mattn/go-colorable v0.1.13 github.com/mattn/go-isatty v0.0.20 github.com/stretchr/testify v1.10.0 - github.com/tinylib/msgp v1.2.4 + github.com/tinylib/msgp v1.2.5 github.com/valyala/bytebufferpool v1.0.0 github.com/valyala/fasthttp v1.57.0 ) diff --git a/go.sum b/go.sum index f3fda29192..293e2d6c08 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tinylib/msgp v1.2.4 h1:yLFeUGostXXSGW5vxfT5dXG/qzkn4schv2I7at5+hVU= -github.com/tinylib/msgp v1.2.4/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +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.57.0 h1:Xw8SjWGEP/+wAAgyy5XTvgrWlOD1+TxbbvNADYCm1Tg= From 27cfd3c8cd2e596d7bca5f2f2efe9ad92f2a1a4d Mon Sep 17 00:00:00 2001 From: Jinquan Wang <35188480+wangjq4214@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:12:33 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=94=A5=20feat:=20Add=20support=20for?= =?UTF-8?q?=20AutoTLS=20/=20ACME=20(#3201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add a simple support for app.Listen * fix: fix the nil access error * chore: add test case for simple tls * fix: align the struct * chore: change the test case can't passed and not chack the file yet * fix: use TLS1.2 min * Fix lint issues * Fix call to os.MkdirTemp * Fix test check order * Update unit-tests for ACME * Update docs * Fix identation of whats_new examples * More updates to docs * Remove ACME tests. Add check for tlsConfig * Add ACME section to whats_new docs * Update docs/whats_new.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update fiber.md * Update whats_new.md --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Co-authored-by: Juan Calderon-Perez Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- docs/api/fiber.md | 22 ++- docs/whats_new.md | 434 ++++++++++++++++++++++++---------------------- go.mod | 1 + go.sum | 2 + listen.go | 16 +- 5 files changed, 264 insertions(+), 211 deletions(-) diff --git a/docs/api/fiber.md b/docs/api/fiber.md index 2c50339d50..a34a5d944c 100644 --- a/docs/api/fiber.md +++ b/docs/api/fiber.md @@ -114,8 +114,9 @@ app.Listen(":8080", fiber.ListenConfig{ | 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` | +| 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 | `func(tlsConfig *tls.Config)` | Manages TLS certificates automatically using the ACME protocol. Enables integration with Let's Encrypt or other ACME-compatible providers. | `nil` | ### Listen @@ -166,6 +167,25 @@ app.Listen(":443", fiber.ListenConfig{CertClientFile: "./ca-chain-cert.pem"}) app.Listen(":443", fiber.ListenConfig{CertFile: "./cert.pem", CertKeyFile: "./cert.key", CertClientFile: "./ca-chain-cert.pem"}) ``` +#### TLS AutoCert support (ACME / Let's Encrypt) + +Provides automatic access to certificates management from Let's Encrypt and any other ACME-based providers. + +```go title="Examples" +// Certificate manager +certManager := &autocert.Manager{ + Prompt: autocert.AcceptTOS, + // Replace with your domain name + HostPolicy: autocert.HostWhitelist("example.com"), + // Folder to store the certificates + Cache: autocert.DirCache("./certs"), +} + +app.Listen(":444", fiber.ListenConfig{ + AutoCertManager: certManager, +}) +``` + ### Listener You can pass your own [`net.Listener`](https://pkg.go.dev/net/#Listener) using the `Listener` method. This method can be used to enable **TLS/HTTPS** with a custom tls.Config. diff --git a/docs/whats_new.md b/docs/whats_new.md index 3221f5dd5e..bfc6f25c29 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -130,6 +130,25 @@ In this example, a custom context `CustomCtx` is created with an additional meth +#### TLS AutoCert support (ACME / Let's Encrypt) + +We have added native support for automatic certificates management from Let's Encrypt and any other ACME-based providers. + +```go +// Certificate manager +certManager := &autocert.Manager{ + Prompt: autocert.AcceptTOS, + // Replace with your domain name + HostPolicy: autocert.HostWhitelist("example.com"), + // Folder to store the certificates + Cache: autocert.DirCache("./certs"), +} + +app.Listen(":444", fiber.ListenConfig{ + AutoCertManager: certManager, +}) +``` + ## 🗺 Router We have slightly adapted our router interface @@ -175,22 +194,22 @@ The route method is now like [`Express`](https://expressjs.com/de/api.html#app.r ```go app.Route("/api").Route("/user/:id?") - .Get(func(c fiber.Ctx) error { - // Get user - return c.JSON(fiber.Map{"message": "Get user", "id": c.Params("id")}) - }) - .Post(func(c fiber.Ctx) error { - // Create user - return c.JSON(fiber.Map{"message": "User created"}) - }) - .Put(func(c fiber.Ctx) error { - // Update user - return c.JSON(fiber.Map{"message": "User updated", "id": c.Params("id")}) - }) - .Delete(func(c fiber.Ctx) error { - // Delete user - return c.JSON(fiber.Map{"message": "User deleted", "id": c.Params("id")}) - }) + .Get(func(c fiber.Ctx) error { + // Get user + return c.JSON(fiber.Map{"message": "Get user", "id": c.Params("id")}) + }) + .Post(func(c fiber.Ctx) error { + // Create user + return c.JSON(fiber.Map{"message": "User created"}) + }) + .Put(func(c fiber.Ctx) error { + // Update user + return c.JSON(fiber.Map{"message": "User updated", "id": c.Params("id")}) + }) + .Delete(func(c fiber.Ctx) error { + // Delete user + return c.JSON(fiber.Map{"message": "User deleted", "id": c.Params("id")}) + }) ``` @@ -209,14 +228,14 @@ Registering a subapp is now also possible via the [`Use`](./api/app#use) method ```go // register mulitple prefixes app.Use(["/v1", "/v2"], func(c fiber.Ctx) error { - // Middleware for /v1 and /v2 - return c.Next() + // Middleware for /v1 and /v2 + return c.Next() }) // define subapp api := fiber.New() api.Get("/user", func(c fiber.Ctx) error { - return c.SendString("User") + return c.SendString("User") }) // register subapp app.Use("/api", api) @@ -242,14 +261,14 @@ The `app.Test()` method now allows users to customize their test configurations: // Create a test app with a handler to test app := fiber.New() app.Get("/", func(c fiber.Ctx) { - return c.SendString("hello world") + return c.SendString("hello world") }) // Define the HTTP request and custom TestConfig to test the handler req := httptest.NewRequest(MethodGet, "/", nil) testConfig := fiber.TestConfig{ - Timeout: 0, - FailOnTimeout: false, + Timeout: 0, + FailOnTimeout: false, } // Test the handler using the request and testConfig @@ -277,8 +296,8 @@ If a custom `TestConfig` isn't provided, then the following will be used: ```go testConfig := fiber.TestConfig{ - Timeout: time.Second, - FailOnTimeout: true, + Timeout: time.Second, + FailOnTimeout: true, } ``` @@ -288,8 +307,8 @@ An empty `TestConfig` is the equivalent of: ```go testConfig := fiber.TestConfig{ - Timeout: 0, - FailOnTimeout: false, + Timeout: 0, + FailOnTimeout: false, } ``` @@ -340,7 +359,7 @@ testConfig := fiber.TestConfig{ ### SendStreamWriter -In v3, we added support for buffered streaming by providing the new method `SendStreamWriter()`. +In v3, we introduced support for buffered streaming with the addition of the `SendStreamWriter` method: ```go func (c Ctx) SendStreamWriter(streamWriter func(w *bufio.Writer)) @@ -354,22 +373,22 @@ With this new method, you can implement: ```go app.Get("/sse", func(c fiber.Ctx) { - c.Set("Content-Type", "text/event-stream") - c.Set("Cache-Control", "no-cache") - c.Set("Connection", "keep-alive") - c.Set("Transfer-Encoding", "chunked") - - return c.SendStreamWriter(func(w *bufio.Writer) { - for { - fmt.Fprintf(w, "event: my-event\n") - fmt.Fprintf(w, "data: Hello SSE\n\n") - - if err := w.Flush(); err != nil { - log.Print("Client disconnected!") - return - } - } - }) + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + return c.SendStreamWriter(func(w *bufio.Writer) { + for { + fmt.Fprintf(w, "event: my-event\n") + fmt.Fprintf(w, "data: Hello SSE\n\n") + + if err := w.Flush(); err != nil { + log.Print("Client disconnected!") + return + } + } + }) }) ``` @@ -397,17 +416,17 @@ Fiber v3 introduces a new binding mechanism that simplifies the process of bindi ```go type User struct { - ID int `params:"id"` - Name string `json:"name"` - Email string `json:"email"` + ID int `params:"id"` + Name string `json:"name"` + Email string `json:"email"` } app.Post("/user/:id", func(c fiber.Ctx) error { - var user User - if err := c.Bind().Body(&user); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) - } - return c.JSON(user) + var user User + if err := c.Bind().Body(&user); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(user) }) ``` @@ -430,11 +449,11 @@ Fiber v3 enhances the redirect functionality by introducing new methods and impr ```go app.Get("/old", func(c fiber.Ctx) error { - return c.Redirect().To("/new") + return c.Redirect().To("/new") }) app.Get("/new", func(c fiber.Ctx) error { - return c.SendString("Welcome to the new route!") + return c.SendString("Welcome to the new route!") }) ``` @@ -461,22 +480,22 @@ Fiber v3 introduces new generic functions that provide additional utility and fl package main import ( - "strconv" - "github.com/gofiber/fiber/v3" + "strconv" + "github.com/gofiber/fiber/v3" ) func main() { - app := fiber.New() + app := fiber.New() - app.Get("/convert", func(c fiber.Ctx) error { - value, err := Convert[string](c.Query("value"), strconv.Atoi, 0) - if err != nil { - return c.Status(fiber.StatusBadRequest).SendString(err.Error()) - } - return c.JSON(value) - }) + app.Get("/convert", func(c fiber.Ctx) error { + value, err := Convert[string](c.Query("value"), strconv.Atoi, 0) + if err != nil { + return c.Status(fiber.StatusBadRequest).SendString(err.Error()) + } + return c.JSON(value) + }) - app.Listen(":3000") + app.Listen(":3000") } ``` @@ -540,20 +559,19 @@ curl "http://localhost:3000/user/5" package main import ( - "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3" ) func main() { - app := fiber.New() + app := fiber.New() - app.Get("/params/:id", func(c fiber.Ctx) error { - id := Params[int](c, "id", 0) - return c.JSON(id) - }) + app.Get("/params/:id", func(c fiber.Ctx) error { + id := Params[int](c, "id", 0) + return c.JSON(id) + }) - app.Listen(":3000") + app.Listen(":3000") } - ``` ```sh @@ -573,24 +591,23 @@ curl "http://localhost:3000/params/abc" package main import ( - "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3" ) func main() { - app := fiber.New() + app := fiber.New() - app.Get("/query", func(c fiber.Ctx) error { - age := Query[int](c, "age", 0) - return c.JSON(age) - }) + app.Get("/query", func(c fiber.Ctx) error { + age := Query[int](c, "age", 0) + return c.JSON(age) + }) - app.Listen(":3000") + app.Listen(":3000") } ``` ```sh - curl "http://localhost:3000/query?age=25" # Output: 25 @@ -607,18 +624,18 @@ curl "http://localhost:3000/query?age=abc" package main import ( - "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3" ) func main() { - app := fiber.New() + app := fiber.New() - app.Get("/header", func(c fiber.Ctx) error { - userAgent := GetReqHeader[string](c, "User-Agent", "Unknown") - return c.JSON(userAgent) - }) + app.Get("/header", func(c fiber.Ctx) error { + userAgent := GetReqHeader[string](c, "User-Agent", "Unknown") + return c.JSON(userAgent) + }) - app.Listen(":3000") + app.Listen(":3000") } ``` @@ -805,7 +822,7 @@ Since we've removed `app.Static()`, you need to move methods to static middlewar app.Static("/", "./public") app.Static("/prefix", "./public") app.Static("/prefix", "./public", Static{ - Index: "index.htm", + Index: "index.htm", }) app.Static("*", "./public/index.html") ``` @@ -815,7 +832,7 @@ app.Static("*", "./public/index.html") app.Get("/*", static.New("./public")) app.Get("/prefix*", static.New("./public")) app.Get("/prefix*", static.New("./public", static.Config{ - IndexNames: []string{"index.htm", "index.html"}, + IndexNames: []string{"index.htm", "index.html"}, })) app.Get("*", static.New("./public/index.html")) ``` @@ -831,25 +848,25 @@ We've renamed `EnableTrustedProxyCheck` to `TrustProxy` and moved `TrustedProxie ```go // Before app := fiber.New(fiber.Config{ - // EnableTrustedProxyCheck enables the trusted proxy check. - EnableTrustedProxyCheck: true, - // TrustedProxies is a list of trusted proxy IP ranges/addresses. - TrustedProxies: []string{"0.8.0.0", "127.0.0.0/8", "::1/128"}, + // EnableTrustedProxyCheck enables the trusted proxy check. + EnableTrustedProxyCheck: true, + // TrustedProxies is a list of trusted proxy IP ranges/addresses. + TrustedProxies: []string{"0.8.0.0", "127.0.0.0/8", "::1/128"}, }) ``` ```go // After app := fiber.New(fiber.Config{ - // TrustProxy enables the trusted proxy check - TrustProxy: true, - // TrustProxyConfig allows for configuring trusted proxies. - TrustProxyConfig: fiber.TrustProxyConfig{ - // Proxies is a list of trusted proxy IP ranges/addresses. - Proxies: []string{"0.8.0.0"}, - // Trust all loop-back IP addresses (127.0.0.0/8, ::1/128) - Loopback: true, - } + // TrustProxy enables the trusted proxy check + TrustProxy: true, + // TrustProxyConfig allows for configuring trusted proxies. + TrustProxyConfig: fiber.TrustProxyConfig{ + // Proxies is a list of trusted proxy IP ranges/addresses. + Proxies: []string{"0.8.0.0"}, + // Trust all loop-back IP addresses (127.0.0.0/8, ::1/128) + Loopback: true, + } }) ``` @@ -890,21 +907,21 @@ app.Route("/api", func(apiGrp Router) { ```go // After app.Route("/api").Route("/user/:id?") - .Get(func(c fiber.Ctx) error { - // Get user - return c.JSON(fiber.Map{"message": "Get user", "id": c.Params("id")}) - }) - .Post(func(c fiber.Ctx) error { - // Create user - return c.JSON(fiber.Map{"message": "User created"}) - }); + .Get(func(c fiber.Ctx) error { + // Get user + return c.JSON(fiber.Map{"message": "Get user", "id": c.Params("id")}) + }) + .Post(func(c fiber.Ctx) error { + // Create user + return c.JSON(fiber.Map{"message": "User created"}) + }); ``` ### 🗺 RebuildTree -We have added a new method that allows the route tree stack to be rebuilt in runtime, with it, you can add a route while your application is running and rebuild the route tree stack to make it registered and available for calls. +We introduced a new method that enables rebuilding the route tree stack at runtime. This allows you to add routes dynamically while your application is running and update the route tree to make the new routes available for use. -You can find more reference on it in the [app](./api/app.md#rebuildtree): +For more details, refer to the [app documentation](./api/app.md#rebuildtree): #### Example Usage @@ -920,10 +937,9 @@ app.Get("/define", func(c Ctx) error { // Define a new route dynamically }) ``` -In this example, a new route is defined and then `RebuildTree()` is called to make sure the new route is registered and available. +In this example, a new route is defined, and `RebuildTree()` is called to ensure the new route is registered and available. -**Note:** Use this method with caution. It is **not** thread-safe and calling it can be very performance-intensive, so it should be used sparingly and only in -development mode. Avoid using it concurrently. +Note: Use this method with caution. It is **not** thread-safe and can be very performance-intensive. Therefore, it should be used sparingly and primarily in development mode. It should not be invoke concurrently. ### 🧠 Context @@ -946,18 +962,18 @@ In Fiber v3, the `Ctx` parameter in handlers is now an interface, which means th package main import ( - "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2" ) func main() { - app := fiber.New() + app := fiber.New() - // Route Handler with *fiber.Ctx - app.Get("/", func(c *fiber.Ctx) error { - return c.SendString("Hello, World!") - }) + // Route Handler with *fiber.Ctx + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) - app.Listen(":3000") + app.Listen(":3000") } ``` @@ -967,18 +983,18 @@ func main() { package main import ( - "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3" ) func main() { - app := fiber.New() + app := fiber.New() - // Route Handler without *fiber.Ctx - app.Get("/", func(c fiber.Ctx) error { - return c.SendString("Hello, World!") - }) + // Route Handler without *fiber.Ctx + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("Hello, World!") + }) - app.Listen(":3000") + app.Listen(":3000") } ``` @@ -1002,22 +1018,22 @@ The `Parser` section in Fiber v3 has undergone significant changes to improve fu ```go // Before app.Post("/user", func(c *fiber.Ctx) error { - var user User - if err := c.BodyParser(&user); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) - } - return c.JSON(user) + var user User + if err := c.BodyParser(&user); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(user) }) ``` ```go // After app.Post("/user", func(c fiber.Ctx) error { - var user User - if err := c.Bind().Body(&user); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) - } - return c.JSON(user) + var user User + if err := c.Bind().Body(&user); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(user) }) ``` @@ -1031,22 +1047,22 @@ The `Parser` section in Fiber v3 has undergone significant changes to improve fu ```go // Before app.Get("/user/:id", func(c *fiber.Ctx) error { - var params Params - if err := c.ParamsParser(¶ms); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) - } - return c.JSON(params) + var params Params + if err := c.ParamsParser(¶ms); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(params) }) ``` ```go // After app.Get("/user/:id", func(c fiber.Ctx) error { - var params Params - if err := c.Bind().URL(¶ms); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) - } - return c.JSON(params) + var params Params + if err := c.Bind().URL(¶ms); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(params) }) ``` @@ -1060,22 +1076,22 @@ The `Parser` section in Fiber v3 has undergone significant changes to improve fu ```go // Before app.Get("/search", func(c *fiber.Ctx) error { - var query Query - if err := c.QueryParser(&query); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) - } - return c.JSON(query) + var query Query + if err := c.QueryParser(&query); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(query) }) ``` ```go // After app.Get("/search", func(c fiber.Ctx) error { - var query Query - if err := c.Bind().Query(&query); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) - } - return c.JSON(query) + var query Query + if err := c.Bind().Query(&query); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(query) }) ``` @@ -1089,22 +1105,22 @@ The `Parser` section in Fiber v3 has undergone significant changes to improve fu ```go // Before app.Get("/cookie", func(c *fiber.Ctx) error { - var cookie Cookie - if err := c.CookieParser(&cookie); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) - } - return c.JSON(cookie) + var cookie Cookie + if err := c.CookieParser(&cookie); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(cookie) }) ``` ```go // After app.Get("/cookie", func(c fiber.Ctx) error { - var cookie Cookie - if err := c.Bind().Cookie(&cookie); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) - } - return c.JSON(cookie) + var cookie Cookie + if err := c.Bind().Cookie(&cookie); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(cookie) }) ``` @@ -1124,14 +1140,14 @@ Fiber v3 enhances the redirect functionality by introducing new methods and impr ```go // Before app.Get("/old", func(c *fiber.Ctx) error { - return c.RedirectToRoute("newRoute") + return c.RedirectToRoute("newRoute") }) ``` ```go // After app.Get("/old", func(c fiber.Ctx) error { - return c.Redirect().Route("newRoute") + return c.Redirect().Route("newRoute") }) ``` @@ -1145,14 +1161,14 @@ Fiber v3 enhances the redirect functionality by introducing new methods and impr ```go // Before app.Get("/back", func(c *fiber.Ctx) error { - return c.RedirectBack() + return c.RedirectBack() }) ``` ```go // After app.Get("/back", func(c fiber.Ctx) error { - return c.Redirect().Back() + return c.Redirect().Back() }) ``` @@ -1166,14 +1182,14 @@ Fiber v3 enhances the redirect functionality by introducing new methods and impr ```go // Before app.Get("/old", func(c *fiber.Ctx) error { - return c.Redirect("/new") + return c.Redirect("/new") }) ``` ```go // After app.Get("/old", func(c fiber.Ctx) error { - return c.Redirect().To("/new") + return c.Redirect().To("/new") }) ``` @@ -1226,18 +1242,18 @@ The CORS middleware has been updated to use slices instead of strings for the `A ```go // Before app.Use(cors.New(cors.Config{ - AllowOrigins: "https://example.com,https://example2.com", - AllowMethods: strings.Join([]string{fiber.MethodGet, fiber.MethodPost}, ","), - AllowHeaders: "Content-Type", - ExposeHeaders: "Content-Length", + AllowOrigins: "https://example.com,https://example2.com", + AllowMethods: strings.Join([]string{fiber.MethodGet, fiber.MethodPost}, ","), + AllowHeaders: "Content-Type", + ExposeHeaders: "Content-Length", })) // After app.Use(cors.New(cors.Config{ - AllowOrigins: []string{"https://example.com", "https://example2.com"}, - AllowMethods: []string{fiber.MethodGet, fiber.MethodPost}, - AllowHeaders: []string{"Content-Type"}, - ExposeHeaders: []string{"Content-Length"}, + AllowOrigins: []string{"https://example.com", "https://example2.com"}, + AllowMethods: []string{fiber.MethodGet, fiber.MethodPost}, + AllowHeaders: []string{"Content-Type"}, + ExposeHeaders: []string{"Content-Length"}, })) ``` @@ -1248,12 +1264,12 @@ app.Use(cors.New(cors.Config{ ```go // Before app.Use(csrf.New(csrf.Config{ - Expiration: 10 * time.Minute, + Expiration: 10 * time.Minute, })) // After app.Use(csrf.New(csrf.Config{ - IdleTimeout: 10 * time.Minute, + IdleTimeout: 10 * time.Minute, })) ``` @@ -1266,28 +1282,28 @@ You need to move filesystem middleware to static middleware due to it has been r ```go // Before app.Use(filesystem.New(filesystem.Config{ - Root: http.Dir("./assets"), + Root: http.Dir("./assets"), })) app.Use(filesystem.New(filesystem.Config{ - Root: http.Dir("./assets"), - Browse: true, - Index: "index.html", - MaxAge: 3600, + Root: http.Dir("./assets"), + Browse: true, + Index: "index.html", + MaxAge: 3600, })) ``` ```go // After app.Use(static.New("", static.Config{ - FS: os.DirFS("./assets"), + FS: os.DirFS("./assets"), })) app.Use(static.New("", static.Config{ - FS: os.DirFS("./assets"), - Browse: true, - IndexNames: []string{"index.html"}, - MaxAge: 3600, + FS: os.DirFS("./assets"), + Browse: true, + IndexNames: []string{"index.html"}, + MaxAge: 3600, })) ``` @@ -1298,14 +1314,14 @@ Previously, the Healthcheck middleware was configured with a combined setup for ```go //before app.Use(healthcheck.New(healthcheck.Config{ - LivenessProbe: func(c fiber.Ctx) bool { - return true - }, - LivenessEndpoint: "/live", - ReadinessProbe: func(c fiber.Ctx) bool { - return serviceA.Ready() && serviceB.Ready() && ... - }, - ReadinessEndpoint: "/ready", + LivenessProbe: func(c fiber.Ctx) bool { + return true + }, + LivenessEndpoint: "/live", + ReadinessProbe: func(c fiber.Ctx) bool { + return serviceA.Ready() && serviceB.Ready() && ... + }, + ReadinessEndpoint: "/ready", })) ``` @@ -1316,9 +1332,9 @@ With the new version, each health check endpoint is configured separately, allow // Default liveness endpoint configuration app.Get(healthcheck.DefaultLivenessEndpoint, healthcheck.NewHealthChecker(healthcheck.Config{ - Probe: func(c fiber.Ctx) bool { - return true - }, + Probe: func(c fiber.Ctx) bool { + return true + }, })) // Default readiness endpoint configuration @@ -1327,9 +1343,9 @@ app.Get(healthcheck.DefaultReadinessEndpoint, healthcheck.NewHealthChecker()) // New default startup endpoint configuration // Default endpoint is /startupz app.Get(healthcheck.DefaultStartupEndpoint, healthcheck.NewHealthChecker(healthcheck.Config{ - Probe: func(c fiber.Ctx) bool { - return serviceA.Ready() && serviceB.Ready() && ... - }, + Probe: func(c fiber.Ctx) bool { + return serviceA.Ready() && serviceB.Ready() && ... + }, })) // Custom liveness endpoint configuration diff --git a/go.mod b/go.mod index a17d0b1aee..a979f1837e 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/tinylib/msgp v1.2.5 github.com/valyala/bytebufferpool v1.0.0 github.com/valyala/fasthttp v1.57.0 + golang.org/x/crypto v0.28.0 ) require ( diff --git a/go.sum b/go.sum index 293e2d6c08..3601863b69 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,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.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/listen.go b/listen.go index 624b2539f3..e0c5536968 100644 --- a/listen.go +++ b/listen.go @@ -23,6 +23,7 @@ import ( "github.com/gofiber/fiber/v3/log" "github.com/mattn/go-colorable" "github.com/mattn/go-isatty" + "golang.org/x/crypto/acme/autocert" ) // Figlet text to show Fiber ASCII art on startup message @@ -69,6 +70,13 @@ type ListenConfig struct { // // Default: nil OnShutdownSuccess func() + + // AutoCertManager manages TLS certificates automatically using the ACME protocol, + // Enables integration with Let's Encrypt or other ACME-compatible providers. + // + // Default: nil + AutoCertManager *autocert.Manager `json:"auto_cert_manager"` + // Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only) // WARNING: When prefork is set to true, only "tcp4" and "tcp6" can be chosen. // @@ -183,9 +191,15 @@ func (app *App) Listen(addr string, config ...ListenConfig) error { // Attach the tlsHandler to the config app.SetTLSHandler(tlsHandler) + } else if cfg.AutoCertManager != nil { + tlsConfig = &tls.Config{ + MinVersion: tls.VersionTLS12, + GetCertificate: cfg.AutoCertManager.GetCertificate, + NextProtos: []string{"http/1.1", "acme-tls/1"}, + } } - if cfg.TLSConfigFunc != nil { + if tlsConfig != nil && cfg.TLSConfigFunc != nil { cfg.TLSConfigFunc(tlsConfig) } From e9849b758d0afaaf700f52ef76372bc41d373412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=2E=20Efe=20=C3=87etin?= Date: Tue, 10 Dec 2024 12:39:23 +0300 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=94=A5=20feat:=20Add=20support=20for?= =?UTF-8?q?=20iterator=20methods=20to=20Fiber=20client=20(#3228)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: simplify parserRequestBodyFile logic * client: add support for go1.23 iterators * Apply suggestions from code review Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix linter * fix tests * correct benchmark * fix linter * create docs * update * rename FormDatas -> AllFormData * add examples for maps.Collect() * change request/response markdown examples --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: René --- Makefile | 2 +- client/hooks.go | 25 ++--- client/request.go | 133 +++++++++++++++++++++++++ client/request_test.go | 213 ++++++++++++++++++++++++++++++++++++++++ client/response.go | 27 +++++ client/response_test.go | 79 +++++++++++++++ docs/client/request.md | 184 +++++++++++++++++++++++++++------- docs/client/response.md | 75 ++++++++++++-- 8 files changed, 673 insertions(+), 65 deletions(-) diff --git a/Makefile b/Makefile index 87d4b50db5..4b348cd574 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ coverage: format: go run mvdan.cc/gofumpt@latest -w -l . -## markdown: 🎨 Find markdown format issues (Requires markdownlint-cli) +## markdown: 🎨 Find markdown format issues (Requires markdownlint-cli2) .PHONY: markdown markdown: markdownlint-cli2 "**/*.md" "#vendor" diff --git a/client/hooks.go b/client/hooks.go index ec3987938e..4c36145f2f 100644 --- a/client/hooks.go +++ b/client/hooks.go @@ -1,7 +1,6 @@ package client import ( - "errors" "fmt" "io" "math/rand" @@ -241,8 +240,8 @@ func parserRequestBodyFile(req *Request) error { return fmt.Errorf("write formdata error: %w", err) } - // add file - b := make([]byte, 512) + // add files + fileBuf := make([]byte, 1<<20) // Allocate 1MB buffer for i, v := range req.files { if v.name == "" && v.path == "" { return ErrFileNoName @@ -273,24 +272,12 @@ func parserRequestBodyFile(req *Request) error { return fmt.Errorf("create file error: %w", err) } - for { - n, err := v.reader.Read(b) - if err != nil && !errors.Is(err, io.EOF) { - return fmt.Errorf("read file error: %w", err) - } - - if errors.Is(err, io.EOF) { - break - } - - _, err = w.Write(b[:n]) - if err != nil { - return fmt.Errorf("write file error: %w", err) - } + // Copy the file from reader to multipart writer + if _, err := io.CopyBuffer(w, v.reader, fileBuf); err != nil { + return fmt.Errorf("failed to copy file data: %w", err) } - err = v.reader.Close() - if err != nil { + if err := v.reader.Close(); err != nil { return fmt.Errorf("close file error: %w", err) } } diff --git a/client/request.go b/client/request.go index a86d927c4e..f303fb7490 100644 --- a/client/request.go +++ b/client/request.go @@ -5,8 +5,10 @@ import ( "context" "errors" "io" + "iter" "path/filepath" "reflect" + "slices" "strconv" "sync" "time" @@ -129,6 +131,31 @@ func (r *Request) Header(key string) []string { return r.header.PeekMultiple(key) } +// Headers returns all headers in the request using an iterator. +// You can use maps.Collect() to collect all headers into a map. +// +// The returned value is valid until the request object is released. +// Any future calls to Headers method will return the modified value. Do not store references to returned value. Make copies instead. +func (r *Request) Headers() iter.Seq2[string, []string] { + return func(yield func(string, []string) bool) { + peekKeys := r.header.PeekKeys() + keys := make([][]byte, len(peekKeys)) + copy(keys, peekKeys) // It is necessary to have immutable byte slice. + + for _, key := range keys { + vals := r.header.PeekAll(utils.UnsafeString(key)) + valsStr := make([]string, len(vals)) + for i, v := range vals { + valsStr[i] = utils.UnsafeString(v) + } + + if !yield(utils.UnsafeString(key), valsStr) { + return + } + } + } +} + // AddHeader method adds a single header field and its value in the request instance. func (r *Request) AddHeader(key, val string) *Request { r.header.Add(key, val) @@ -168,6 +195,33 @@ func (r *Request) Param(key string) []string { return res } +// Params returns all params in the request using an iterator. +// You can use maps.Collect() to collect all params into a map. +// +// The returned value is valid until the request object is released. +// Any future calls to Params method will return the modified value. Do not store references to returned value. Make copies instead. +func (r *Request) Params() iter.Seq2[string, []string] { + return func(yield func(string, []string) bool) { + keys := r.params.Keys() + + for _, key := range keys { + if key == "" { + continue + } + + vals := r.params.PeekMulti(key) + valsStr := make([]string, len(vals)) + for i, v := range vals { + valsStr[i] = utils.UnsafeString(v) + } + + if !yield(key, valsStr) { + return + } + } + } +} + // AddParam method adds a single param field and its value in the request instance. func (r *Request) AddParam(key, val string) *Request { r.params.Add(key, val) @@ -254,6 +308,18 @@ func (r *Request) Cookie(key string) string { return "" } +// Cookies returns all cookies in the cookies using an iterator. +// You can use maps.Collect() to collect all cookies into a map. +func (r *Request) Cookies() iter.Seq2[string, string] { + return func(yield func(string, string) bool) { + r.cookies.VisitAll(func(key, val string) { + if !yield(key, val) { + return + } + }) + } +} + // SetCookie method sets a single cookie field and its value in the request instance. // It will override cookie which set in client instance. func (r *Request) SetCookie(key, val string) *Request { @@ -291,6 +357,18 @@ func (r *Request) PathParam(key string) string { return "" } +// PathParams returns all path params in request instance. +// You can use maps.Collect() to collect all cookies into a map. +func (r *Request) PathParams() iter.Seq2[string, string] { + return func(yield func(string, string) bool) { + r.path.VisitAll(func(key, val string) { + if !yield(key, val) { + return + } + }) + } +} + // SetPathParam method sets a single path param field and its value in the request instance. // It will override path param which set in client instance. func (r *Request) SetPathParam(key, val string) *Request { @@ -376,6 +454,33 @@ func (r *Request) FormData(key string) []string { return res } +// AllFormData method returns all form datas in request instance. +// You can use maps.Collect() to collect all cookies into a map. +// +// The returned value is valid until the request object is released. +// Any future calls to FormDatas method will return the modified value. Do not store references to returned value. Make copies instead. +func (r *Request) AllFormData() iter.Seq2[string, []string] { + return func(yield func(string, []string) bool) { + keys := r.formData.Keys() + + for _, key := range keys { + if key == "" { + continue + } + + vals := r.formData.PeekMulti(key) + valsStr := make([]string, len(vals)) + for i, v := range vals { + valsStr[i] = utils.UnsafeString(v) + } + + if !yield(key, valsStr) { + return + } + } + } +} + // AddFormData method adds a single form data field and its value in the request instance. func (r *Request) AddFormData(key, val string) *Request { r.formData.AddData(key, val) @@ -435,6 +540,14 @@ func (r *Request) File(name string) *File { return nil } +// Files method returns all files in request instance. +// +// The returned value is valid until the request object is released. +// Any future calls to Files method will return the modified value. Do not store references to returned value. Make copies instead. +func (r *Request) Files() []*File { + return r.files +} + // FileByPath returns file ptr store in request obj by path. func (r *Request) FileByPath(path string) *File { for _, v := range r.files { @@ -617,6 +730,16 @@ type QueryParam struct { *fasthttp.Args } +// Keys method returns all keys in the query params. +func (p *QueryParam) Keys() []string { + keys := make([]string, 0, p.Len()) + p.VisitAll(func(key, _ []byte) { + keys = append(keys, utils.UnsafeString(key)) + }) + + return slices.Compact(keys) +} + // AddParams receive a map and add each value to param. func (p *QueryParam) AddParams(r map[string][]string) { for k, v := range r { @@ -747,6 +870,16 @@ type FormData struct { *fasthttp.Args } +// Keys method returns all keys in the form data. +func (f *FormData) Keys() []string { + keys := make([]string, 0, f.Len()) + f.VisitAll(func(key, _ []byte) { + keys = append(keys, utils.UnsafeString(key)) + }) + + return slices.Compact(keys) +} + // AddData method is a wrapper of Args's Add method. func (f *FormData) AddData(key, val string) { f.Add(key, val) diff --git a/client/request_test.go b/client/request_test.go index f62865a342..e663f2feeb 100644 --- a/client/request_test.go +++ b/client/request_test.go @@ -5,6 +5,7 @@ import ( "context" "errors" "io" + "maps" "mime/multipart" "net" "os" @@ -157,6 +158,42 @@ func Test_Request_Header(t *testing.T) { }) } +func Test_Request_Headers(t *testing.T) { + t.Parallel() + + req := AcquireRequest() + req.AddHeaders(map[string][]string{ + "foo": {"bar", "fiber"}, + "bar": {"foo"}, + }) + + headers := maps.Collect(req.Headers()) + + require.Contains(t, headers["Foo"], "fiber") + require.Contains(t, headers["Foo"], "bar") + require.Contains(t, headers["Bar"], "foo") + + require.Len(t, headers, 2) +} + +func Benchmark_Request_Headers(b *testing.B) { + req := AcquireRequest() + req.AddHeaders(map[string][]string{ + "foo": {"bar", "fiber"}, + "bar": {"foo"}, + }) + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + for k, v := range req.Headers() { + _ = k + _ = v + } + } +} + func Test_Request_QueryParam(t *testing.T) { t.Parallel() @@ -282,6 +319,42 @@ func Test_Request_QueryParam(t *testing.T) { }) } +func Test_Request_Params(t *testing.T) { + t.Parallel() + + req := AcquireRequest() + req.AddParams(map[string][]string{ + "foo": {"bar", "fiber"}, + "bar": {"foo"}, + }) + + pathParams := maps.Collect(req.Params()) + + require.Contains(t, pathParams["foo"], "bar") + require.Contains(t, pathParams["foo"], "fiber") + require.Contains(t, pathParams["bar"], "foo") + + require.Len(t, pathParams, 2) +} + +func Benchmark_Request_Params(b *testing.B) { + req := AcquireRequest() + req.AddParams(map[string][]string{ + "foo": {"bar", "fiber"}, + "bar": {"foo"}, + }) + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + for k, v := range req.Params() { + _ = k + _ = v + } + } +} + func Test_Request_UA(t *testing.T) { t.Parallel() @@ -364,6 +437,41 @@ func Test_Request_Cookie(t *testing.T) { }) } +func Test_Request_Cookies(t *testing.T) { + t.Parallel() + + req := AcquireRequest() + req.SetCookies(map[string]string{ + "foo": "bar", + "bar": "foo", + }) + + cookies := maps.Collect(req.Cookies()) + + require.Equal(t, "bar", cookies["foo"]) + require.Equal(t, "foo", cookies["bar"]) + + require.Len(t, cookies, 2) +} + +func Benchmark_Request_Cookies(b *testing.B) { + req := AcquireRequest() + req.SetCookies(map[string]string{ + "foo": "bar", + "bar": "foo", + }) + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + for k, v := range req.Cookies() { + _ = k + _ = v + } + } +} + func Test_Request_PathParam(t *testing.T) { t.Parallel() @@ -441,6 +549,41 @@ func Test_Request_PathParam(t *testing.T) { }) } +func Test_Request_PathParams(t *testing.T) { + t.Parallel() + + req := AcquireRequest() + req.SetPathParams(map[string]string{ + "foo": "bar", + "bar": "foo", + }) + + pathParams := maps.Collect(req.PathParams()) + + require.Equal(t, "bar", pathParams["foo"]) + require.Equal(t, "foo", pathParams["bar"]) + + require.Len(t, pathParams, 2) +} + +func Benchmark_Request_PathParams(b *testing.B) { + req := AcquireRequest() + req.SetPathParams(map[string]string{ + "foo": "bar", + "bar": "foo", + }) + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + for k, v := range req.PathParams() { + _ = k + _ = v + } + } +} + func Test_Request_FormData(t *testing.T) { t.Parallel() @@ -610,6 +753,40 @@ func Test_Request_File(t *testing.T) { }) } +func Test_Request_Files(t *testing.T) { + t.Parallel() + + req := AcquireRequest() + req.AddFile("../.github/index.html") + req.AddFiles(AcquireFile(SetFileName("tmp.txt"))) + + files := req.Files() + + require.Equal(t, "../.github/index.html", files[0].path) + require.Nil(t, files[0].reader) + + require.Equal(t, "tmp.txt", files[1].name) + require.Nil(t, files[1].reader) + + require.Len(t, files, 2) +} + +func Benchmark_Request_Files(b *testing.B) { + req := AcquireRequest() + req.AddFile("../.github/index.html") + req.AddFiles(AcquireFile(SetFileName("tmp.txt"))) + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + for k, v := range req.Files() { + _ = k + _ = v + } + } +} + func Test_Request_Timeout(t *testing.T) { t.Parallel() @@ -1181,6 +1358,42 @@ func Test_Request_Body_With_Server(t *testing.T) { }) } +func Test_Request_AllFormData(t *testing.T) { + t.Parallel() + + req := AcquireRequest() + req.AddFormDatas(map[string][]string{ + "foo": {"bar", "fiber"}, + "bar": {"foo"}, + }) + + pathParams := maps.Collect(req.AllFormData()) + + require.Contains(t, pathParams["foo"], "bar") + require.Contains(t, pathParams["foo"], "fiber") + require.Contains(t, pathParams["bar"], "foo") + + require.Len(t, pathParams, 2) +} + +func Benchmark_Request_AllFormData(b *testing.B) { + req := AcquireRequest() + req.AddFormDatas(map[string][]string{ + "foo": {"bar", "fiber"}, + "bar": {"foo"}, + }) + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + for k, v := range req.AllFormData() { + _ = k + _ = v + } + } +} + func Test_Request_Error_Body_With_Server(t *testing.T) { t.Parallel() t.Run("json error", func(t *testing.T) { diff --git a/client/response.go b/client/response.go index e60c6bd0fb..8d21329774 100644 --- a/client/response.go +++ b/client/response.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "io/fs" + "iter" "os" "path/filepath" "sync" @@ -55,7 +56,33 @@ func (r *Response) Header(key string) string { return utils.UnsafeString(r.RawResponse.Header.Peek(key)) } +// Headers returns all headers in the response using an iterator. +// You can use maps.Collect() to collect all headers into a map. +// +// The returned value is valid until the response object is released. +// Any future calls to Headers method will return the modified value. Do not store references to returned value. Make copies instead. +func (r *Response) Headers() iter.Seq2[string, []string] { + return func(yield func(string, []string) bool) { + keys := r.RawResponse.Header.PeekKeys() + + for _, key := range keys { + vals := r.RawResponse.Header.PeekAll(utils.UnsafeString(key)) + valsStr := make([]string, len(vals)) + for i, v := range vals { + valsStr[i] = utils.UnsafeString(v) + } + + if !yield(utils.UnsafeString(key), valsStr) { + return + } + } + } +} + // Cookies method to access all the response cookies. +// +// The returned value is valid until the response object is released. +// Any future calls to Cookies method will return the modified value. Do not store references to returned value. Make copies instead. func (r *Response) Cookies() []*fasthttp.Cookie { return r.cookie } diff --git a/client/response_test.go b/client/response_test.go index bf12e75161..60b87bd155 100644 --- a/client/response_test.go +++ b/client/response_test.go @@ -199,6 +199,85 @@ func Test_Response_Header(t *testing.T) { resp.Close() } +func Test_Response_Headers(t *testing.T) { + t.Parallel() + + server := startTestServer(t, func(app *fiber.App) { + app.Get("/", func(c fiber.Ctx) error { + c.Response().Header.Add("foo", "bar") + c.Response().Header.Add("foo", "bar2") + c.Response().Header.Add("foo2", "bar") + + return c.SendString("hello world") + }) + }) + defer server.stop() + + client := New().SetDial(server.dial()) + + resp, err := AcquireRequest(). + SetClient(client). + Get("http://example.com") + + require.NoError(t, err) + + headers := make(map[string][]string) + for k, v := range resp.Headers() { + headers[k] = append(headers[k], v...) + } + + require.Equal(t, "hello world", resp.String()) + + require.Contains(t, headers["Foo"], "bar") + require.Contains(t, headers["Foo"], "bar2") + require.Contains(t, headers["Foo2"], "bar") + + require.Len(t, headers, 3) // Foo + Foo2 + Date + + resp.Close() +} + +func Benchmark_Headers(b *testing.B) { + server := startTestServer( + b, + func(app *fiber.App) { + app.Get("/", func(c fiber.Ctx) error { + c.Response().Header.Add("foo", "bar") + c.Response().Header.Add("foo", "bar2") + c.Response().Header.Add("foo", "bar3") + + c.Response().Header.Add("foo2", "bar") + c.Response().Header.Add("foo2", "bar2") + c.Response().Header.Add("foo2", "bar3") + + return c.SendString("helo world") + }) + }, + ) + + client := New().SetDial(server.dial()) + + resp, err := AcquireRequest(). + SetClient(client). + Get("http://example.com") + require.NoError(b, err) + + b.Cleanup(func() { + resp.Close() + server.stop() + }) + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + for k, v := range resp.Headers() { + _ = k + _ = v + } + } +} + func Test_Response_Cookie(t *testing.T) { t.Parallel() diff --git a/docs/client/request.md b/docs/client/request.md index 08a04e65e0..9789ea0d9a 100644 --- a/docs/client/request.md +++ b/docs/client/request.md @@ -211,6 +211,60 @@ Header method returns header value via key, this method will visit all field in func (r *Request) Header(key string) []string ``` +### Headers + +Headers returns all headers in the request using an iterator. You can use `maps.Collect()` to collect all headers into a map. +The returned value is valid until the request object is released. Any future calls to Headers method will return the modified value. Do not store references to returned value. Make copies instead. + +```go title="Signature" +func (r *Request) Headers() iter.Seq2[string, []string] +``` + +
+Example + +```go title="Example" +req := client.AcquireRequest() + +req.AddHeader("Golang", "Fiber") +req.AddHeader("Test", "123456") +req.AddHeader("Test", "654321") + +for k, v := range req.Headers() { + fmt.Printf("Header Key: %s, Header Value: %v\n", k, v) +} +``` + +```sh +Header Key: Golang, Header Value: [Fiber] +Header Key: Test, Header Value: [123456 654321] +``` + +
+ +
+Example with maps.Collect() + +```go title="Example with maps.Collect()" +req := client.AcquireRequest() + +req.AddHeader("Golang", "Fiber") +req.AddHeader("Test", "123456") +req.AddHeader("Test", "654321") + +headers := maps.Collect(req.Headers()) // Collect all headers into a map +for k, v := range headers { + fmt.Printf("Header Key: %s, Header Value: %v\n", k, v) +} +``` + +```sh +Header Key: Golang, Header Value: [Fiber] +Header Key: Test, Header Value: [123456 654321] +``` + +
+ ### AddHeader AddHeader method adds a single header field and its value in the request instance. @@ -219,6 +273,9 @@ AddHeader method adds a single header field and its value in the request instanc func (r *Request) AddHeader(key, val string) *Request ``` +
+Example + ```go title="Example" req := client.AcquireRequest() defer client.ReleaseRequest(req) @@ -235,9 +292,6 @@ if err != nil { fmt.Println(resp.String()) ``` -
-Click here to see the result - ```json { "headers": { @@ -262,6 +316,9 @@ It will override the header which has been set in the client instance. func (r *Request) SetHeader(key, val string) *Request ``` +
+Example + ```go title="Example" req := client.AcquireRequest() defer client.ReleaseRequest(req) @@ -277,9 +334,6 @@ if err != nil { fmt.Println(resp.String()) ``` -
-Click here to see the result - ```json { "headers": { @@ -320,6 +374,15 @@ Param method returns params value via key, this method will visit all field in t func (r *Request) Param(key string) []string ``` +### Params + +Params returns all params in the request using an iterator. You can use `maps.Collect()` to collect all params into a map. +The returned value is valid until the request object is released. Any future calls to Params method will return the modified value. Do not store references to returned value. Make copies instead. + +```go title="Signature" +func (r *Request) Params() iter.Seq2[string, []string] +``` + ### AddParam AddParam method adds a single param field and its value in the request instance. @@ -328,6 +391,9 @@ AddParam method adds a single param field and its value in the request instance. func (r *Request) AddParam(key, val string) *Request ``` +
+Example + ```go title="Example" req := client.AcquireRequest() defer client.ReleaseRequest(req) @@ -344,9 +410,6 @@ if err != nil { fmt.Println(string(resp.Body())) ``` -
-Click here to see the result - ```json { "Content-Length": "145", @@ -396,6 +459,9 @@ It will override param, which has been set in client instance. func (r *Request) SetParamsWithStruct(v any) *Request ``` +
+Example + ```go title="Example" req := client.AcquireRequest() defer client.ReleaseRequest(req) @@ -419,9 +485,6 @@ if err != nil { fmt.Println(string(resp.Body())) ``` -
-Click here to see the result - ```json { "Content-Length": "147", @@ -502,6 +565,14 @@ Cookie returns the cookie set in the request instance. If the cookie doesn't exi func (r *Request) Cookie(key string) string ``` +### Cookies + +Cookies returns all cookies in the request using an iterator. You can use `maps.Collect()` to collect all cookies into a map. + +```go title="Signature" +func (r *Request) Cookies() iter.Seq2[string, string] +``` + ### SetCookie SetCookie method sets a single cookie field and its value in the request instance. @@ -520,6 +591,9 @@ It will override the cookie which is set in the client instance. func (r *Request) SetCookies(m map[string]string) *Request ``` +
+Example + ```go title="Example" req := client.AcquireRequest() defer client.ReleaseRequest(req) @@ -537,9 +611,6 @@ if err != nil { fmt.Println(string(resp.Body())) ``` -
-Click here to see the result - ```json { "cookies": { @@ -575,6 +646,14 @@ PathParam returns the path param set in the request instance. If the path param func (r *Request) PathParam(key string) string ``` +### PathParams + +PathParams returns all path params in the request using an iterator. You can use `maps.Collect()` to collect all path params into a map. + +```go title="Signature" +func (r *Request) PathParams() iter.Seq2[string, string] +``` + ### SetPathParam SetPathParam method sets a single path param field and its value in the request instance. @@ -584,6 +663,9 @@ It will override path param which set in client instance. func (r *Request) SetPathParam(key, val string) *Request ``` +
+Example + ```go title="Example" req := client.AcquireRequest() defer client.ReleaseRequest(req) @@ -598,9 +680,6 @@ if err != nil { fmt.Println(string(resp.Body())) ``` -
-Click here to see the result - ```plaintext Gofiber ``` @@ -682,6 +761,14 @@ FormData method returns form data value via key, this method will visit all fiel func (r *Request) FormData(key string) []string ``` +### AllFormData + +AllFormData returns all form data in the request using an iterator. You can use `maps.Collect()` to collect all form data into a map. + +```go title="Signature" +func (r *Request) AllFormData() iter.Seq2[string, []string] +``` + ### AddFormData AddFormData method adds a single form data field and its value in the request instance. @@ -690,6 +777,9 @@ AddFormData method adds a single form data field and its value in the request in func (r *Request) AddFormData(key, val string) *Request ``` +
+Example + ```go title="Example" req := client.AcquireRequest() defer client.ReleaseRequest(req) @@ -706,9 +796,6 @@ if err != nil { fmt.Println(string(resp.Body())) ``` -
-Click here to see the result - ```json { "args": {}, @@ -735,6 +822,9 @@ SetFormData method sets a single form data field and its value in the request in func (r *Request) SetFormData(key, val string) *Request ``` +
+Example + ```go title="Example" req := client.AcquireRequest() defer client.ReleaseRequest(req) @@ -750,9 +840,6 @@ if err != nil { fmt.Println(string(resp.Body())) ``` -
-Click here to see the result - ```json { "args": {}, @@ -817,6 +904,15 @@ If the name field is empty, it will try to match path. func (r *Request) File(name string) *File ``` +### Files + +Files method returns all files in request instance. +The returned value is valid until the request object is released. Any future calls to Files method will return the modified value. Do not store references to returned value. Make copies instead. + +```go title="Signature" +func (r *Request) Files() []*File +``` + ### FileByPath FileByPath returns file ptr store in request obj by path. @@ -833,6 +929,9 @@ AddFile method adds a single file field and its value in the request instance vi func (r *Request) AddFile(path string) *Request ``` +
+Example + ```go title="Example" req := client.AcquireRequest() defer client.ReleaseRequest(req) @@ -847,9 +946,6 @@ if err != nil { fmt.Println(string(resp.Body())) ``` -
-Click here to see the result - ```json { "args": {}, @@ -872,6 +968,9 @@ AddFileWithReader method adds a single field and its value in the request instan func (r *Request) AddFileWithReader(name string, reader io.ReadCloser) *Request ``` +
+Example + ```go title="Example" req := client.AcquireRequest() defer client.ReleaseRequest(req) @@ -887,9 +986,6 @@ if err != nil { fmt.Println(string(resp.Body())) ``` -
-Click here to see the result - ```json { "args": {}, @@ -929,6 +1025,9 @@ It will override timeout which set in client instance. func (r *Request) SetTimeout(t time.Duration) *Request ``` +
+Example 1 + ```go title="Example 1" req := client.AcquireRequest() defer client.ReleaseRequest(req) @@ -943,9 +1042,6 @@ if err != nil { fmt.Println(string(resp.Body())) ``` -
-Click here to see the result - ```json { "args": {}, @@ -958,6 +1054,9 @@ fmt.Println(string(resp.Body()))
+
+Example 2 + ```go title="Example 2" req := client.AcquireRequest() defer client.ReleaseRequest(req) @@ -972,9 +1071,6 @@ if err != nil { fmt.Println(string(resp.Body())) ``` -
-Click here to see the result - ```shell panic: timeout or cancel @@ -1063,6 +1159,14 @@ type QueryParam struct { } ``` +### Keys + +Keys method returns all keys in the query params. + +```go title="Signature" +func (p *QueryParam) Keys() []string +``` + ### AddParams AddParams receive a map and add each value to param. @@ -1242,6 +1346,14 @@ type FormData struct { } ``` +### Keys + +Keys method returns all keys in the form data. + +```go title="Signature" +func (f *FormData) Keys() []string +``` + ### AddData AddData method is a wrapper of Args's Add method. diff --git a/docs/client/response.md b/docs/client/response.md index 0dba3c6b48..135970069c 100644 --- a/docs/client/response.md +++ b/docs/client/response.md @@ -68,6 +68,9 @@ Protocol method returns the HTTP response protocol used for the request. func (r *Response) Protocol() string ``` +
+Example + ```go title="Example" resp, err := client.Get("https://httpbin.org/get") if err != nil { @@ -77,9 +80,6 @@ if err != nil { fmt.Println(resp.Protocol()) ``` -
-Click here to see the result - ```text HTTP/1.1 ``` @@ -94,14 +94,74 @@ Header method returns the response headers. func (r *Response) Header(key string) string ``` +## Headers + +Headers returns all headers in the response using an iterator. You can use `maps.Collect()` to collect all headers into a map. +The returned value is valid until the response object is released. Any future calls to Headers method will return the modified value. Do not store references to returned value. Make copies instead. + +```go title="Signature" +func (r *Response) Headers() iter.Seq2[string, []string] +``` + +
+Example + +```go title="Example" +resp, err := client.Get("https://httpbin.org/get") +if err != nil { + panic(err) +} + +for key, values := range resp.Headers() { + fmt.Printf("%s => %s\n", key, strings.Join(values, ", ")) +} +``` + +```text +Date => Wed, 04 Dec 2024 15:28:29 GMT +Connection => keep-alive +Access-Control-Allow-Origin => * +Access-Control-Allow-Credentials => true +``` + +
+ +
+Example with maps.Collect() + +```go title="Example with maps.Collect()" +resp, err := client.Get("https://httpbin.org/get") +if err != nil { + panic(err) +} + +headers := maps.Collect(resp.Headers()) // Collect all headers into a map +for key, values := range headers { + fmt.Printf("%s => %s\n", key, strings.Join(values, ", ")) +} +``` + +```text +Date => Wed, 04 Dec 2024 15:28:29 GMT +Connection => keep-alive +Access-Control-Allow-Origin => * +Access-Control-Allow-Credentials => true +``` + +
+ ## Cookies Cookies method to access all the response cookies. +The returned value is valid until the response object is released. Any future calls to Cookies method will return the modified value. Do not store references to returned value. Make copies instead. ```go title="Signature" func (r *Response) Cookies() []*fasthttp.Cookie ``` +
+Example + ```go title="Example" resp, err := client.Get("https://httpbin.org/cookies/set/go/fiber") if err != nil { @@ -114,9 +174,6 @@ for _, cookie := range cookies { } ``` -
-Click here to see the result - ```text go => fiber ``` @@ -147,6 +204,9 @@ JSON method will unmarshal body to json. func (r *Response) JSON(v any) error ``` +
+Example + ```go title="Example" type Body struct { Slideshow struct { @@ -170,9 +230,6 @@ if err != nil { fmt.Printf("%+v\n", out) ``` -
-Click here to see the result - ```text {Slideshow:{Author:Yours Truly Date:date of publication Title:Sample Slide Show}} ``` From b513a00f4495636453523168dcf4eae9a5d38099 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 12:16:06 +0000 Subject: [PATCH 4/4] build(deps): bump golang.org/x/crypto from 0.28.0 to 0.30.0 Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.28.0 to 0.30.0. - [Commits](https://github.com/golang/crypto/compare/v0.28.0...v0.30.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index a979f1837e..4b1f36988c 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/tinylib/msgp v1.2.5 github.com/valyala/bytebufferpool v1.0.0 github.com/valyala/fasthttp v1.57.0 - golang.org/x/crypto v0.28.0 + golang.org/x/crypto v0.30.0 ) require ( @@ -25,7 +25,7 @@ require ( github.com/valyala/tcplisten v1.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect golang.org/x/net v0.30.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.19.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3601863b69..40860c3c65 100644 --- a/go.sum +++ b/go.sum @@ -35,16 +35,16 @@ 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.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= +golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=