From 1c526892e7ef932eb412932b7421d83b174d8e73 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Thu, 18 Jul 2024 07:41:39 -0400 Subject: [PATCH 01/31] =?UTF-8?q?=F0=9F=90=9B=20bug:=20Use=20Content-Lengt?= =?UTF-8?q?h=20for=20bytesReceived=20and=20bytesSent=20tags=20in=20Logger?= =?UTF-8?q?=20Middleware=20in=20v2=20(#3067)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use Content-Length for bytesSent and bytesReceived in Logger --- middleware/logger/logger_test.go | 5 +++-- middleware/logger/tags.go | 8 +++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/middleware/logger/logger_test.go b/middleware/logger/logger_test.go index 6876a7219fa..94b68d97514 100644 --- a/middleware/logger/logger_test.go +++ b/middleware/logger/logger_test.go @@ -386,13 +386,14 @@ func Test_Logger_AppendUint(t *testing.T) { })) app.Get("/", func(c *fiber.Ctx) error { + c.Response().Header.SetContentLength(5) return c.SendString("hello") }) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) utils.AssertEqual(t, nil, err) utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) - utils.AssertEqual(t, "0 5 200", buf.String()) + utils.AssertEqual(t, "-2 5 200", buf.String()) } // go test -run Test_Logger_Data_Race -race @@ -629,7 +630,7 @@ func Test_Logger_ByteSent_Streaming(t *testing.T) { resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) utils.AssertEqual(t, nil, err) utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) - utils.AssertEqual(t, "0 0 200", buf.String()) + utils.AssertEqual(t, "-2 -1 200", buf.String()) } // go test -run Test_Logger_EnableColors diff --git a/middleware/logger/tags.go b/middleware/logger/tags.go index 9a10279ecac..c348871c422 100644 --- a/middleware/logger/tags.go +++ b/middleware/logger/tags.go @@ -2,6 +2,7 @@ package logger import ( "fmt" + "strconv" "strings" "github.com/gofiber/fiber/v2" @@ -85,13 +86,10 @@ func createTagMap(cfg *Config) map[string]LogFunc { return output.Write(c.Body()) }, TagBytesReceived: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { - return appendInt(output, len(c.Request().Body())) + return output.WriteString(strconv.Itoa((c.Request().Header.ContentLength()))) }, TagBytesSent: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { - if c.Response().Header.ContentLength() < 0 { - return appendInt(output, 0) - } - return appendInt(output, len(c.Response().Body())) + return output.WriteString(strconv.Itoa((c.Response().Header.ContentLength()))) }, TagRoute: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { return output.WriteString(c.Route().Path) From 87bb93ecf089247402e7f1e4ed2423ea5c14749c Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Tue, 23 Jul 2024 02:25:25 -0400 Subject: [PATCH 02/31] v2: Update benchmark-action to v1.20.3 (#3084) Update benchmark gh action --- .github/workflows/benchmark.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 42e7afd6531..79e2c0d05c2 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -36,7 +36,7 @@ jobs: key: ${{ runner.os }}-benchmark - name: Save Benchmark Results - uses: benchmark-action/github-action-benchmark@v1.16.2 + uses: benchmark-action/github-action-benchmark@v1.20.3 with: tool: "go" output-file-path: output.txt From ca935c3f8f0ee28a8a8e91ddcd9b9f85dcfb0419 Mon Sep 17 00:00:00 2001 From: Giovanni Rivera Date: Wed, 28 Aug 2024 00:01:02 -0700 Subject: [PATCH 03/31] =?UTF-8?q?=F0=9F=93=9A=20Doc:=20Add=20detailed=20do?= =?UTF-8?q?cumentation=20for=20the=20templates=20guide=20(#3113)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Organize and reword templates guide * Add full example to templates guide * Add advanced templating section to template guide * Add template repo link and fix typo in Templates guide - Add link to https://github.com/gofiber/template in Templates Guide - Fix typo: missing period in info block about ctx.Render() * Update docs/guide/templates.md * Update docs/guide/templates.md --------- Co-authored-by: RW --- docs/guide/templates.md | 228 ++++++++++++++++++++++++++++++++++------ 1 file changed, 197 insertions(+), 31 deletions(-) diff --git a/docs/guide/templates.md b/docs/guide/templates.md index cc52c2d9ac0..393df1bb8dd 100644 --- a/docs/guide/templates.md +++ b/docs/guide/templates.md @@ -8,57 +8,212 @@ sidebar_position: 3 import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -## Template interfaces +Templates are a great tool to render dynamic content without using a separate frontend framework. -Fiber provides a Views interface to provide your own template engine: +## Template Engines - - +Fiber allows you to provide a custom template engine at app initialization. ```go +app := fiber.New(fiber.Config{ + // Pass in Views Template Engine + Views: engine, + + // Default global path to search for views (can be overriden when calling Render()) + ViewsLayout: "layouts/main", + + // Enables/Disables access to `ctx.Locals()` entries in rendered views + // (defaults to false) + PassLocalsToViews: false, +}) +``` + + +### Supported Engines + +The Fiber team maintains a [templates](https://docs.gofiber.io/template) package that provides wrappers for multiple template engines: + +* [ace](https://docs.gofiber.io/template/ace/) +* [amber](https://docs.gofiber.io/template/amber/) +* [django](https://docs.gofiber.io/template/django/) +* [handlebars](https://docs.gofiber.io/template/handlebars) +* [html](https://docs.gofiber.io/template/html) +* [jet](https://docs.gofiber.io/template/jet) +* [mustache](https://docs.gofiber.io/template/mustache) +* [pug](https://docs.gofiber.io/template/pug) +* [slim](https://docs.gofiber.io/template/slim) + +:::info +Custom template engines can implement the `Views` interface to be supported in Fiber. +::: + + +```go title="Views interface" type Views interface { + // Fiber executes Load() on app initialization to load/parse the templates Load() error + + // Outputs a template to the provided buffer using the provided template, + // template name, and binded data Render(io.Writer, string, interface{}, ...string) error } ``` + +:::note +The `Render` method is linked to the [**ctx.Render\(\)**](../api/ctx.md#render) function that accepts a template name and binding data. +::: + + +## Rendering Templates + +Once an engine is set up, a route handler can call the [**ctx.Render\(\)**](../api/ctx.md#render) function with a template name and binded data to send the rendered template. + +```go title="Signature" +func (c *Ctx) Render(name string, bind Map, layouts ...string) error +``` + +:::info +By default, [**ctx.Render\(\)**](../api/ctx.md#render) searches for the template name in the `ViewsLayout` path. To override this setting, provide the path(s) in the `layouts` argument. +::: + + + + + + +```go +app.Get("/", func(c *fiber.Ctx) error { + return c.Render("index", fiber.Map{ + "Title": "Hello, World!", + }) + +}) +``` + + + + +```html + + + +

{{.Title}}

+ + +``` + +
+
-`Views` interface contains a `Load` and `Render` method, `Load` is executed by Fiber on app initialization to load/parse the templates. +:::caution +If the Fiber config option `PassLocalsToViews` is enabled, then all locals set using `ctx.Locals(key, value)` will be passed to the template. It is important to avoid clashing keys when using this setting. +::: + +## Advanced Templating + +### Custom Functions + +Fiber supports adding custom functions to templates. + +#### AddFunc + +Adds a global function to all templates. + +```go title="Signature" +func (e *Engine) AddFunc(name string, fn interface{}) IEngineCore +``` + + + ```go -// Pass engine to Fiber's Views Engine +// Add `ToUpper` to engine +engine := html.New("./views", ".html") +engine.AddFunc("ToUpper", func(s string) string { + return strings.ToUpper(s) +} + +// Initialize Fiber App app := fiber.New(fiber.Config{ Views: engine, - // Views Layout is the global layout for all template render until override on Render function. - ViewsLayout: "layouts/main" +}) + +app.Get("/", func (c *fiber.Ctx) error { + return c.Render("index", fiber.Map{ + "Content": "hello, world!" + }) }) ``` -The `Render` method is linked to the [**ctx.Render\(\)**](../api/ctx.md#render) function that accepts a template name and binding data. It will use global layout if layout is not being defined in `Render` function. -If the Fiber config option `PassLocalsToViews` is enabled, then all locals set using `ctx.Locals(key, value)` will be passed to the template. + + + +```html + + + +

This will be in {{ToUpper "all caps"}}:

+

{{ToUpper .Content}}

+ + +``` + +
+
+ +#### AddFuncMap + +Adds a Map of functions (keyed by name) to all templates. + +```go title="Signature" +func (e *Engine) AddFuncMap(m map[string]interface{}) IEngineCore +``` + + + ```go -app.Get("/", func(c *fiber.Ctx) error { +// Add `ToUpper` to engine +engine := html.New("./views", ".html") +engine.AddFuncMap(map[string]interface{}{ + "ToUpper": func(s string) string { + return strings.ToUpper(s) + }, +}) + +// Initialize Fiber App +app := fiber.New(fiber.Config{ + Views: engine, +}) + +app.Get("/", func (c *fiber.Ctx) error { return c.Render("index", fiber.Map{ - "hello": "world", - }); + "Content": "hello, world!" + }) }) ``` -## Engines + + -Fiber team maintains [templates](https://docs.gofiber.io/template) package that provides wrappers for multiple template engines: +```html + + + +

This will be in {{ToUpper "all caps"}}:

+

{{ToUpper .Content}}

+ + +``` -* [ace](https://docs.gofiber.io/template/ace/) -* [amber](https://docs.gofiber.io/template/amber/) -* [django](https://docs.gofiber.io/template/django/) -* [handlebars](https://docs.gofiber.io/template/handlebars) -* [html](https://docs.gofiber.io/template/html) -* [jet](https://docs.gofiber.io/template/jet) -* [mustache](https://docs.gofiber.io/template/mustache) -* [pug](https://docs.gofiber.io/template/pug) -* [slim](https://docs.gofiber.io/template/slim) +
+
+ +- For more advanced template documentation, please visit the [gofiber/template GitHub Repository](https://github.com/gofiber/template). + +## Full Example @@ -75,7 +230,8 @@ import ( func main() { // Initialize standard Go html template engine engine := html.New("./views", ".html") - // If you want other engine, just replace with following + // If you want to use another engine, + // just replace with following: // Create a new engine with django // engine := django.New("./views", ".django") @@ -85,8 +241,10 @@ func main() { app.Get("/", func(c *fiber.Ctx) error { // Render index template return c.Render("index", fiber.Map{ - "Title": "Hello, World!", - }) + "Title": "Go Fiber Template Example", + "Description": "An example template", + "Greeting": "Hello, world!", + }); }) log.Fatal(app.Listen(":3000")) @@ -95,12 +253,20 @@ func main() { -```markup +```html - -

{{.Title}}

- + + + {{.Title}} + + + +

{{.Title}}

+

{{.Greeting}}

+ ``` +
+ From bfcf91dab87f9219c09f8f387690507033fb925d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Fri, 30 Aug 2024 17:22:07 +0200 Subject: [PATCH 04/31] fix template markdown --- docs/guide/templates.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/docs/guide/templates.md b/docs/guide/templates.md index 393df1bb8dd..18dd0e386c3 100644 --- a/docs/guide/templates.md +++ b/docs/guide/templates.md @@ -28,7 +28,6 @@ app := fiber.New(fiber.Config{ }) ``` - ### Supported Engines The Fiber team maintains a [templates](https://docs.gofiber.io/template) package that provides wrappers for multiple template engines: @@ -47,7 +46,6 @@ The Fiber team maintains a [templates](https://docs.gofiber.io/template) package Custom template engines can implement the `Views` interface to be supported in Fiber. ::: - ```go title="Views interface" type Views interface { // Fiber executes Load() on app initialization to load/parse the templates @@ -63,7 +61,6 @@ type Views interface { The `Render` method is linked to the [**ctx.Render\(\)**](../api/ctx.md#render) function that accepts a template name and binding data. ::: - ## Rendering Templates Once an engine is set up, a route handler can call the [**ctx.Render\(\)**](../api/ctx.md#render) function with a template name and binded data to send the rendered template. @@ -76,9 +73,7 @@ func (c *Ctx) Render(name string, bind Map, layouts ...string) error By default, [**ctx.Render\(\)**](../api/ctx.md#render) searches for the template name in the `ViewsLayout` path. To override this setting, provide the path(s) in the `layouts` argument. ::: - - ```go @@ -211,7 +206,7 @@ app.Get("/", func (c *fiber.Ctx) error { -- For more advanced template documentation, please visit the [gofiber/template GitHub Repository](https://github.com/gofiber/template). +* For more advanced template documentation, please visit the [gofiber/template GitHub Repository](https://github.com/gofiber/template). ## Full Example @@ -250,6 +245,7 @@ func main() { log.Fatal(app.Listen(":3000")) } ``` +
@@ -269,4 +265,3 @@ func main() {
- From cb06bc5f4cf27ae8abbfac227b8f4b558d97cf69 Mon Sep 17 00:00:00 2001 From: Vaibhav Gupta Date: Fri, 6 Sep 2024 11:32:02 +0530 Subject: [PATCH 05/31] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20handle=20un-matched?= =?UTF-8?q?=20open=20brackets=20in=20the=20query=20params=20(#3121)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add logic for counting open brackets * Add UTs * update increment/decrement syntax with ++/-- * Update UT to remove duplicate --- ctx.go | 21 +++++++++++++++++---- ctx_test.go | 8 ++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/ctx.go b/ctx.go index 2589f0e3e70..6f414ed56a9 100644 --- a/ctx.go +++ b/ctx.go @@ -1306,15 +1306,24 @@ func parseParamSquareBrackets(k string) (string, error) { defer bytebufferpool.Put(bb) kbytes := []byte(k) + openBracketsCount := 0 for i, b := range kbytes { - if b == '[' && kbytes[i+1] != ']' { - if err := bb.WriteByte('.'); err != nil { - return "", fmt.Errorf("failed to write: %w", err) + if b == '[' { + openBracketsCount++ + if i+1 < len(kbytes) && kbytes[i+1] != ']' { + if err := bb.WriteByte('.'); err != nil { + return "", fmt.Errorf("failed to write: %w", err) + } } + continue } - if b == '[' || b == ']' { + if b == ']' { + openBracketsCount-- + if openBracketsCount < 0 { + return "", errors.New("unmatched brackets") + } continue } @@ -1323,6 +1332,10 @@ func parseParamSquareBrackets(k string) (string, error) { } } + if openBracketsCount > 0 { + return "", errors.New("unmatched brackets") + } + return bb.String(), nil } diff --git a/ctx_test.go b/ctx_test.go index a278d2f2500..76ee3fa5f74 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -4508,6 +4508,10 @@ func Test_Ctx_QueryParser(t *testing.T) { utils.AssertEqual(t, nil, c.QueryParser(empty)) utils.AssertEqual(t, 0, len(empty.Hobby)) + c.Request().URI().SetQueryString("id=1&name[=tom") + q = new(Query) + utils.AssertEqual(t, "unmatched brackets", c.QueryParser(q).Error()) + type Query2 struct { Bool bool ID int @@ -4790,6 +4794,10 @@ func Test_Ctx_QueryParser_Schema(t *testing.T) { utils.AssertEqual(t, "doe", cq.Data[1].Name) utils.AssertEqual(t, 12, cq.Data[1].Age) + c.Request().URI().SetQueryString("data[0][name]=john&data[0][age]=10&data[1]name]=doe&data[1][age]=12") + cq = new(CollectionQuery) + utils.AssertEqual(t, "unmatched brackets", c.QueryParser(cq).Error()) + c.Request().URI().SetQueryString("data.0.name=john&data.0.age=10&data.1.name=doe&data.1.age=12") cq = new(CollectionQuery) utils.AssertEqual(t, nil, c.QueryParser(cq)) From 6e7411403adc3f82cb941b6a038566006ed6e270 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Fri, 6 Sep 2024 14:18:20 -0400 Subject: [PATCH 06/31] v2: Add CODEOWNERS file (#3124) Add CODEOWNERS file --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000000..957d1b1926b --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @gofiber/maintainers From 8c84b0fd8a07537572e607057b309681922a0b46 Mon Sep 17 00:00:00 2001 From: Aaron Zingerle Date: Mon, 14 Oct 2024 15:04:25 +0200 Subject: [PATCH 07/31] =?UTF-8?q?=F0=9F=A9=B9=20fix:=20Middleware/CORS=20R?= =?UTF-8?q?emove=20Scheme=20Restriction=20(#3168)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🩹 Fix: Middleware/CORS Remove Scheme Restriction (gofiber#3160) Co-authored-by: Aaron Zingerle --- middleware/cors/utils.go | 5 ---- middleware/cors/utils_test.go | 49 ++++++++++++++++++----------------- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/middleware/cors/utils.go b/middleware/cors/utils.go index 443e6489034..42780103057 100644 --- a/middleware/cors/utils.go +++ b/middleware/cors/utils.go @@ -37,11 +37,6 @@ func normalizeOrigin(origin string) (bool, string) { return false, "" } - // Validate the scheme is either http or https - if parsedOrigin.Scheme != "http" && parsedOrigin.Scheme != "https" { - return false, "" - } - // Don't allow a wildcard with a protocol // wildcards cannot be used within any other value. For example, the following header is not valid: // Access-Control-Allow-Origin: https://* diff --git a/middleware/cors/utils_test.go b/middleware/cors/utils_test.go index 47dddc2c693..aff50defa25 100644 --- a/middleware/cors/utils_test.go +++ b/middleware/cors/utils_test.go @@ -13,30 +13,31 @@ func Test_normalizeOrigin(t *testing.T) { expectedValid bool expectedOrigin string }{ - {"http://example.com", true, "http://example.com"}, // Simple case should work. - {"http://example.com/", true, "http://example.com"}, // Trailing slash should be removed. - {"http://example.com:3000", true, "http://example.com:3000"}, // Port should be preserved. - {"http://example.com:3000/", true, "http://example.com:3000"}, // Trailing slash should be removed. - {"http://", false, ""}, // Invalid origin should not be accepted. - {"file:///etc/passwd", false, ""}, // File scheme should not be accepted. - {"https://*example.com", false, ""}, // Wildcard domain should not be accepted. - {"http://*.example.com", false, ""}, // Wildcard subdomain should not be accepted. - {"http://example.com/path", false, ""}, // Path should not be accepted. - {"http://example.com?query=123", false, ""}, // Query should not be accepted. - {"http://example.com#fragment", false, ""}, // Fragment should not be accepted. - {"http://localhost", true, "http://localhost"}, // Localhost should be accepted. - {"http://127.0.0.1", true, "http://127.0.0.1"}, // IPv4 address should be accepted. - {"http://[::1]", true, "http://[::1]"}, // IPv6 address should be accepted. - {"http://[::1]:8080", true, "http://[::1]:8080"}, // IPv6 address with port should be accepted. - {"http://[::1]:8080/", true, "http://[::1]:8080"}, // IPv6 address with port and trailing slash should be accepted. - {"http://[::1]:8080/path", false, ""}, // IPv6 address with port and path should not be accepted. - {"http://[::1]:8080?query=123", false, ""}, // IPv6 address with port and query should not be accepted. - {"http://[::1]:8080#fragment", false, ""}, // IPv6 address with port and fragment should not be accepted. - {"http://[::1]:8080/path?query=123#fragment", false, ""}, // IPv6 address with port, path, query, and fragment should not be accepted. - {"http://[::1]:8080/path?query=123#fragment/", false, ""}, // IPv6 address with port, path, query, fragment, and trailing slash should not be accepted. - {"http://[::1]:8080/path?query=123#fragment/invalid", false, ""}, // IPv6 address with port, path, query, fragment, trailing slash, and invalid segment should not be accepted. - {"http://[::1]:8080/path?query=123#fragment/invalid/", false, ""}, // IPv6 address with port, path, query, fragment, trailing slash, and invalid segment with trailing slash should not be accepted. - {"http://[::1]:8080/path?query=123#fragment/invalid/segment", false, ""}, // IPv6 address with port, path, query, fragment, trailing slash, and invalid segment with additional segment should not be accepted. + {origin: "http://example.com", expectedValid: true, expectedOrigin: "http://example.com"}, // Simple case should work. + {origin: "http://example.com/", expectedValid: true, expectedOrigin: "http://example.com"}, // Trailing slash should be removed. + {origin: "http://example.com:3000", expectedValid: true, expectedOrigin: "http://example.com:3000"}, // Port should be preserved. + {origin: "http://example.com:3000/", expectedValid: true, expectedOrigin: "http://example.com:3000"}, // Trailing slash should be removed. + {origin: "app://example.com/", expectedValid: true, expectedOrigin: "app://example.com"}, // App scheme should be accepted. + {origin: "http://", expectedValid: false, expectedOrigin: ""}, // Invalid origin should not be accepted. + {origin: "file:///etc/passwd", expectedValid: false, expectedOrigin: ""}, // File scheme should not be accepted. + {origin: "https://*example.com", expectedValid: false, expectedOrigin: ""}, // Wildcard domain should not be accepted. + {origin: "http://*.example.com", expectedValid: false, expectedOrigin: ""}, // Wildcard subdomain should not be accepted. + {origin: "http://example.com/path", expectedValid: false, expectedOrigin: ""}, // Path should not be accepted. + {origin: "http://example.com?query=123", expectedValid: false, expectedOrigin: ""}, // Query should not be accepted. + {origin: "http://example.com#fragment", expectedValid: false, expectedOrigin: ""}, // Fragment should not be accepted. + {origin: "http://localhost", expectedValid: true, expectedOrigin: "http://localhost"}, // Localhost should be accepted. + {origin: "http://127.0.0.1", expectedValid: true, expectedOrigin: "http://127.0.0.1"}, // IPv4 address should be accepted. + {origin: "http://[::1]", expectedValid: true, expectedOrigin: "http://[::1]"}, // IPv6 address should be accepted. + {origin: "http://[::1]:8080", expectedValid: true, expectedOrigin: "http://[::1]:8080"}, // IPv6 address with port should be accepted. + {origin: "http://[::1]:8080/", expectedValid: true, expectedOrigin: "http://[::1]:8080"}, // IPv6 address with port and trailing slash should be accepted. + {origin: "http://[::1]:8080/path", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port and path should not be accepted. + {origin: "http://[::1]:8080?query=123", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port and query should not be accepted. + {origin: "http://[::1]:8080#fragment", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port and fragment should not be accepted. + {origin: "http://[::1]:8080/path?query=123#fragment", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port, path, query, and fragment should not be accepted. + {origin: "http://[::1]:8080/path?query=123#fragment/", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port, path, query, fragment, and trailing slash should not be accepted. + {origin: "http://[::1]:8080/path?query=123#fragment/invalid", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port, path, query, fragment, trailing slash, and invalid segment should not be accepted. + {origin: "http://[::1]:8080/path?query=123#fragment/invalid/", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port, path, query, fragment, trailing slash, and invalid segment with trailing slash should not be accepted. + {origin: "http://[::1]:8080/path?query=123#fragment/invalid/segment", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port, path, query, fragment, trailing slash, and invalid segment with additional segment should not be accepted. } for _, tc := range testCases { From 56ff2de85895bb8eee062700fd205085bf0864c3 Mon Sep 17 00:00:00 2001 From: nickajacks1 <128185314+nickajacks1@users.noreply.github.com> Date: Thu, 12 Dec 2024 23:28:36 -0800 Subject: [PATCH 08/31] =?UTF-8?q?=F0=9F=90=9B=20fix:=20Respect=20Immutable?= =?UTF-8?q?=20config=20for=20Body()=20(#3246)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛 fix: respect Immutable config for Body() * ci: add go 1.22 and 1.23 to test matrix --- .github/workflows/test.yml | 2 +- ctx.go | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a186908aa0b..00566dd9c00 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: Build: strategy: matrix: - go-version: [1.17.x, 1.18.x, 1.19.x, 1.20.x, 1.21.x] + go-version: [1.17.x, 1.18.x, 1.19.x, 1.20.x, 1.21.x, 1.22.x, 1.23.x] platform: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.platform }} steps: diff --git a/ctx.go b/ctx.go index 6f414ed56a9..c7caada7020 100644 --- a/ctx.go +++ b/ctx.go @@ -268,7 +268,7 @@ func (c *Ctx) BaseURL() string { // Returned value is only valid within the handler. Do not store any references. // Make copies or use the Immutable setting instead. func (c *Ctx) BodyRaw() []byte { - return c.fasthttp.Request.Body() + return c.getBody() } func (c *Ctx) tryDecodeBodyInOrder( @@ -340,7 +340,7 @@ func (c *Ctx) Body() []byte { // rule defined at: https://www.rfc-editor.org/rfc/rfc9110#section-8.4-5 encodingOrder = getSplicedStrList(headerEncoding, encodingOrder) if len(encodingOrder) == 0 { - return c.fasthttp.Request.Body() + return c.getBody() } var decodesRealized uint8 @@ -354,6 +354,9 @@ func (c *Ctx) Body() []byte { return []byte(err.Error()) } + if c.app.config.Immutable { + return utils.CopyBytes(body) + } return body } @@ -2004,3 +2007,11 @@ func (*Ctx) isLocalHost(address string) bool { func (c *Ctx) IsFromLocal() bool { return c.isLocalHost(c.fasthttp.RemoteIP().String()) } + +func (c *Ctx) getBody() []byte { + if c.app.config.Immutable { + return utils.CopyBytes(c.fasthttp.Request.Body()) + } + + return c.fasthttp.Request.Body() +} From c9ff17d796ea9fea65408b8c8017d57720098d7b Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Wed, 18 Dec 2024 09:40:07 -0500 Subject: [PATCH 09/31] =?UTF-8?q?=F0=9F=A7=B9=20chore:=20Update=20dependen?= =?UTF-8?q?cies=20(#3254)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update v2 dependencies * Try fasthttp v1.55.0 * Try fasthttp v1.54.0 * Try fasthttp v1.53.0 * Try fasthttp v1.52.0 * Try fasthttp v1.51.0 * Add Makefile * Bump msgp to v1.2.5 --- Makefile | 55 +++++++++++++++++++++++++++++++++++++++++++ go.mod | 16 +++++++------ go.sum | 64 ++++++++++++++++---------------------------------- router_test.go | 21 +++++++++++++++++ 4 files changed, 105 insertions(+), 51 deletions(-) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 00000000000..d2191043831 --- /dev/null +++ b/Makefile @@ -0,0 +1,55 @@ +## help: 💡 Display available commands +.PHONY: help +help: + @echo '⚡️ GoFiber/Fiber Development:' + @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' + +## audit: 🚀 Conduct quality checks +.PHONY: audit +audit: + go mod verify + go vet ./... + go run golang.org/x/vuln/cmd/govulncheck@latest ./... + +## benchmark: 📈 Benchmark code performance +.PHONY: benchmark +benchmark: + go test ./... -benchmem -bench=. -run=^Benchmark_$ + +## coverage: ☂️ Generate coverage report +.PHONY: coverage +coverage: + go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=1 -coverprofile=/tmp/coverage.out -covermode=atomic + go tool cover -html=/tmp/coverage.out + +## format: 🎨 Fix code format issues +.PHONY: format +format: + go run mvdan.cc/gofumpt@latest -w -l . + +## lint: 🚨 Run lint checks +.PHONY: lint +lint: + go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.51.0 run ./... + +## test: 🚦 Execute all tests +.PHONY: test +test: + go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=1 -shuffle=on + +## longtest: 🚦 Execute all tests 10x +.PHONY: longtest +longtest: + go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=15 -shuffle=on + +## tidy: 📌 Clean and tidy dependencies +.PHONY: tidy +tidy: + go mod tidy -v + +## generate: ⚡️ Generate msgp && interface implementations +.PHONY: generate +generate: + go install github.com/tinylib/msgp@latest + go install github.com/vburenin/ifacemaker@975a95966976eeb2d4365a7fb236e274c54da64c + go generate ./... diff --git a/go.mod b/go.mod index 4112a1c4f1c..38efaa551ce 100644 --- a/go.mod +++ b/go.mod @@ -3,20 +3,22 @@ module github.com/gofiber/fiber/v2 go 1.20 require ( - github.com/google/uuid v1.5.0 + github.com/google/uuid v1.6.0 github.com/mattn/go-colorable v0.1.13 github.com/mattn/go-isatty v0.0.20 - github.com/mattn/go-runewidth v0.0.15 - github.com/tinylib/msgp v1.1.8 + github.com/mattn/go-runewidth v0.0.16 + github.com/tinylib/msgp v1.2.5 github.com/valyala/bytebufferpool v1.0.0 github.com/valyala/fasthttp v1.51.0 - golang.org/x/sys v0.15.0 + golang.org/x/sys v0.28.0 ) require ( - github.com/andybalholm/brotli v1.0.5 // indirect - github.com/klauspost/compress v1.17.0 // indirect - github.com/philhofer/fwd v1.1.2 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/tools v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index f000e1cacb2..06b81bad6fa 100644 --- a/go.sum +++ b/go.sum @@ -1,59 +1,35 @@ -github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= -github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= -github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= -github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= -github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= +github.com/tinylib/msgp v1.1.3 h1:3giwAkmtaEDLSV0MdO1lDLuPgklgPzmk8H9+So2BVfA= +github.com/tinylib/msgp v1.1.3/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +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.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= 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/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= diff --git a/router_test.go b/router_test.go index 6a43db59378..b47b025efe8 100644 --- a/router_test.go +++ b/router_test.go @@ -868,3 +868,24 @@ type routeJSON struct { TestRoutes []testRoute `json:"test_routes"` GithubAPI []testRoute `json:"github_api"` } + +// go test -v ./... -run=^$ -bench=Benchmark_Router_Next_Default -benchmem -count=4 +func Benchmark_Router_Next_Default(b *testing.B) { + app := New() + app.Get("/", func(_ *Ctx) error { + return nil + }) + + h := app.Handler() + + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(MethodGet) + fctx.Request.SetRequestURI("/") + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + h(fctx) + } +} From 47be68142a25475ba6ff84b270395ebd7b691cbb Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Sat, 21 Dec 2024 04:51:16 -0500 Subject: [PATCH 10/31] =?UTF-8?q?=F0=9F=A7=B9=20chore:=20Add=20parallel=20?= =?UTF-8?q?benchmark=20for=20Next()=20(#3259)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add parallel benchmark for Next() * Create RequestCtx outside loop --- router_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/router_test.go b/router_test.go index b47b025efe8..8d1e40cbd0b 100644 --- a/router_test.go +++ b/router_test.go @@ -889,3 +889,26 @@ func Benchmark_Router_Next_Default(b *testing.B) { h(fctx) } } + +// go test -v ./... -run=^$ -bench=Benchmark_Router_Next_Default_Parallel -benchmem -count=4 +func Benchmark_Router_Next_Default_Parallel(b *testing.B) { + app := New() + app.Get("/", func(_ *Ctx) error { + return nil + }) + + h := app.Handler() + + b.ReportAllocs() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(MethodGet) + fctx.Request.SetRequestURI("/") + + for pb.Next() { + h(fctx) + } + }) +} From 7eb9d255489192343c647f1d910abbb140705474 Mon Sep 17 00:00:00 2001 From: RW Date: Tue, 31 Dec 2024 16:56:18 +0100 Subject: [PATCH 11/31] Support Square Bracket Notation in Multipart Form data (#3268) * Feature Request: Support Square Bracket Notation in Multipart Form Data #3224 * Feature Request: Support Square Bracket Notation in Multipart Form Data #3224 --- ctx.go | 113 ++++++++++++++-------------------------------------- ctx_test.go | 42 +++++++++++++++++++ go.mod | 2 - go.sum | 6 --- helpers.go | 73 +++++++++++++++++++++++++++++++++ 5 files changed, 144 insertions(+), 92 deletions(-) diff --git a/ctx.go b/ctx.go index c7caada7020..c0a4413ab03 100644 --- a/ctx.go +++ b/ctx.go @@ -406,28 +406,30 @@ func (c *Ctx) BodyParser(out interface{}) error { k := c.app.getString(key) v := c.app.getString(val) - if strings.Contains(k, "[") { - k, err = parseParamSquareBrackets(k) - } - - if c.app.config.EnableSplittingOnParsers && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k, bodyTag) { - values := strings.Split(v, ",") - for i := 0; i < len(values); i++ { - data[k] = append(data[k], values[i]) - } - } else { - data[k] = append(data[k], v) - } + err = formatParserData(out, data, bodyTag, k, v, c.app.config.EnableSplittingOnParsers, true) }) + if err != nil { + return err + } + return c.parseToStruct(bodyTag, out, data) } if strings.HasPrefix(ctype, MIMEMultipartForm) { - data, err := c.fasthttp.MultipartForm() + multipartForm, err := c.fasthttp.MultipartForm() if err != nil { return err } - return c.parseToStruct(bodyTag, out, data.Value) + + data := make(map[string][]string) + for key, values := range multipartForm.Value { + err = formatParserData(out, data, bodyTag, key, values, c.app.config.EnableSplittingOnParsers, true) + if err != nil { + return err + } + } + + return c.parseToStruct(bodyTag, out, data) } if strings.HasPrefix(ctype, MIMETextXML) || strings.HasPrefix(ctype, MIMEApplicationXML) { if err := xml.Unmarshal(c.Body(), out); err != nil { @@ -531,18 +533,7 @@ func (c *Ctx) CookieParser(out interface{}) error { k := c.app.getString(key) v := c.app.getString(val) - if strings.Contains(k, "[") { - k, err = parseParamSquareBrackets(k) - } - - if c.app.config.EnableSplittingOnParsers && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k, cookieTag) { - values := strings.Split(v, ",") - for i := 0; i < len(values); i++ { - data[k] = append(data[k], values[i]) - } - } else { - data[k] = append(data[k], v) - } + err = formatParserData(out, data, cookieTag, k, v, c.app.config.EnableSplittingOnParsers, true) }) if err != nil { return err @@ -1283,18 +1274,7 @@ func (c *Ctx) QueryParser(out interface{}) error { k := c.app.getString(key) v := c.app.getString(val) - if strings.Contains(k, "[") { - k, err = parseParamSquareBrackets(k) - } - - if c.app.config.EnableSplittingOnParsers && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k, queryTag) { - values := strings.Split(v, ",") - for i := 0; i < len(values); i++ { - data[k] = append(data[k], values[i]) - } - } else { - data[k] = append(data[k], v) - } + err = formatParserData(out, data, queryTag, k, v, c.app.config.EnableSplittingOnParsers, true) }) if err != nil { @@ -1304,61 +1284,26 @@ func (c *Ctx) QueryParser(out interface{}) error { return c.parseToStruct(queryTag, out, data) } -func parseParamSquareBrackets(k string) (string, error) { - bb := bytebufferpool.Get() - defer bytebufferpool.Put(bb) - - kbytes := []byte(k) - openBracketsCount := 0 - - for i, b := range kbytes { - if b == '[' { - openBracketsCount++ - if i+1 < len(kbytes) && kbytes[i+1] != ']' { - if err := bb.WriteByte('.'); err != nil { - return "", fmt.Errorf("failed to write: %w", err) - } - } - continue - } - - if b == ']' { - openBracketsCount-- - if openBracketsCount < 0 { - return "", errors.New("unmatched brackets") - } - continue - } - - if err := bb.WriteByte(b); err != nil { - return "", fmt.Errorf("failed to write: %w", err) - } - } - - if openBracketsCount > 0 { - return "", errors.New("unmatched brackets") - } - - return bb.String(), nil -} - // ReqHeaderParser binds the request header strings to a struct. func (c *Ctx) ReqHeaderParser(out interface{}) error { data := make(map[string][]string) + var err error + c.fasthttp.Request.Header.VisitAll(func(key, val []byte) { + if err != nil { + return + } + k := c.app.getString(key) v := c.app.getString(val) - if c.app.config.EnableSplittingOnParsers && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k, reqHeaderTag) { - values := strings.Split(v, ",") - for i := 0; i < len(values); i++ { - data[k] = append(data[k], values[i]) - } - } else { - data[k] = append(data[k], v) - } + err = formatParserData(out, data, reqHeaderTag, k, v, c.app.config.EnableSplittingOnParsers, false) }) + if err != nil { + return err + } + return c.parseToStruct(reqHeaderTag, out, data) } diff --git a/ctx_test.go b/ctx_test.go index 76ee3fa5f74..6a3998f7fa2 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -610,6 +610,48 @@ func Test_Ctx_BodyParser(t *testing.T) { utils.AssertEqual(t, 2, len(cq.Data)) utils.AssertEqual(t, "john", cq.Data[0].Name) utils.AssertEqual(t, "doe", cq.Data[1].Name) + + t.Run("MultipartCollectionQueryDotNotation", func(t *testing.T) { + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().Reset() + + buf := &bytes.Buffer{} + writer := multipart.NewWriter(buf) + utils.AssertEqual(t, nil, writer.WriteField("data.0.name", "john")) + utils.AssertEqual(t, nil, writer.WriteField("data.1.name", "doe")) + utils.AssertEqual(t, nil, writer.Close()) + + c.Request().Header.SetContentType(writer.FormDataContentType()) + c.Request().SetBody(buf.Bytes()) + c.Request().Header.SetContentLength(len(c.Body())) + + cq := new(CollectionQuery) + utils.AssertEqual(t, nil, c.BodyParser(cq)) + utils.AssertEqual(t, len(cq.Data), 2) + utils.AssertEqual(t, "john", cq.Data[0].Name) + utils.AssertEqual(t, "doe", cq.Data[1].Name) + }) + + t.Run("MultipartCollectionQuerySquareBrackets", func(t *testing.T) { + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().Reset() + + buf := &bytes.Buffer{} + writer := multipart.NewWriter(buf) + utils.AssertEqual(t, nil, writer.WriteField("data[0][name]", "john")) + utils.AssertEqual(t, nil, writer.WriteField("data[1][name]", "doe")) + utils.AssertEqual(t, nil, writer.Close()) + + c.Request().Header.SetContentType(writer.FormDataContentType()) + c.Request().SetBody(buf.Bytes()) + c.Request().Header.SetContentLength(len(c.Body())) + + cq := new(CollectionQuery) + utils.AssertEqual(t, nil, c.BodyParser(cq)) + utils.AssertEqual(t, len(cq.Data), 2) + utils.AssertEqual(t, "john", cq.Data[0].Name) + utils.AssertEqual(t, "doe", cq.Data[1].Name) + }) } func Test_Ctx_ParamParser(t *testing.T) { diff --git a/go.mod b/go.mod index 38efaa551ce..4a3b38df97e 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,4 @@ require ( github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - golang.org/x/mod v0.18.0 // indirect - golang.org/x/tools v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index 06b81bad6fa..67a47b010ac 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,6 @@ github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1Gsh github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/tinylib/msgp v1.1.3 h1:3giwAkmtaEDLSV0MdO1lDLuPgklgPzmk8H9+So2BVfA= -github.com/tinylib/msgp v1.1.3/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= 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= @@ -25,11 +23,7 @@ github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1S github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= diff --git a/helpers.go b/helpers.go index dd8de15f91c..2896c2a47d9 100644 --- a/helpers.go +++ b/helpers.go @@ -7,6 +7,7 @@ package fiber import ( "bytes" "crypto/tls" + "errors" "fmt" "hash/crc32" "io" @@ -1151,3 +1152,75 @@ func IndexRune(str string, needle int32) bool { } return false } + +func parseParamSquareBrackets(k string) (string, error) { + bb := bytebufferpool.Get() + defer bytebufferpool.Put(bb) + + kbytes := []byte(k) + openBracketsCount := 0 + + for i, b := range kbytes { + if b == '[' { + openBracketsCount++ + if i+1 < len(kbytes) && kbytes[i+1] != ']' { + if err := bb.WriteByte('.'); err != nil { + return "", fmt.Errorf("failed to write: %w", err) + } + } + continue + } + + if b == ']' { + openBracketsCount-- + if openBracketsCount < 0 { + return "", errors.New("unmatched brackets") + } + continue + } + + if err := bb.WriteByte(b); err != nil { + return "", fmt.Errorf("failed to write: %w", err) + } + } + + if openBracketsCount > 0 { + return "", errors.New("unmatched brackets") + } + + return bb.String(), nil +} + +func formatParserData(out interface{}, data map[string][]string, aliasTag, key string, value interface{}, enableSplitting, supportBracketNotation bool) error { //nolint:revive // it's okay + var err error + if supportBracketNotation && strings.Contains(key, "[") { + key, err = parseParamSquareBrackets(key) + if err != nil { + return err + } + } + + switch v := value.(type) { + case string: + assignBindData(out, data, aliasTag, key, v, enableSplitting) + case []string: + for _, val := range v { + assignBindData(out, data, aliasTag, key, val, enableSplitting) + } + default: + return fmt.Errorf("unsupported value type: %T", value) + } + + return err +} + +func assignBindData(out interface{}, data map[string][]string, aliasTag, key, value string, enableSplitting bool) { //nolint:revive // it's okay + if enableSplitting && strings.Contains(value, ",") && equalFieldType(out, reflect.Slice, key, aliasTag) { + values := strings.Split(value, ",") + for i := 0; i < len(values); i++ { + data[key] = append(data[key], values[i]) + } + } else { + data[key] = append(data[key], value) + } +} From e04f815c43cb8a84f4de90e1d1bd9507bc33fa8c Mon Sep 17 00:00:00 2001 From: RW Date: Tue, 31 Dec 2024 18:04:19 +0100 Subject: [PATCH 12/31] prepare release v2.52.6 --- app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.go b/app.go index cea6407a93c..751df9f232a 100644 --- a/app.go +++ b/app.go @@ -30,7 +30,7 @@ import ( ) // Version of current fiber package -const Version = "2.52.5" +const Version = "2.52.6" // Handler defines a function to serve HTTP requests. type Handler = func(*Ctx) error From 3729281a1ebd8e3be5f7a3e008996070795b9a89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Wed, 22 Jan 2025 08:35:17 +0100 Subject: [PATCH 13/31] Doc BodyParser: Add multipart form info about the file data --- docs/api/ctx.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/api/ctx.md b/docs/api/ctx.md index fdb2adc0105..d24eef21617 100644 --- a/docs/api/ctx.md +++ b/docs/api/ctx.md @@ -276,6 +276,12 @@ It is important to specify the correct struct tag based on the content type to b | `application/xml` | xml | | `text/xml` | xml | +:::note + +When handling `multipart/form-data`, only the form values can be directly assigned to the struct fields. Files included in the request are not automatically assigned to the struct. You must handle files separately using [`FormFile`](#FormFile) or other file-specific methods. + +::: + ```go title="Signature" func (c *Ctx) BodyParser(out interface{}) error ``` From 8b9db059d7cc2c93a97040e2d225bf40076e7859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Thu, 23 Jan 2025 08:32:43 +0100 Subject: [PATCH 14/31] Doc BodyParser: Add multipart form info about the file data --- docs/api/ctx.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/ctx.md b/docs/api/ctx.md index d24eef21617..40d03ca5ae5 100644 --- a/docs/api/ctx.md +++ b/docs/api/ctx.md @@ -278,7 +278,7 @@ It is important to specify the correct struct tag based on the content type to b :::note -When handling `multipart/form-data`, only the form values can be directly assigned to the struct fields. Files included in the request are not automatically assigned to the struct. You must handle files separately using [`FormFile`](#FormFile) or other file-specific methods. +When handling `multipart/form-data`, only the form values can be directly assigned to the struct fields. Files included in the request are not automatically assigned to the struct. You must handle files separately using [`FormFile`](#formfile) or other file-specific methods. ::: From 42d921d353c0255197245d12a55d9280d0157c10 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Mon, 27 Jan 2025 09:00:51 -0500 Subject: [PATCH 15/31] =?UTF-8?q?=F0=9F=A7=B9=20chore:=20Backport=20ctx.St?= =?UTF-8?q?ring()=20from=20v3=20(#3294)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Backport ctx.String() to v2 * Fix lint issues --- ctx.go | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/ctx.go b/ctx.go index c0a4413ab03..b418dc8e906 100644 --- a/ctx.go +++ b/ctx.go @@ -1834,14 +1834,39 @@ func (c *Ctx) Status(status int) *Ctx { // // The returned value may be useful for logging. func (c *Ctx) String() string { - return fmt.Sprintf( - "#%016X - %s <-> %s - %s %s", - c.fasthttp.ID(), - c.fasthttp.LocalAddr(), - c.fasthttp.RemoteAddr(), - c.fasthttp.Request.Header.Method(), - c.fasthttp.URI().FullURI(), - ) + // Get buffer from pool + buf := bytebufferpool.Get() + + // Start with the ID, converting it to a hex string without fmt.Sprintf + buf.WriteByte('#') //nolint:errcheck // Not needed here + // Convert ID to hexadecimal + id := strconv.FormatUint(c.fasthttp.ID(), 16) + // Pad with leading zeros to ensure 16 characters + for i := 0; i < (16 - len(id)); i++ { + buf.WriteByte('0') //nolint:errcheck // Not needed here + } + buf.WriteString(id) //nolint:errcheck // Not needed here + buf.WriteString(" - ") //nolint:errcheck // Not needed here + + // Add local and remote addresses directly + buf.WriteString(c.fasthttp.LocalAddr().String()) //nolint:errcheck // Not needed here + buf.WriteString(" <-> ") //nolint:errcheck // Not needed here + buf.WriteString(c.fasthttp.RemoteAddr().String()) //nolint:errcheck // Not needed here + buf.WriteString(" - ") //nolint:errcheck // Not needed here + + // Add method and URI + buf.Write(c.fasthttp.Request.Header.Method()) //nolint:errcheck // Not needed here + buf.WriteByte(' ') //nolint:errcheck // Not needed here + buf.Write(c.fasthttp.URI().FullURI()) //nolint:errcheck // Not needed here + + // Allocate string + str := buf.String() + + // Reset buffer + buf.Reset() + bytebufferpool.Put(buf) + + return str } // Type sets the Content-Type HTTP header to the MIME type specified by the file extension. From 8add1ad27043edd06d4d9c09f3da04d33ea7c0be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Wed, 9 Apr 2025 15:38:16 +0200 Subject: [PATCH 16/31] update benchmarks.md --- docs/extra/benchmarks.md | 76 +++++++++------------------------------- 1 file changed, 17 insertions(+), 59 deletions(-) diff --git a/docs/extra/benchmarks.md b/docs/extra/benchmarks.md index 3c2a82037dd..02bc190d111 100644 --- a/docs/extra/benchmarks.md +++ b/docs/extra/benchmarks.md @@ -9,15 +9,16 @@ sidebar_position: 2 ## TechEmpower -[TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=composite) provides a performance comparison of many web application frameworks executing fundamental tasks such as JSON serialization, database access, and server-side template composition. +[TechEmpower](https://www.techempower.com/benchmarks/#section=data-r23) provides a performance comparison of many web application frameworks executing fundamental tasks such as JSON serialization, database access, and server-side template composition. Each framework is operating in a realistic production configuration. Results are captured on cloud instances and on physical hardware. The test implementations are largely community-contributed and all source is available at the [GitHub repository](https://github.com/TechEmpower/FrameworkBenchmarks). -* Fiber `v1.10.0` -* 28 HT Cores Intel\(R\) Xeon\(R\) Gold 5120 CPU @ 2.20GHz -* 32GB RAM -* Ubuntu 18.04.3 4.15.0-88-generic -* Dedicated Cisco 10-Gbit Ethernet switch. +* Fiber `v2.52.5` +* 56 Cores Intel(R) Xeon(R) Gold 6330 CPU @ 2.00GHz (Three homogeneous ProLiant DL360 Gen10 Plus) +* 64GB RAM +* Enterprise SSD +* Ubuntu +* Mellanox Technologies MT28908 Family ConnectX-6 40Gbps Ethernet ### Plaintext @@ -25,8 +26,8 @@ The Plaintext test is an exercise of the request-routing fundamentals only, desi See [Plaintext requirements](https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview#single-database-query) -**Fiber** - **6,162,556** responses per second with an average latency of **2.0** ms. -**Express** - **367,069** responses per second with an average latency of **354.1** ms. +**Fiber** - **13,509,592** responses per second with an average latency of **0.9** ms. +**Express** - **279,922** responses per second with an average latency of **551.3** ms. ![](/img/plaintext.png) @@ -34,8 +35,8 @@ See [Plaintext requirements](https://github.com/TechEmpower/FrameworkBenchmarks/ ### Data Updates -**Fiber** handled **11,846** responses per second with an average latency of **42.8** ms. -**Express** handled **2,066** responses per second with an average latency of **390.44** ms. +**Fiber** handled **30,884** responses per second with an average latency of **16.5** ms. +**Express** handled **50,818** responses per second with an average latency of **10.1** ms. ![](/img/data_updates.png) @@ -43,8 +44,8 @@ See [Plaintext requirements](https://github.com/TechEmpower/FrameworkBenchmarks/ ### Multiple Queries -**Fiber** handled **19,664** responses per second with an average latency of **25.7** ms. -**Express** handled **4,302** responses per second with an average latency of **117.2** ms. +**Fiber** handled **55,577** responses per second with an average latency of **9.2** ms. +**Express** handled **62,036** responses per second with an average latency of **8.3** ms. ![](/img/multiple_queries.png) @@ -52,8 +53,8 @@ See [Plaintext requirements](https://github.com/TechEmpower/FrameworkBenchmarks/ ### Single Query -**Fiber** handled **368,647** responses per second with an average latency of **0.7** ms. -**Express** handled **57,880** responses per second with an average latency of **4.4** ms. +**Fiber** handled **1,000,519** responses per second with an average latency of **0.5** ms. +**Express** handled **214,177** responses per second with an average latency of **2.5** ms. ![](/img/single_query.png) @@ -61,52 +62,9 @@ See [Plaintext requirements](https://github.com/TechEmpower/FrameworkBenchmarks/ ### JSON Serialization -**Fiber** handled **1,146,667** responses per second with an average latency of **0.4** ms. -**Express** handled **244,847** responses per second with an average latency of **1.1** ms. +**Fiber** handled **2,479,768** responses per second with an average latency of **0.2** ms. +**Express** handled **301,213** responses per second with an average latency of **2.0** ms. ![](/img/json.png) ![Fiber vs Express](/img/json_express.png) - -## Go web framework benchmark - -🔗 [https://github.com/smallnest/go-web-framework-benchmark](https://github.com/smallnest/go-web-framework-benchmark) - -* **CPU** Intel\(R\) Xeon\(R\) Gold 6140 CPU @ 2.30GHz -* **MEM** 4GB -* **GO** go1.13.6 linux/amd64 -* **OS** Linux - -The first test case is to mock **0 ms**, **10 ms**, **100 ms**, **500 ms** processing time in handlers. - -![](/img/benchmark.png) - -The concurrency clients are **5000**. - -![](/img/benchmark_latency.png) - -Latency is the time of real processing time by web servers. _The smaller is the better._ - -![](/img/benchmark_alloc.png) - -Allocs is the heap allocations by web servers when test is running. The unit is MB. _The smaller is the better._ - -If we enable **http pipelining**, test result as below: - -![](/img/benchmark-pipeline.png) - -Concurrency test in **30 ms** processing time, the test result for **100**, **1000**, **5000** clients is: - -![](/img/concurrency.png) - -![](/img/concurrency_latency.png) - -![](/img/concurrency_alloc.png) - -If we enable **http pipelining**, test result as below: - -![](/img/concurrency-pipeline.png) - -Dependency graph for `v1.9.0` - -![](/img/graph.svg) From cf5041b9d714e10e1225c4e6e4b82c47c64b4f8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Sun, 13 Apr 2025 18:37:24 +0200 Subject: [PATCH 17/31] repair readme benchmarks --- .github/README.md | 4 ++-- .github/README_az.md | 4 ++-- .github/README_ckb.md | 4 ++-- .github/README_de.md | 4 ++-- .github/README_eg.md | 4 ++-- .github/README_es.md | 4 ++-- .github/README_fa.md | 4 ++-- .github/README_fr.md | 4 ++-- .github/README_he.md | 4 ++-- .github/README_id.md | 4 ++-- .github/README_it.md | 4 ++-- .github/README_ja.md | 4 ++-- .github/README_ko.md | 4 ++-- .github/README_nl.md | 4 ++-- .github/README_pl.md | 4 ++-- .github/README_pt.md | 4 ++-- .github/README_ru.md | 4 ++-- .github/README_sa.md | 4 ++-- .github/README_tr.md | 4 ++-- .github/README_uk.md | 4 ++-- .github/README_zh-CN.md | 4 ++-- .github/README_zh-TW.md | 4 ++-- 22 files changed, 44 insertions(+), 44 deletions(-) diff --git a/.github/README.md b/.github/README.md index 172ecc599f5..0b19126cf85 100644 --- a/.github/README.md +++ b/.github/README.md @@ -124,8 +124,8 @@ func main() { These tests are performed by [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) and [Go Web](https://github.com/smallnest/go-web-framework-benchmark). If you want to see all the results, please visit our [Wiki](https://docs.gofiber.io/extra/benchmarks).

- - + +

## ⚙️ Installation diff --git a/.github/README_az.md b/.github/README_az.md index f1b6174e988..e31d33037fb 100644 --- a/.github/README_az.md +++ b/.github/README_az.md @@ -123,8 +123,8 @@ func main() { Bu testlər [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) və [Go Web](https://github.com/smallnest/go-web-framework-benchmark) tərəfindən aparılıb. Bütün nəticələri görmək üçün [Wiki](https://docs.gofiber.io/extra/benchmarks) səhifəsinə keçid edə bilərsiniz.

- - + +

## ⚙️ Quraşdırılması diff --git a/.github/README_ckb.md b/.github/README_ckb.md index 5f52bf8aea5..872ad7062dd 100644 --- a/.github/README_ckb.md +++ b/.github/README_ckb.md @@ -120,8 +120,8 @@ func main() { ئەم تاقیکردنەوانە لەلایەن [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) و [Go Web](https://github.com/smallnest/go-web-framework-benchmark) ئەنجام دراون. دەتوانیت هەموو ئەنجامەکان [لێرە](https://docs.gofiber.io/extra/benchmarks) ببینیت.

- - + +

## ⚙️ دامەزراندن diff --git a/.github/README_de.md b/.github/README_de.md index 8fd89352630..49127e35512 100644 --- a/.github/README_de.md +++ b/.github/README_de.md @@ -122,8 +122,8 @@ func main() { Diese Tests wurden von [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) und [Go Web](https://github.com/smallnest/go-web-framework-benchmark) ausgeführt. Falls du alle Resultate sehen möchtest, besuche bitte unser [Wiki](https://docs.gofiber.io/extra/benchmarks).

- - + +

## ⚙️ Installation diff --git a/.github/README_eg.md b/.github/README_eg.md index 3eea1171ac3..0c139514036 100644 --- a/.github/README_eg.md +++ b/.github/README_eg.md @@ -124,8 +124,8 @@ func main() { القياسات دي اتعملت عن طريق [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) و [Go Web](https://github.com/smallnest/go-web-framework-benchmark). لو عاوز تشوف كل النتايج زور [الويكي بتاعتنا](https://docs.gofiber.io/extra/benchmarks).

- - + +

## ⚙️ التسطيب diff --git a/.github/README_es.md b/.github/README_es.md index b3e5b9f7bfb..2b429a6787b 100644 --- a/.github/README_es.md +++ b/.github/README_es.md @@ -122,8 +122,8 @@ func main() { Estas pruebas son realizadas por [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) y [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Si desea ver todos los resultados, visite nuestra [Wiki](https://docs.gofiber.io/extra/benchmarks).

- - + +

## ⚙️ Instalación diff --git a/.github/README_fa.md b/.github/README_fa.md index 390922d68be..79fc594386b 100644 --- a/.github/README_fa.md +++ b/.github/README_fa.md @@ -137,8 +137,8 @@ func main() {

- - + +


diff --git a/.github/README_fr.md b/.github/README_fr.md index 2f93e567ded..9f6ce58f99b 100644 --- a/.github/README_fr.md +++ b/.github/README_fr.md @@ -122,8 +122,8 @@ func main() { Ces tests sont effectués par [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) et [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Si vous voulez voir tous les résultats, n'hésitez pas à consulter notre [Wiki](https://docs.gofiber.io/extra/benchmarks).

- - + +

## ⚙️ Installation diff --git a/.github/README_he.md b/.github/README_he.md index 0ddda261df6..04dcb114dc7 100644 --- a/.github/README_he.md +++ b/.github/README_he.md @@ -139,8 +139,8 @@ func main() {

- - + +

diff --git a/.github/README_id.md b/.github/README_id.md index 658e86a522a..999de07d6e7 100644 --- a/.github/README_id.md +++ b/.github/README_id.md @@ -122,8 +122,8 @@ func main() { Pengukuran ini dilakukan oleh [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) dan [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Apabila anda ingin melihat hasil lengkapnya, silahkan kunjungi halaman [Wiki](https://docs.gofiber.io/extra/benchmarks) kami.

- - + +

## ⚙️ Instalasi diff --git a/.github/README_it.md b/.github/README_it.md index 159bcff9a18..eb2aefc259d 100644 --- a/.github/README_it.md +++ b/.github/README_it.md @@ -122,8 +122,8 @@ func main() { Questi test sono stati eseguiti da [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) e [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Se vuoi vedere tutti i risultati, visita la nostra [Wiki](https://docs.gofiber.io/extra/benchmarks).

- - + +

## ⚙️ Installazione diff --git a/.github/README_ja.md b/.github/README_ja.md index 23dfb60999a..f82dc762e13 100644 --- a/.github/README_ja.md +++ b/.github/README_ja.md @@ -123,8 +123,8 @@ func main() { これらのテストは[TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext)および[Go Web](https://github.com/smallnest/go-web-framework-benchmark)によって計測を行っています 。すべての結果を表示するには、 [Wiki](https://docs.gofiber.io/extra/benchmarks)にアクセスしてください。

- - + +

## ⚙️ インストール diff --git a/.github/README_ko.md b/.github/README_ko.md index c2ed6afb9b6..e7e75da53b3 100644 --- a/.github/README_ko.md +++ b/.github/README_ko.md @@ -122,8 +122,8 @@ func main() { 이 테스트들은 [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext)와 [Go Web](https://github.com/smallnest/go-web-framework-benchmark)을 통해 측정되었습니다. 만약 모든 결과를 보고 싶다면, [Wiki](https://docs.gofiber.io/extra/benchmarks)를 확인해 주세요.

- - + +

## ⚙️ 설치 diff --git a/.github/README_nl.md b/.github/README_nl.md index e31040a06db..1eced32ceaf 100644 --- a/.github/README_nl.md +++ b/.github/README_nl.md @@ -122,8 +122,8 @@ func main() { Deze tests zijn uitgevoerd door [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) en [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Bezoek onze [Wiki](https://fiber.wiki/benchmarks) voor alle benchmark resultaten.

- - + +

## ⚙️ Installatie diff --git a/.github/README_pl.md b/.github/README_pl.md index 5cff10c87e2..77b75c42bd2 100644 --- a/.github/README_pl.md +++ b/.github/README_pl.md @@ -125,8 +125,8 @@ Testy te zostały przeprowadzone przez [TechEmpower](https://www.techempower.com

- - + +

## ⚙️ Instalacja diff --git a/.github/README_pt.md b/.github/README_pt.md index e55f4f58b72..06c73614ec2 100644 --- a/.github/README_pt.md +++ b/.github/README_pt.md @@ -122,8 +122,8 @@ func main() { Esses testes são realizados pelo [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) e [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Se você quiser ver todos os resultados, visite nosso [Wiki](https://docs.gofiber.io/extra/benchmarks) .

- - + +

## ⚙️ Instalação diff --git a/.github/README_ru.md b/.github/README_ru.md index bfb41661d8a..f42bcc29091 100644 --- a/.github/README_ru.md +++ b/.github/README_ru.md @@ -122,8 +122,8 @@ func main() { Тестирование проводилось с помощью [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) и [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Если вы хотите увидеть все результаты, пожалуйста, посетите наш [Wiki](https://docs.gofiber.io/extra/benchmarks).

- - + +

## ⚙️ Установка diff --git a/.github/README_sa.md b/.github/README_sa.md index a5a54e9e470..e3f3f81850e 100644 --- a/.github/README_sa.md +++ b/.github/README_sa.md @@ -128,8 +128,8 @@ func main() { يتم تنفيذ هذه الاختبارات من قبل [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) و [Go Web](https://github.com/smallnest/go-web-framework-benchmark). إذا كنت تريد رؤية جميع النتائج ، يرجى زيارة موقعنا [Wiki](https://docs.gofiber.io/extra/benchmarks).

- - + +

## ⚙️ تثبيت diff --git a/.github/README_tr.md b/.github/README_tr.md index 3ab0f15a6d8..29365154386 100644 --- a/.github/README_tr.md +++ b/.github/README_tr.md @@ -122,8 +122,8 @@ func main() { Bu testler [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) ve [Go Web](https://github.com/smallnest/go-web-framework-benchmark) tarafından gerçekleştirildi. Bütün sonuçları görmek için lütfen [Wiki](https://docs.gofiber.io/extra/benchmarks) sayfasını ziyaret ediniz.

- - + +

## ⚙️ Kurulum diff --git a/.github/README_uk.md b/.github/README_uk.md index ef9f36536b4..e3d6b11eaa2 100644 --- a/.github/README_uk.md +++ b/.github/README_uk.md @@ -132,8 +132,8 @@ func main() { відвідайте наш [Wiki](https://docs.gofiber.io/extra/benchmarks).

- - + +

## ⚙️ Встановлення diff --git a/.github/README_zh-CN.md b/.github/README_zh-CN.md index 4314e0580f6..eb80f137d23 100644 --- a/.github/README_zh-CN.md +++ b/.github/README_zh-CN.md @@ -124,8 +124,8 @@ func main() { 这些测试由 [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) 和 [Go Web](https://github.com/smallnest/go-web-framework-benchmark) 完成。如果您想查看所有结果,请访问我们的 [Wiki](https://docs.gofiber.io/extra/benchmarks) 。

- - + +

## ⚙️ 安装 diff --git a/.github/README_zh-TW.md b/.github/README_zh-TW.md index 7bf431a99fd..37e6eea6eba 100644 --- a/.github/README_zh-TW.md +++ b/.github/README_zh-TW.md @@ -126,8 +126,8 @@ func main() { 這些測試由 [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) 和 [Go Web 框架效能測試](https://github.com/smallnest/go-web-framework-benchmark) 完成。若需參閱所有結果,請參閱我們的 [Wiki](https://docs.gofiber.io/extra/benchmarks) 資訊。

- - + +

## ⚙️ 安裝 From ac8b1c95c3af9424bafcaa3b7e94cacc25018c41 Mon Sep 17 00:00:00 2001 From: Isaac Andrade Date: Sat, 26 Apr 2025 11:16:09 -0600 Subject: [PATCH 18/31] =?UTF-8?q?=F0=9F=93=92=20docs:=20Update=20usage=20o?= =?UTF-8?q?f=20ctx.Redirect()=20(#3417)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📚 Improve docs for Ctx.Redirect Closes #3405 * Update ctx.md * Only run golangci-lint for go related changes --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> --- .github/workflows/linter.yml | 11 +++++++-- docs/api/ctx.md | 47 ++++++++++++++++++++++++------------ 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 607e0fa0554..6f98d77b49d 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -1,11 +1,18 @@ -# Adapted from https://github.com/golangci/golangci-lint-action/blob/b56f6f529003f1c81d4d759be6bd5f10bf9a0fa0/README.md#how-to-use - name: golangci-lint on: push: branches: - v2 + paths: + - "**" + - "!docs/**" + - "!**.md" pull_request: + paths: + - "**" + - "!docs/**" + - "!**.md" + permissions: contents: read diff --git a/docs/api/ctx.md b/docs/api/ctx.md index 40d03ca5ae5..dcde9dd96c0 100644 --- a/docs/api/ctx.md +++ b/docs/api/ctx.md @@ -57,7 +57,7 @@ Media-Type parameters are supported. app.Get("/", func(c *fiber.Ctx) error { // Extra parameters in the accept are ignored c.Accepts("text/plain;format=flowed") // "text/plain;format=flowed" - + // An offer must contain all parameters present in the Accept type c.Accepts("application/json") // "" @@ -239,7 +239,7 @@ app.Post("/", func(c *fiber.Ctx) error { }) ``` -> _Returned value is only valid within the handler. Do not store any references. +> _Returned value is only valid within the handler. Do not store any references. > Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) ## Body @@ -259,7 +259,7 @@ app.Post("/", func(c *fiber.Ctx) error { }) ``` -> _Returned value is only valid within the handler. Do not store any references. +> _Returned value is only valid within the handler. Do not store any references. > Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) ## BodyParser @@ -319,7 +319,7 @@ app.Post("/", func(c *fiber.Ctx) error { // curl -X POST "http://localhost:3000/?name=john&pass=doe" ``` -> _Returned value is only valid within the handler. Do not store any references. +> _Returned value is only valid within the handler. Do not store any references. > Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) ## ClearCookie @@ -489,7 +489,7 @@ app.Get("/", func(c *fiber.Ctx) error { }) ``` -> _Returned value is only valid within the handler. Do not store any references. +> _Returned value is only valid within the handler. Do not store any references. > Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) ## Download @@ -579,7 +579,7 @@ app.Post("/", func(c *fiber.Ctx) error { }) ``` -> _Returned value is only valid within the handler. Do not store any references. +> _Returned value is only valid within the handler. Do not store any references. > Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) ## Fresh @@ -615,7 +615,7 @@ app.Get("/", func(c *fiber.Ctx) error { }) ``` -> _Returned value is only valid within the handler. Do not store any references. +> _Returned value is only valid within the handler. Do not store any references. > Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) ## GetReqHeaders @@ -626,7 +626,7 @@ Returns the HTTP request headers as a map. Since a header can be set multiple ti func (c *Ctx) GetReqHeaders() map[string][]string ``` -> _Returned value is only valid within the handler. Do not store any references. +> _Returned value is only valid within the handler. Do not store any references. > Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) ## GetRespHeader @@ -650,7 +650,7 @@ app.Get("/", func(c *fiber.Ctx) error { }) ``` -> _Returned value is only valid within the handler. Do not store any references. +> _Returned value is only valid within the handler. Do not store any references. > Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) ## GetRespHeaders @@ -661,7 +661,7 @@ Returns the HTTP response headers as a map. Since a header can be set multiple t func (c *Ctx) GetRespHeaders() map[string][]string ``` -> _Returned value is only valid within the handler. Do not store any references. +> _Returned value is only valid within the handler. Do not store any references. > Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) ## GetRouteURL @@ -707,7 +707,7 @@ app.Get("/", func(c *fiber.Ctx) error { }) ``` -> _Returned value is only valid within the handler. Do not store any references. +> _Returned value is only valid within the handler. Do not store any references. > Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) ## IP @@ -954,7 +954,7 @@ app.Post("/", func(c *fiber.Ctx) error { ## Method -Returns a string corresponding to the HTTP method of the request: `GET`, `POST`, `PUT`, and so on. +Returns a string corresponding to the HTTP method of the request: `GET`, `POST`, `PUT`, and so on. Optionally, you could override the method by passing a string. ```go title="Signature" @@ -1054,7 +1054,7 @@ app.Get("/", func(c *fiber.Ctx) error { }) ``` -> _Returned value is only valid within the handler. Do not store any references. +> _Returned value is only valid within the handler. Do not store any references. > Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) ## Params @@ -1103,7 +1103,7 @@ app.Get("/v1/*/shop/*", func(c *fiber.Ctx) error { }) ``` -> _Returned value is only valid within the handler. Do not store any references. +> _Returned value is only valid within the handler. Do not store any references. > Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) ## ParamsInt @@ -1280,7 +1280,7 @@ app.Get("/", func(c *fiber.Ctx) error { }) ``` -> _Returned value is only valid within the handler. Do not store any references. +> _Returned value is only valid within the handler. Do not store any references. > Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) ## QueryBool @@ -1402,7 +1402,7 @@ app.Get("/", func(c *fiber.Ctx) error { log.Println(p.Products) // ["shoe,hat"] // fiber.Config{EnableSplittingOnParsers: true} // log.Println(p.Products) // ["shoe", "hat"] - + // ... }) @@ -1444,6 +1444,21 @@ Redirects to the URL derived from the specified path, with specified status, a p If **not** specified, status defaults to **302 Found**. ::: +:::caution +When the redirect status code is **302 Found**, web browsers may change the HTTP method +(for example, converting a POST into a GET) depending on the browser implementation. To ensure that the +original HTTP method is preserved, especially in scenarios where a non-GET method is required, you +should explicitly use one of the following status codes: + +- 303 See Other +- 307 Temporary Redirect +- 308 Permanent Redirect + +For example, to preserve the HTTP method, use: + +`c.Redirect("/new-path", fiber.StatusSeeOther)` +::: + ```go title="Signature" func (c *Ctx) Redirect(location string, status ...int) error ``` From f7a10c9a611913957b4db88a893c0d364f0f3ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Mon, 12 May 2025 11:02:15 +0200 Subject: [PATCH 19/31] fix golangci errors Error: G104: Errors unhandled. (gosec) Error: G104: Errors unhandled. (gosec) Error: G104: Errors unhandled. (gosec) --- .golangci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.golangci.yml b/.golangci.yml index c58d52511e3..bb14703d1fc 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -15,6 +15,11 @@ linters-settings: check-type-assertions: true check-blank: true disable-default-exclusions: true + exclude-functions: + - '(*bytes.Buffer).Write' # always returns nil error + - '(*github.com/valyala/bytebufferpool.ByteBuffer).Write' # always returns nil error + - '(*github.com/valyala/bytebufferpool.ByteBuffer).WriteByte' # always returns nil error + - '(*github.com/valyala/bytebufferpool.ByteBuffer).WriteString' # always returns nil error errchkjson: report-no-exported: true From fccff196064e95411c0cdaaee8d9f67e2627e33c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Mon, 12 May 2025 11:20:39 +0200 Subject: [PATCH 20/31] fix golangci errors Error: G104: Errors unhandled. (gosec) --- .golangci.yml | 2 ++ ctx.go | 35 +++++++++++++++--------------- ctx_test.go | 4 ++-- log/default.go | 10 ++++----- middleware/adaptor/adaptor_test.go | 2 +- middleware/etag/etag.go | 8 +++---- middleware/logger/logger.go | 4 ++-- middleware/session/session.go | 2 +- 8 files changed, 35 insertions(+), 32 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index bb14703d1fc..88919e8e935 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -44,6 +44,8 @@ linters-settings: extra-rules: true gosec: + excludes: + - G104 # TODO: Enable this again. Mostly provided by errcheck config: global: audit: true diff --git a/ctx.go b/ctx.go index b418dc8e906..97dfe9cf2b2 100644 --- a/ctx.go +++ b/ctx.go @@ -948,11 +948,11 @@ func (c *Ctx) Links(link ...string) { bb := bytebufferpool.Get() for i := range link { if i%2 == 0 { - _ = bb.WriteByte('<') //nolint:errcheck // This will never fail - _, _ = bb.WriteString(link[i]) //nolint:errcheck // This will never fail - _ = bb.WriteByte('>') //nolint:errcheck // This will never fail + _ = bb.WriteByte('<') + _, _ = bb.WriteString(link[i]) + _ = bb.WriteByte('>') } else { - _, _ = bb.WriteString(`; rel="` + link[i] + `",`) //nolint:errcheck // This will never fail + _, _ = bb.WriteString(`; rel="` + link[i] + `",`) } } c.setCanonical(HeaderLink, utils.TrimRight(c.app.getString(bb.Bytes()), ',')) @@ -1510,10 +1510,10 @@ func (c *Ctx) RedirectToRoute(routeName string, params Map, status ...int) error i := 1 for k, v := range queries { - _, _ = queryText.WriteString(k + "=" + v) //nolint:errcheck // This will never fail + _, _ = queryText.WriteString(k + "=" + v) if i != len(queries) { - _, _ = queryText.WriteString("&") //nolint:errcheck // This will never fail + _, _ = queryText.WriteString("&") } i++ } @@ -1838,26 +1838,27 @@ func (c *Ctx) String() string { buf := bytebufferpool.Get() // Start with the ID, converting it to a hex string without fmt.Sprintf - buf.WriteByte('#') //nolint:errcheck // Not needed here + buf.WriteByte('#') + // Convert ID to hexadecimal id := strconv.FormatUint(c.fasthttp.ID(), 16) // Pad with leading zeros to ensure 16 characters for i := 0; i < (16 - len(id)); i++ { - buf.WriteByte('0') //nolint:errcheck // Not needed here + buf.WriteByte('0') } - buf.WriteString(id) //nolint:errcheck // Not needed here - buf.WriteString(" - ") //nolint:errcheck // Not needed here + buf.WriteString(id) + buf.WriteString(" - ") // Add local and remote addresses directly - buf.WriteString(c.fasthttp.LocalAddr().String()) //nolint:errcheck // Not needed here - buf.WriteString(" <-> ") //nolint:errcheck // Not needed here - buf.WriteString(c.fasthttp.RemoteAddr().String()) //nolint:errcheck // Not needed here - buf.WriteString(" - ") //nolint:errcheck // Not needed here + buf.WriteString(c.fasthttp.LocalAddr().String()) + buf.WriteString(" <-> ") + buf.WriteString(c.fasthttp.RemoteAddr().String()) + buf.WriteString(" - ") // Add method and URI - buf.Write(c.fasthttp.Request.Header.Method()) //nolint:errcheck // Not needed here - buf.WriteByte(' ') //nolint:errcheck // Not needed here - buf.Write(c.fasthttp.URI().FullURI()) //nolint:errcheck // Not needed here + buf.Write(c.fasthttp.Request.Header.Method()) + buf.WriteByte(' ') + buf.Write(c.fasthttp.URI().FullURI()) // Allocate string str := buf.String() diff --git a/ctx_test.go b/ctx_test.go index 6a3998f7fa2..c1602abe9c9 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -3641,7 +3641,7 @@ func Test_Ctx_RenderWithBind(t *testing.T) { utils.AssertEqual(t, nil, err) buf := bytebufferpool.Get() - _, _ = buf.WriteString("overwrite") //nolint:errcheck // This will never fail + _, _ = buf.WriteString("overwrite") defer bytebufferpool.Put(buf) utils.AssertEqual(t, "

Hello, World!

", string(c.Response().Body())) @@ -3663,7 +3663,7 @@ func Test_Ctx_RenderWithOverwrittenBind(t *testing.T) { utils.AssertEqual(t, nil, err) buf := bytebufferpool.Get() - _, _ = buf.WriteString("overwrite") //nolint:errcheck // This will never fail + _, _ = buf.WriteString("overwrite") defer bytebufferpool.Put(buf) utils.AssertEqual(t, "

Hello from Fiber!

", string(c.Response().Body())) diff --git a/log/default.go b/log/default.go index c898cd6c83a..5cc7500afc7 100644 --- a/log/default.go +++ b/log/default.go @@ -27,8 +27,8 @@ func (l *defaultLogger) privateLog(lv Level, fmtArgs []interface{}) { } level := lv.toString() buf := bytebufferpool.Get() - _, _ = buf.WriteString(level) //nolint:errcheck // It is fine to ignore the error - _, _ = buf.WriteString(fmt.Sprint(fmtArgs...)) //nolint:errcheck // It is fine to ignore the error + _, _ = buf.WriteString(level) + _, _ = buf.WriteString(fmt.Sprint(fmtArgs...)) _ = l.stdlog.Output(l.depth, buf.String()) //nolint:errcheck // It is fine to ignore the error buf.Reset() @@ -46,7 +46,7 @@ func (l *defaultLogger) privateLogf(lv Level, format string, fmtArgs []interface } level := lv.toString() buf := bytebufferpool.Get() - _, _ = buf.WriteString(level) //nolint:errcheck // It is fine to ignore the error + _, _ = buf.WriteString(level) if len(fmtArgs) > 0 { _, _ = fmt.Fprintf(buf, format, fmtArgs...) @@ -69,11 +69,11 @@ func (l *defaultLogger) privateLogw(lv Level, format string, keysAndValues []int } level := lv.toString() buf := bytebufferpool.Get() - _, _ = buf.WriteString(level) //nolint:errcheck // It is fine to ignore the error + _, _ = buf.WriteString(level) // Write format privateLog buffer if format != "" { - _, _ = buf.WriteString(format) //nolint:errcheck // It is fine to ignore the error + _, _ = buf.WriteString(format) } var once sync.Once isFirst := true diff --git a/middleware/adaptor/adaptor_test.go b/middleware/adaptor/adaptor_test.go index dc52704760d..03fdbc1a00d 100644 --- a/middleware/adaptor/adaptor_test.go +++ b/middleware/adaptor/adaptor_test.go @@ -78,7 +78,7 @@ func Test_HTTPHandler(t *testing.T) { req.Header.SetMethod(expectedMethod) req.SetRequestURI(expectedRequestURI) req.Header.SetHost(expectedHost) - req.BodyWriter().Write([]byte(expectedBody)) //nolint:errcheck, gosec // not needed + req.BodyWriter().Write([]byte(expectedBody)) //nolint:errcheck // not needed for k, v := range expectedHeader { req.Header.Set(k, v) } diff --git a/middleware/etag/etag.go b/middleware/etag/etag.go index 13148fc63ba..7a37f84effb 100644 --- a/middleware/etag/etag.go +++ b/middleware/etag/etag.go @@ -54,14 +54,14 @@ func New(config ...Config) fiber.Handler { // Enable weak tag if cfg.Weak { - _, _ = bb.Write(weakPrefix) //nolint:errcheck // This will never fail + _, _ = bb.Write(weakPrefix) } - _ = bb.WriteByte('"') //nolint:errcheck // This will never fail + _ = bb.WriteByte('"') bb.B = appendUint(bb.Bytes(), uint32(len(body))) - _ = bb.WriteByte('-') //nolint:errcheck // This will never fail + _ = bb.WriteByte('-') bb.B = appendUint(bb.Bytes(), crc32.Checksum(body, crc32q)) - _ = bb.WriteByte('"') //nolint:errcheck // This will never fail + _ = bb.WriteByte('"') etag := bb.Bytes() diff --git a/middleware/logger/logger.go b/middleware/logger/logger.go index b03617b2b34..a41d33ef6c7 100644 --- a/middleware/logger/logger.go +++ b/middleware/logger/logger.go @@ -138,7 +138,7 @@ func New(config ...Config) fiber.Handler { // Loop over template parts execute dynamic parts and add fixed parts to the buffer for i, logFunc := range logFunChain { if logFunc == nil { - _, _ = buf.Write(templateChain[i]) //nolint:errcheck // This will never fail + _, _ = buf.Write(templateChain[i]) } else if templateChain[i] == nil { _, err = logFunc(buf, c, data, "") } else { @@ -151,7 +151,7 @@ func New(config ...Config) fiber.Handler { // Also write errors to the buffer if err != nil { - _, _ = buf.WriteString(err.Error()) //nolint:errcheck // This will never fail + _, _ = buf.WriteString(err.Error()) } mu.Lock() // Write buffer to output diff --git a/middleware/session/session.go b/middleware/session/session.go index b4115faf238..e1fa174f6d7 100644 --- a/middleware/session/session.go +++ b/middleware/session/session.go @@ -302,7 +302,7 @@ func (s *Session) delSession() { // decodeSessionData decodes the session data from raw bytes. func (s *Session) decodeSessionData(rawData []byte) error { - _, _ = s.byteBuffer.Write(rawData) //nolint:errcheck // This will never fail + _, _ = s.byteBuffer.Write(rawData) encCache := gob.NewDecoder(s.byteBuffer) if err := encCache.Decode(&s.data.Data); err != nil { return fmt.Errorf("failed to decode session data: %w", err) From 94e30d7124cbf75bc2055cc49ec1ca7b8d7690e3 Mon Sep 17 00:00:00 2001 From: RW Date: Fri, 16 May 2025 08:29:39 +0200 Subject: [PATCH 21/31] =?UTF-8?q?=F0=9F=90=9B=20Fix=20routing=20with=20mou?= =?UTF-8?q?nt=20and=20static=20(#3454)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix routing with mount and static [Bug]: Static server in sub app does not work #3104 https://github.com/gofiber/fiber/issues/3104 [Bug]: When mounting a subapp with mount, the static route is inaccessible. #3442 https://github.com/gofiber/fiber/issues/3442 --- app.go | 2 - mount.go | 8 ---- mount_test.go | 6 --- router.go | 100 ++++++++++++++++++++++++++++--------------------- router_test.go | 48 +++++++++++++++++++----- 5 files changed, 97 insertions(+), 67 deletions(-) diff --git a/app.go b/app.go index 751df9f232a..3c1cc3cc986 100644 --- a/app.go +++ b/app.go @@ -93,8 +93,6 @@ type App struct { treeStack []map[string][]*Route // contains the information if the route stack has been changed to build the optimized tree routesRefreshed bool - // Amount of registered routes - routesCount uint32 // Amount of registered handlers handlersCount uint32 // Ctx pool diff --git a/mount.go b/mount.go index abb5695e9fe..b26eecc64e8 100644 --- a/mount.go +++ b/mount.go @@ -174,7 +174,6 @@ func (app *App) processSubAppsRoutes() { } } var handlersCount uint32 - var routePos uint32 // Iterate over the stack of the parent app for m := range app.stack { // Iterate over each route in the stack @@ -183,9 +182,6 @@ func (app *App) processSubAppsRoutes() { route := app.stack[m][i] // Check if the route has a mounted app if !route.mount { - routePos++ - // If not, update the route's position and continue - route.pos = routePos if !route.use || (route.use && m == 0) { handlersCount += uint32(len(route.Handlers)) } @@ -214,11 +210,7 @@ func (app *App) processSubAppsRoutes() { copy(newStack[i+len(subRoutes):], app.stack[m][i+1:]) app.stack[m] = newStack - // Decrease the parent app's route count to account for the mounted app's original route - atomic.AddUint32(&app.routesCount, ^uint32(0)) i-- - // Increase the parent app's route count to account for the sub-app's routes - atomic.AddUint32(&app.routesCount, uint32(len(subRoutes))) // Mark the parent app's routes as refreshed app.routesRefreshed = true diff --git a/mount_test.go b/mount_test.go index c0ca6bf3c9f..63d4f1b3f21 100644 --- a/mount_test.go +++ b/mount_test.go @@ -89,7 +89,6 @@ func Test_App_Mount_Nested(t *testing.T) { utils.AssertEqual(t, 200, resp.StatusCode, "Status code") utils.AssertEqual(t, uint32(6), app.handlersCount) - utils.AssertEqual(t, uint32(6), app.routesCount) } // go test -run Test_App_Mount_Express_Behavior @@ -139,7 +138,6 @@ func Test_App_Mount_Express_Behavior(t *testing.T) { testEndpoint(app, "/unknown", ErrNotFound.Message, StatusNotFound) utils.AssertEqual(t, uint32(17), app.handlersCount) - utils.AssertEqual(t, uint32(16+9), app.routesCount) } // go test -run Test_App_Mount_RoutePositions @@ -195,19 +193,15 @@ func Test_App_Mount_RoutePositions(t *testing.T) { utils.AssertEqual(t, true, routeStackGET[1].use) utils.AssertEqual(t, "/", routeStackGET[1].path) - utils.AssertEqual(t, true, routeStackGET[0].pos < routeStackGET[1].pos, "wrong position of route 0") utils.AssertEqual(t, false, routeStackGET[2].use) utils.AssertEqual(t, "/bar", routeStackGET[2].path) - utils.AssertEqual(t, true, routeStackGET[1].pos < routeStackGET[2].pos, "wrong position of route 1") utils.AssertEqual(t, true, routeStackGET[3].use) utils.AssertEqual(t, "/", routeStackGET[3].path) - utils.AssertEqual(t, true, routeStackGET[2].pos < routeStackGET[3].pos, "wrong position of route 2") utils.AssertEqual(t, false, routeStackGET[4].use) utils.AssertEqual(t, "/subapp2/world", routeStackGET[4].path) - utils.AssertEqual(t, true, routeStackGET[3].pos < routeStackGET[4].pos, "wrong position of route 3") utils.AssertEqual(t, 5, len(routeStackGET)) } diff --git a/router.go b/router.go index 4afa7415377..61ddc1bf17d 100644 --- a/router.go +++ b/router.go @@ -7,7 +7,6 @@ package fiber import ( "fmt" "html" - "sort" "strconv" "strings" "sync/atomic" @@ -47,9 +46,8 @@ type Router interface { // Route is a struct that holds all metadata for each registered handler. type Route struct { - // ### important: always keep in sync with the copy method "app.copyRoute" ### + // ### important: always keep in sync with the copy method "app.copyRoute" and all creations of Route struct ### // Data for routing - pos uint32 // Position in stack -> important for the sort of the matched routes use bool // USE matches path prefixes mount bool // Indicated a mounted app on a specific route star bool // Path equals '*' @@ -215,9 +213,6 @@ func (*App) copyRoute(route *Route) *Route { path: route.path, routeParser: route.routeParser, - // misc - pos: route.pos, - // Public data Path: route.Path, Params: route.Params, @@ -298,11 +293,11 @@ func (app *App) register(method, pathRaw string, group *Group, handlers ...Handl for _, m := range app.config.RequestMethods { // Create a route copy to avoid duplicates during compression r := route - app.addRoute(m, &r, isMount) + app.addRoute(m, &r) } } else { // Add route to stack - app.addRoute(method, &route, isMount) + app.addRoute(method, &route) } } @@ -428,12 +423,20 @@ func (app *App) registerStatic(prefix, root string, config ...Static) { // Create route metadata without pointer route := Route{ // Router booleans - use: true, - root: isRoot, + use: true, + mount: false, + star: isStar, + root: isRoot, + + // Path data path: prefix, + + // Group data + group: nil, + // Public data - Method: MethodGet, Path: prefix, + Method: MethodGet, Handlers: []Handler{handler}, } // Increment global handler count @@ -444,13 +447,7 @@ func (app *App) registerStatic(prefix, root string, config ...Static) { app.addRoute(MethodHead, &route) } -func (app *App) addRoute(method string, route *Route, isMounted ...bool) { - // Check mounted routes - var mounted bool - if len(isMounted) > 0 { - mounted = isMounted[0] - } - +func (app *App) addRoute(method string, route *Route) { // Get unique HTTP method identifier m := app.methodInt(method) @@ -460,8 +457,6 @@ func (app *App) addRoute(method string, route *Route, isMounted ...bool) { preRoute := app.stack[m][l-1] preRoute.Handlers = append(preRoute.Handlers, route.Handlers...) } else { - // Increment global route position - route.pos = atomic.AddUint32(&app.routesCount, 1) route.Method = method // Add route to the stack app.stack[m] = append(app.stack[m], route) @@ -469,7 +464,7 @@ func (app *App) addRoute(method string, route *Route, isMounted ...bool) { } // Execute onRoute hooks & change latestRoute if not adding mounted route - if !mounted { + if !route.mount { app.mutex.Lock() app.latestRoute = route if err := app.hooks.executeOnRouteHooks(*route); err != nil { @@ -481,38 +476,59 @@ func (app *App) addRoute(method string, route *Route, isMounted ...bool) { // buildTree build the prefix tree from the previously registered routes func (app *App) buildTree() *App { + // If routes haven't been refreshed, nothing to do if !app.routesRefreshed { return app } - // loop all the methods and stacks and create the prefix tree - for m := range app.config.RequestMethods { - tsMap := make(map[string][]*Route) - for _, route := range app.stack[m] { - treePath := "" + // 1) First loop: determine all possible 3-char prefixes ("treePaths") for each method + for method := range app.config.RequestMethods { + prefixSet := map[string]struct{}{ + "": {}, + } + for _, route := range app.stack[method] { if len(route.routeParser.segs) > 0 && len(route.routeParser.segs[0].Const) >= 3 { - treePath = route.routeParser.segs[0].Const[:3] + prefix := route.routeParser.segs[0].Const[:3] + prefixSet[prefix] = struct{}{} } - // create tree stack - tsMap[treePath] = append(tsMap[treePath], route) } - app.treeStack[m] = tsMap - } + tsMap := make(map[string][]*Route, len(prefixSet)) + for prefix := range prefixSet { + tsMap[prefix] = nil + } + app.treeStack[method] = tsMap + } + + // 2) Second loop: for each method and each discovered treePath, assign matching routes + for method := range app.config.RequestMethods { + // get the map of buckets for this method + tsMap := app.treeStack[method] + + // for every treePath key (including the empty one) + for treePath := range tsMap { + // iterate all routes of this method + for _, route := range app.stack[method] { + // compute this route's own prefix ("" or first 3 chars) + routePath := "" + if len(route.routeParser.segs) > 0 && len(route.routeParser.segs[0].Const) >= 3 { + routePath = route.routeParser.segs[0].Const[:3] + } - // loop the methods and tree stacks and add global stack and sort everything - for m := range app.config.RequestMethods { - tsMap := app.treeStack[m] - for treePart := range tsMap { - if treePart != "" { - // merge global tree routes in current tree stack - tsMap[treePart] = uniqueRouteStack(append(tsMap[treePart], tsMap[""]...)) + // if it's a global route, assign to every bucket + if routePath == "" { + tsMap[treePath] = append(tsMap[treePath], route) + // otherwise only assign if this route's prefix matches the current bucket's key + } else if routePath == treePath { + tsMap[treePath] = append(tsMap[treePath], route) + } } - // sort tree slices with the positions - slc := tsMap[treePart] - sort.Slice(slc, func(i, j int) bool { return slc[i].pos < slc[j].pos }) + + // after collecting, dedupe the bucket if it's not the global one + tsMap[treePath] = uniqueRouteStack(tsMap[treePath]) } } - app.routesRefreshed = false + // reset the flag and return + app.routesRefreshed = false return app } diff --git a/router_test.go b/router_test.go index 8d1e40cbd0b..2c4424f80d5 100644 --- a/router_test.go +++ b/router_test.go @@ -20,7 +20,10 @@ import ( "github.com/valyala/fasthttp" ) -var routesFixture routeJSON +var ( + routesFixture routeJSON + cssDir = "./.github/testdata/fs/css" +) func init() { dat, err := os.ReadFile("./.github/testdata/testRoutes.json") @@ -354,9 +357,8 @@ func Test_Router_Handler_Catch_Error(t *testing.T) { func Test_Route_Static_Root(t *testing.T) { t.Parallel() - dir := "./.github/testdata/fs/css" app := New() - app.Static("/", dir, Static{ + app.Static("/", cssDir, Static{ Browse: true, }) @@ -373,7 +375,7 @@ func Test_Route_Static_Root(t *testing.T) { utils.AssertEqual(t, true, strings.Contains(app.getString(body), "color")) app = New() - app.Static("/", dir) + app.Static("/", cssDir) resp, err = app.Test(httptest.NewRequest(MethodGet, "/", nil)) utils.AssertEqual(t, nil, err, "app.Test(req)") @@ -391,9 +393,8 @@ func Test_Route_Static_Root(t *testing.T) { func Test_Route_Static_HasPrefix(t *testing.T) { t.Parallel() - dir := "./.github/testdata/fs/css" app := New() - app.Static("/static", dir, Static{ + app.Static("/static", cssDir, Static{ Browse: true, }) @@ -414,7 +415,7 @@ func Test_Route_Static_HasPrefix(t *testing.T) { utils.AssertEqual(t, true, strings.Contains(app.getString(body), "color")) app = New() - app.Static("/static/", dir, Static{ + app.Static("/static/", cssDir, Static{ Browse: true, }) @@ -435,7 +436,7 @@ func Test_Route_Static_HasPrefix(t *testing.T) { utils.AssertEqual(t, true, strings.Contains(app.getString(body), "color")) app = New() - app.Static("/static", dir) + app.Static("/static", cssDir) resp, err = app.Test(httptest.NewRequest(MethodGet, "/static", nil)) utils.AssertEqual(t, nil, err, "app.Test(req)") @@ -454,7 +455,7 @@ func Test_Route_Static_HasPrefix(t *testing.T) { utils.AssertEqual(t, true, strings.Contains(app.getString(body), "color")) app = New() - app.Static("/static/", dir) + app.Static("/static/", cssDir) resp, err = app.Test(httptest.NewRequest(MethodGet, "/static", nil)) utils.AssertEqual(t, nil, err, "app.Test(req)") @@ -474,6 +475,8 @@ func Test_Route_Static_HasPrefix(t *testing.T) { } func Test_Router_NotFound(t *testing.T) { + t.Parallel() + app := New() app.Use(func(c *Ctx) error { return c.Next() @@ -491,6 +494,8 @@ func Test_Router_NotFound(t *testing.T) { } func Test_Router_NotFound_HTML_Inject(t *testing.T) { + t.Parallel() + app := New() app.Use(func(c *Ctx) error { return c.Next() @@ -507,6 +512,31 @@ func Test_Router_NotFound_HTML_Inject(t *testing.T) { utils.AssertEqual(t, "Cannot DELETE /does/not/exist<script>alert('foo');</script>", string(c.Response.Body())) } +func Test_Router_Mount_n_Static(t *testing.T) { + t.Parallel() + + app := New() + + app.Static("/static", cssDir, Static{Browse: true}) + app.Get("/", func(c *Ctx) error { + return c.SendString("Home") + }) + + subApp := New() + app.Mount("/mount", subApp) + subApp.Get("/test", func(c *Ctx) error { + return c.SendString("Hello from /test") + }) + + app.Use(func(c *Ctx) error { + return c.Status(StatusNotFound).SendString("Not Found") + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/static/style.css", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") +} + ////////////////////////////////////////////// ///////////////// BENCHMARKS ///////////////// ////////////////////////////////////////////// From 7db10b6976b70f3fa780cffc9417923265bdafd2 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Mon, 19 May 2025 05:31:20 -0400 Subject: [PATCH 22/31] docs: Add AGENTS.md (#3461) Create AGENTS.md --- AGENTS.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000000..241f7beb7b4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,21 @@ +# AGENTS.md + +## Startup script + +- Fetch dependencies: + + ```bash + go get ./... + ``` + +- Install CLI tools referenced in Makefile: + + ```bash + go install gotest.tools/gotestsum@latest # test runner + go install golang.org/x/vuln/cmd/govulncheck@latest # vulnerability scanner + go install mvdan.cc/gofumpt@latest # code formatter + go install github.com/tinylib/msgp@latest # msgp codegen + go install github.com/vburenin/ifacemaker@975a95966976eeb2d4365a7fb236e274c54da64c # interface impls + go install github.com/dkorunic/betteralign/cmd/betteralign@latest # struct alignment + go mod tidy # clean up go.mod & go.sum + ``` From d15355116c37102bd9a8bcc252e3e3e399671af7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Tue, 20 May 2025 07:29:08 +0200 Subject: [PATCH 23/31] Update AGENTS.md --- AGENTS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 241f7beb7b4..580bf1fa527 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,11 @@ # AGENTS.md +## Agent Instructions + +> **Note for the AI agent:** Before running the startup script, check for a `vendor` directory. If it exists, skip fetching modules; otherwise proceed. This vendor-check logic is for the agent’s decision-making and is not part of the user-facing script. + +--- + ## Startup script - Fetch dependencies: From e115c08b8f059a4a031b492aa9eef0712411853d Mon Sep 17 00:00:00 2001 From: RW Date: Thu, 22 May 2025 08:45:40 +0200 Subject: [PATCH 24/31] Merge commit from fork * BodyParser: slice/array invalid range - add test case * BodyParser: slice/array invalid range --- ctx_test.go | 25 +++++++++++++++++++++++++ internal/schema/decoder.go | 15 ++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/ctx_test.go b/ctx_test.go index c1602abe9c9..6042fcff075 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -654,6 +654,31 @@ func Test_Ctx_BodyParser(t *testing.T) { }) } +func Test_Ctx_BodyParser_InvalidRequestData(t *testing.T) { + t.Parallel() + + type RequestBody struct { + NestedContent []*struct { + Value string `form:"value"` + } `form:"nested-content"` + } + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + c.Request().Reset() + c.Request().Header.SetContentType(MIMEApplicationForm) + // Test with invalid form data + c.Request().SetBody([]byte("nested-content[-1].value=Foo&nested-content[0].value=Bar&nested-content[1].value=FooBar")) + c.Request().Header.SetContentLength(len(c.Body())) + + subject := new(RequestBody) + err := c.BodyParser(subject) + + utils.AssertEqual(t, true, nil != err) + utils.AssertEqual(t, "failed to decode: schema: panic while decoding: reflect: slice index out of range", fmt.Sprintf("%v", err)) +} + func Test_Ctx_ParamParser(t *testing.T) { t.Parallel() app := New() diff --git a/internal/schema/decoder.go b/internal/schema/decoder.go index b63c45e1dcd..1f5d208e9d0 100644 --- a/internal/schema/decoder.go +++ b/internal/schema/decoder.go @@ -67,11 +67,24 @@ func (d *Decoder) RegisterConverter(value interface{}, converterFunc Converter) // Keys are "paths" in dotted notation to the struct fields and nested structs. // // See the package documentation for a full explanation of the mechanics. -func (d *Decoder) Decode(dst interface{}, src map[string][]string) error { +func (d *Decoder) Decode(dst interface{}, src map[string][]string) (err error) { v := reflect.ValueOf(dst) if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { return errors.New("schema: interface must be a pointer to struct") } + + // Catch panics from the decoder and return them as an error. + // This is needed because the decoder calls reflect and reflect panics + defer func() { + if r := recover(); r != nil { + if e, ok := r.(error); ok { + err = e + } else { + err = fmt.Errorf("schema: panic while decoding: %v", r) + } + } + }() + v = v.Elem() t := v.Type() multiError := MultiError{} From 22c3c0ced919f73309018b7a7d27b029bad3e4b4 Mon Sep 17 00:00:00 2001 From: RW Date: Thu, 22 May 2025 09:00:28 +0200 Subject: [PATCH 25/31] Update app.go --- app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.go b/app.go index 3c1cc3cc986..a11850e5fa9 100644 --- a/app.go +++ b/app.go @@ -30,7 +30,7 @@ import ( ) // Version of current fiber package -const Version = "2.52.6" +const Version = "2.52.8" // Handler defines a function to serve HTTP requests. type Handler = func(*Ctx) error From b6f077275f12d25e0a4aecf59d01d77be8005ee8 Mon Sep 17 00:00:00 2001 From: RW Date: Mon, 26 May 2025 19:48:06 +0200 Subject: [PATCH 26/31] Update AGENTS.md --- AGENTS.md | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 580bf1fa527..4b23d8d8704 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,16 +2,16 @@ ## Agent Instructions -> **Note for the AI agent:** Before running the startup script, check for a `vendor` directory. If it exists, skip fetching modules; otherwise proceed. This vendor-check logic is for the agent’s decision-making and is not part of the user-facing script. +> **Note for the AI agent:** Before running the startup script, verify that all dependencies and CLI tools are already available. If a `vendor/` directory exists **or** `go list -m -mod=readonly all` succeeds (indicating modules are cached), you may skip the entire startup script—including all `go mod` commands and `go install` steps. Otherwise run the script as listed below. --- -## Startup script +## Startup script (reference only – do not run) - Fetch dependencies: ```bash - go get ./... + go mod tidy && go mod download && go mod vendor ``` - Install CLI tools referenced in Makefile: @@ -25,3 +25,20 @@ go install github.com/dkorunic/betteralign/cmd/betteralign@latest # struct alignment go mod tidy # clean up go.mod & go.sum ``` + +## Makefile commands + +Use `make help` to list all available commands. Common targets include: + +- **audit**: run `go mod verify`, `go vet`, and `govulncheck` for quality checks. +- **benchmark**: run benchmarks with `go test`. +- **coverage**: generate a coverage report. +- **format**: apply formatting using `gofumpt`. +- **lint**: execute `golangci-lint`. +- **test**: run the test suite with `gotestsum`. +- **longtest**: run the test suite 15 times with shuffling enabled. +- **tidy**: clean and tidy dependencies. +- **betteralign**: optimize struct field alignment. +- **generate**: run `go generate` after installing msgp and ifacemaker. + +These targets can be invoked via `make ` as needed during development and testing. From 40d14a9c717a3db1222ba480b9a26cddd6cd231b Mon Sep 17 00:00:00 2001 From: RW Date: Mon, 26 May 2025 20:22:44 +0200 Subject: [PATCH 27/31] =?UTF-8?q?=F0=9F=90=9B=20fix:=20Embedded=20struct?= =?UTF-8?q?=20parsing=20(#3478)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix "[Bug]: Incorrect Parsing of Slice by QueryParser() with Embedded Structs #2859" * Fix "[Bug]: Incorrect Parsing of Slice by QueryParser() with Embedded Structs #2859" --- ctx.go | 41 +++++++++++++++++++++-------------------- ctx_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/ctx.go b/ctx.go index 97dfe9cf2b2..06145c5c46d 100644 --- a/ctx.go +++ b/ctx.go @@ -1326,43 +1326,44 @@ func (*Ctx) parseToStruct(aliasTag string, out interface{}, data map[string][]st } func equalFieldType(out interface{}, kind reflect.Kind, key, tag string) bool { - // Get type of interface outTyp := reflect.TypeOf(out).Elem() - key = utils.ToLower(key) - // Must be a struct to match a field if outTyp.Kind() != reflect.Struct { return false } - // Copy interface to an value to be used - outVal := reflect.ValueOf(out).Elem() - // Loop over each field + key = utils.ToLower(key) + return checkEqualFieldType(outTyp, kind, key, tag) +} + +func checkEqualFieldType(outTyp reflect.Type, kind reflect.Kind, key, tag string) bool { for i := 0; i < outTyp.NumField(); i++ { - // Get field value data - structField := outVal.Field(i) - // Can this field be changed? - if !structField.CanSet() { + typeField := outTyp.Field(i) + + if typeField.Anonymous && typeField.Type.Kind() == reflect.Struct { + if checkEqualFieldType(typeField.Type, kind, key, tag) { + return true + } + } + + if typeField.PkgPath != "" { // unexported field continue } - // Get field key data - typeField := outTyp.Field(i) - // Get type of field key - structFieldKind := structField.Kind() - // Does the field type equals input? - if structFieldKind != kind { + + if typeField.Type.Kind() != kind { continue } - // Get tag from field if exist + inputFieldName := typeField.Tag.Get(tag) if inputFieldName == "" { inputFieldName = typeField.Name - } else { - inputFieldName = strings.Split(inputFieldName, ",")[0] + } else if idx := strings.IndexByte(inputFieldName, ','); idx > -1 { + inputFieldName = inputFieldName[:idx] } - // Compare field/tag with provided key + if utils.ToLower(inputFieldName) == key { return true } } + return false } diff --git a/ctx_test.go b/ctx_test.go index 6042fcff075..53369a3dc89 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -4875,6 +4875,28 @@ func Test_Ctx_QueryParser_Schema(t *testing.T) { utils.AssertEqual(t, 12, cq.Data[1].Age) } +func Test_Ctx_QueryParser_EmbeddedStruct(t *testing.T) { + t.Parallel() + app := New(Config{EnableSplittingOnParsers: true}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + type A struct { + Products []string `query:"products"` + } + + type Person struct { + Name string `query:"name"` + Pass string `query:"pass"` + A + } + + c.Request().URI().SetQueryString("name=john&pass=doe&products=shoe,hat") + p := new(Person) + utils.AssertEqual(t, nil, c.QueryParser(p)) + utils.AssertEqual(t, []string{"shoe", "hat"}, p.Products) +} + // go test -run Test_Ctx_ReqHeaderParser -v func Test_Ctx_ReqHeaderParser(t *testing.T) { t.Parallel() @@ -5402,6 +5424,25 @@ func Benchmark_Ctx_ReqHeaderParser(b *testing.B) { utils.AssertEqual(b, nil, c.ReqHeaderParser(q)) } +// go test -v -run=^$ -bench=Benchmark_equalFieldType -benchmem -count=4 +func Benchmark_equalFieldType(b *testing.B) { + type user struct { + Name string `query:"name"` + Age int `query:"age"` + } + + u := new(user) + b.ReportAllocs() + b.ResetTimer() + + var res bool + for n := 0; n < b.N; n++ { + res = equalFieldType(u, reflect.String, "name", queryTag) + } + + utils.AssertEqual(b, true, res) +} + // go test -run Test_Ctx_BodyStreamWriter func Test_Ctx_BodyStreamWriter(t *testing.T) { t.Parallel() From 1c037c4900cf87dd110a69e4f0b542f64ecb84b3 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Tue, 10 Jun 2025 02:45:49 -0400 Subject: [PATCH 28/31] =?UTF-8?q?=F0=9F=A7=B9=20chore:=20Add=20upper=20ind?= =?UTF-8?q?ex=20limit=20for=20parsers=20(#3503)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add error for index overflow * Update ctx_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update ctx_test.go * Update ctx_test.go * Update ctx_test.go * Fix proxy tests and update parser index limit * Fix proxy tests and enforce parser index limit * test: fix index limit and proxy tests * Fix proxy test helpers * Update proxy_test.go * fix index overflow error handling --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ctx_test.go | 24 +++++++ internal/schema/cache.go | 10 ++- internal/schema/decoder.go | 8 ++- middleware/proxy/proxy_test.go | 122 +++++++++++++++++++++++---------- 4 files changed, 125 insertions(+), 39 deletions(-) diff --git a/ctx_test.go b/ctx_test.go index 53369a3dc89..980f89e694e 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -679,6 +679,30 @@ func Test_Ctx_BodyParser_InvalidRequestData(t *testing.T) { utils.AssertEqual(t, "failed to decode: schema: panic while decoding: reflect: slice index out of range", fmt.Sprintf("%v", err)) } +func Test_Ctx_BodyParser_IndexTooLarge(t *testing.T) { + defer SetParserDecoder(ParserConfig{IgnoreUnknownKeys: true}) + SetParserDecoder(ParserConfig{IgnoreUnknownKeys: false}) + type RequestBody struct { + NestedContent []*struct { + Value string `form:"value"` + } `form:"nested-content"` + } + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + c.Request().Reset() + c.Request().Header.SetContentType(MIMEApplicationForm) + c.Request().SetBody([]byte("nested-content.1001.value=Foo")) + c.Request().Header.SetContentLength(len(c.Body())) + + subject := new(RequestBody) + err := c.BodyParser(subject) + + utils.AssertEqual(t, true, nil != err) + utils.AssertEqual(t, "failed to decode: schema: index exceeds parser limit", fmt.Sprintf("%v", err)) +} + func Test_Ctx_ParamParser(t *testing.T) { t.Parallel() app := New() diff --git a/internal/schema/cache.go b/internal/schema/cache.go index bf21697cf19..9cfd503fbbc 100644 --- a/internal/schema/cache.go +++ b/internal/schema/cache.go @@ -12,7 +12,12 @@ import ( "sync" ) -var errInvalidPath = errors.New("schema: invalid path") +const maxParserIndex = 1000 + +var ( + errInvalidPath = errors.New("schema: invalid path") + errIndexTooLarge = errors.New("schema: index exceeds parser limit") +) // newCache returns a new cache. func newCache() *cache { @@ -77,6 +82,9 @@ func (c *cache) parsePath(p string, t reflect.Type) ([]pathPart, error) { if index64, err = strconv.ParseInt(keys[i], 10, 0); err != nil { return nil, errInvalidPath } + if index64 > maxParserIndex { + return nil, errIndexTooLarge + } parts = append(parts, pathPart{ path: path, field: field, diff --git a/internal/schema/decoder.go b/internal/schema/decoder.go index 1f5d208e9d0..586e4d70598 100644 --- a/internal/schema/decoder.go +++ b/internal/schema/decoder.go @@ -93,8 +93,12 @@ func (d *Decoder) Decode(dst interface{}, src map[string][]string) (err error) { if err = d.decode(v, path, parts, values); err != nil { multiError[path] = err } - } else if !d.ignoreUnknownKeys { - multiError[path] = UnknownKeyError{Key: path} + } else { + if errors.Is(err, errIndexTooLarge) { + multiError[path] = err + } else if !d.ignoreUnknownKeys { + multiError[path] = UnknownKeyError{Key: path} + } } } multiError.merge(d.checkRequired(t, src)) diff --git a/middleware/proxy/proxy_test.go b/middleware/proxy/proxy_test.go index 49be2a2c958..13c71e58581 100644 --- a/middleware/proxy/proxy_test.go +++ b/middleware/proxy/proxy_test.go @@ -17,25 +17,60 @@ import ( "github.com/valyala/fasthttp" ) -func createProxyTestServer(t *testing.T, handler fiber.Handler) (*fiber.App, string) { +func startServer(t *testing.T, app *fiber.App, ln net.Listener) { t.Helper() + go func() { + utils.AssertEqual(t, nil, app.Listener(ln)) + }() + time.Sleep(200 * time.Millisecond) +} - target := fiber.New(fiber.Config{DisableStartupMessage: true}) +func createProxyTestServer(t *testing.T, handler fiber.Handler, network, address string) (*fiber.App, string) { + t.Helper() + + target := fiber.New() target.Get("/", handler) - ln, err := net.Listen(fiber.NetworkTCP4, "127.0.0.1:0") + ln, err := net.Listen(network, address) utils.AssertEqual(t, nil, err) - go func() { - utils.AssertEqual(t, nil, target.Listener(ln)) - }() - - time.Sleep(2 * time.Second) addr := ln.Addr().String() + startServer(t, target, ln) + return target, addr } +func createProxyTestServerIPv4(t *testing.T, handler fiber.Handler) (*fiber.App, string) { + t.Helper() + return createProxyTestServer(t, handler, fiber.NetworkTCP4, "127.0.0.1:0") +} + +// createRedirectServer creates a simple redirecting server used in tests. +// +//nolint:unparam // this is only for testing +func createRedirectServer(t *testing.T) (*fiber.App, string) { + t.Helper() + app := fiber.New() + + var addr string + app.Get("/", func(c *fiber.Ctx) error { + c.Location("http://" + addr + "/final") + return c.Status(fiber.StatusMovedPermanently).SendString("redirect") + }) + app.Get("/final", func(c *fiber.Ctx) error { + return c.SendString("final") + }) + + ln, err := net.Listen(fiber.NetworkTCP4, "127.0.0.1:0") + utils.AssertEqual(t, nil, err) + addr = ln.Addr().String() + + startServer(t, app, ln) + + return app, addr +} + // go test -run Test_Proxy_Empty_Host func Test_Proxy_Empty_Upstream_Servers(t *testing.T) { t.Parallel() @@ -83,7 +118,7 @@ func Test_Proxy_Next(t *testing.T) { func Test_Proxy(t *testing.T) { t.Parallel() - target, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + target, addr := createProxyTestServerIPv4(t, func(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusTeapot) }) @@ -142,7 +177,7 @@ func Test_Proxy_Balancer_WithTlsConfig(t *testing.T) { func Test_Proxy_Forward_WithTlsConfig_To_Http(t *testing.T) { t.Parallel() - _, targetAddr := createProxyTestServer(t, func(c *fiber.Ctx) error { + _, targetAddr := createProxyTestServerIPv4(t, func(c *fiber.Ctx) error { return c.SendString("hello from target") }) @@ -178,7 +213,7 @@ func Test_Proxy_Forward(t *testing.T) { app := fiber.New() - _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + _, addr := createProxyTestServerIPv4(t, func(c *fiber.Ctx) error { return c.SendString("forwarded") }) @@ -231,7 +266,7 @@ func Test_Proxy_Forward_WithTlsConfig(t *testing.T) { func Test_Proxy_Modify_Response(t *testing.T) { t.Parallel() - _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + _, addr := createProxyTestServerIPv4(t, func(c *fiber.Ctx) error { return c.Status(500).SendString("not modified") }) @@ -257,7 +292,7 @@ func Test_Proxy_Modify_Response(t *testing.T) { func Test_Proxy_Modify_Request(t *testing.T) { t.Parallel() - _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + _, addr := createProxyTestServerIPv4(t, func(c *fiber.Ctx) error { b := c.Request().Body() return c.SendString(string(b)) }) @@ -284,7 +319,7 @@ func Test_Proxy_Modify_Request(t *testing.T) { func Test_Proxy_Timeout_Slow_Server(t *testing.T) { t.Parallel() - _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + _, addr := createProxyTestServerIPv4(t, func(c *fiber.Ctx) error { time.Sleep(2 * time.Second) return c.SendString("fiber is awesome") }) @@ -308,7 +343,7 @@ func Test_Proxy_Timeout_Slow_Server(t *testing.T) { func Test_Proxy_With_Timeout(t *testing.T) { t.Parallel() - _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + _, addr := createProxyTestServerIPv4(t, func(c *fiber.Ctx) error { time.Sleep(1 * time.Second) return c.SendString("fiber is awesome") }) @@ -332,7 +367,7 @@ func Test_Proxy_With_Timeout(t *testing.T) { func Test_Proxy_Buffer_Size_Response(t *testing.T) { t.Parallel() - _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + _, addr := createProxyTestServerIPv4(t, func(c *fiber.Ctx) error { long := strings.Join(make([]string, 5000), "-") c.Set("Very-Long-Header", long) return c.SendString("ok") @@ -359,7 +394,7 @@ func Test_Proxy_Buffer_Size_Response(t *testing.T) { // go test -race -run Test_Proxy_Do_RestoreOriginalURL func Test_Proxy_Do_RestoreOriginalURL(t *testing.T) { t.Parallel() - _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + _, addr := createProxyTestServerIPv4(t, func(c *fiber.Ctx) error { return c.SendString("proxied") }) @@ -379,48 +414,60 @@ func Test_Proxy_Do_RestoreOriginalURL(t *testing.T) { // go test -race -run Test_Proxy_Do_WithRealURL func Test_Proxy_Do_WithRealURL(t *testing.T) { t.Parallel() + + _, addr := createProxyTestServerIPv4(t, func(c *fiber.Ctx) error { + return c.SendString("real url") + }) + app := fiber.New() app.Get("/test", func(c *fiber.Ctx) error { - return Do(c, "https://www.google.com") + return Do(c, "http://"+addr) }) - resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) + resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), 2000) utils.AssertEqual(t, nil, err1) utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) utils.AssertEqual(t, "/test", resp.Request.URL.String()) body, err := io.ReadAll(resp.Body) utils.AssertEqual(t, nil, err) - utils.AssertEqual(t, true, strings.Contains(string(body), "https://www.google.com/")) + utils.AssertEqual(t, "real url", string(body)) } // go test -race -run Test_Proxy_Do_WithRedirect func Test_Proxy_Do_WithRedirect(t *testing.T) { t.Parallel() + + _, addr := createRedirectServer(t) + app := fiber.New() app.Get("/test", func(c *fiber.Ctx) error { - return Do(c, "https://google.com") + return Do(c, "http://"+addr) }) - resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) + resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), 2000) utils.AssertEqual(t, nil, err1) body, err := io.ReadAll(resp.Body) utils.AssertEqual(t, nil, err) - utils.AssertEqual(t, true, strings.Contains(string(body), "https://www.google.com/")) - utils.AssertEqual(t, 301, resp.StatusCode) + utils.AssertEqual(t, "redirect", string(body)) + utils.AssertEqual(t, fiber.StatusMovedPermanently, resp.StatusCode) } // go test -race -run Test_Proxy_DoRedirects_RestoreOriginalURL func Test_Proxy_DoRedirects_RestoreOriginalURL(t *testing.T) { t.Parallel() + + _, addr := createRedirectServer(t) + app := fiber.New() app.Get("/test", func(c *fiber.Ctx) error { - return DoRedirects(c, "http://google.com", 1) + return DoRedirects(c, "http://"+addr, 1) }) - resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) + resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), 2000) utils.AssertEqual(t, nil, err1) - _, err := io.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "final", string(body)) utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) utils.AssertEqual(t, "/test", resp.Request.URL.String()) } @@ -428,12 +475,15 @@ func Test_Proxy_DoRedirects_RestoreOriginalURL(t *testing.T) { // go test -race -run Test_Proxy_DoRedirects_TooManyRedirects func Test_Proxy_DoRedirects_TooManyRedirects(t *testing.T) { t.Parallel() + + _, addr := createRedirectServer(t) + app := fiber.New() app.Get("/test", func(c *fiber.Ctx) error { - return DoRedirects(c, "http://google.com", 0) + return DoRedirects(c, "http://"+addr, 0) }) - resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) + resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), 2000) utils.AssertEqual(t, nil, err1) body, err := io.ReadAll(resp.Body) utils.AssertEqual(t, nil, err) @@ -446,7 +496,7 @@ func Test_Proxy_DoRedirects_TooManyRedirects(t *testing.T) { func Test_Proxy_DoTimeout_RestoreOriginalURL(t *testing.T) { t.Parallel() - _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + _, addr := createProxyTestServerIPv4(t, func(c *fiber.Ctx) error { return c.SendString("proxied") }) @@ -468,7 +518,7 @@ func Test_Proxy_DoTimeout_RestoreOriginalURL(t *testing.T) { func Test_Proxy_DoTimeout_Timeout(t *testing.T) { t.Parallel() - _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + _, addr := createProxyTestServerIPv4(t, func(c *fiber.Ctx) error { time.Sleep(time.Second * 5) return c.SendString("proxied") }) @@ -486,7 +536,7 @@ func Test_Proxy_DoTimeout_Timeout(t *testing.T) { func Test_Proxy_DoDeadline_RestoreOriginalURL(t *testing.T) { t.Parallel() - _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + _, addr := createProxyTestServerIPv4(t, func(c *fiber.Ctx) error { return c.SendString("proxied") }) @@ -508,7 +558,7 @@ func Test_Proxy_DoDeadline_RestoreOriginalURL(t *testing.T) { func Test_Proxy_DoDeadline_PastDeadline(t *testing.T) { t.Parallel() - _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + _, addr := createProxyTestServerIPv4(t, func(c *fiber.Ctx) error { time.Sleep(time.Second * 5) return c.SendString("proxied") }) @@ -526,7 +576,7 @@ func Test_Proxy_DoDeadline_PastDeadline(t *testing.T) { func Test_Proxy_Do_HTTP_Prefix_URL(t *testing.T) { t.Parallel() - _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + _, addr := createProxyTestServerIPv4(t, func(c *fiber.Ctx) error { return c.SendString("hello world") }) @@ -603,7 +653,7 @@ func Test_Proxy_Forward_Local_Client(t *testing.T) { func Test_ProxyBalancer_Custom_Client(t *testing.T) { t.Parallel() - target, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + target, addr := createProxyTestServerIPv4(t, func(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusTeapot) }) @@ -672,7 +722,7 @@ func Test_Proxy_Balancer_Forward_Local(t *testing.T) { app := fiber.New() - _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + _, addr := createProxyTestServerIPv4(t, func(c *fiber.Ctx) error { return c.SendString("forwarded") }) From 845f95f441718b1be1cf228f879f0a761118f317 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Mon, 23 Jun 2025 02:04:26 -0400 Subject: [PATCH 29/31] =?UTF-8?q?=F0=9F=90=9B=20bug:=20Fix=20Content-Type?= =?UTF-8?q?=20comparison=20in=20Is()=20(#3537)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: improve Is content type matching * Update ctx.go --------- Co-authored-by: RW --- ctx.go | 10 ++++++---- ctx_test.go | 10 ++++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/ctx.go b/ctx.go index 06145c5c46d..06ab0ae1d38 100644 --- a/ctx.go +++ b/ctx.go @@ -877,10 +877,12 @@ func (c *Ctx) Is(extension string) bool { return false } - return strings.HasPrefix( - utils.TrimLeft(c.app.getString(c.fasthttp.Request.Header.ContentType()), ' '), - extensionHeader, - ) + ct := c.app.getString(c.fasthttp.Request.Header.ContentType()) + if i := strings.IndexByte(ct, ';'); i != -1 { + ct = ct[:i] + } + ct = utils.Trim(ct, ' ') + return utils.EqualFold(ct, extensionHeader) } // JSON converts any interface or string to JSON. diff --git a/ctx_test.go b/ctx_test.go index 980f89e694e..af08c4eeaf9 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -2091,6 +2091,16 @@ func Test_Ctx_Is(t *testing.T) { utils.AssertEqual(t, false, c.Is("html")) utils.AssertEqual(t, true, c.Is("txt")) utils.AssertEqual(t, true, c.Is(".txt")) + + // case-insensitive and trimmed + c.Request().Header.Set(HeaderContentType, "APPLICATION/JSON; charset=utf-8") + utils.AssertEqual(t, true, c.Is("json")) + utils.AssertEqual(t, true, c.Is(".json")) + + // mismatched subtype should not match + c.Request().Header.Set(HeaderContentType, "application/json+xml") + utils.AssertEqual(t, false, c.Is("json")) + utils.AssertEqual(t, false, c.Is(".json")) } // go test -v -run=^$ -bench=Benchmark_Ctx_Is -benchmem -count=4 From b60408c9bde7e71faac0519aa680a6fb8d64a255 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Sun, 20 Jul 2025 08:54:11 -0400 Subject: [PATCH 30/31] =?UTF-8?q?=F0=9F=90=9B=20bug:=20Fix=20MIME=20type?= =?UTF-8?q?=20equality=20checks=20(#3603)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ctx.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ctx.go b/ctx.go index 06ab0ae1d38..c42e6e6668b 100644 --- a/ctx.go +++ b/ctx.go @@ -394,7 +394,7 @@ func (c *Ctx) BodyParser(out interface{}) error { if strings.HasSuffix(ctype, "json") { return c.app.config.JSONDecoder(c.Body(), out) } - if strings.HasPrefix(ctype, MIMEApplicationForm) { + if ctype == MIMEApplicationForm { data := make(map[string][]string) var err error @@ -415,7 +415,7 @@ func (c *Ctx) BodyParser(out interface{}) error { return c.parseToStruct(bodyTag, out, data) } - if strings.HasPrefix(ctype, MIMEMultipartForm) { + if ctype == MIMEMultipartForm { multipartForm, err := c.fasthttp.MultipartForm() if err != nil { return err @@ -431,7 +431,7 @@ func (c *Ctx) BodyParser(out interface{}) error { return c.parseToStruct(bodyTag, out, data) } - if strings.HasPrefix(ctype, MIMETextXML) || strings.HasPrefix(ctype, MIMEApplicationXML) { + if ctype == MIMETextXML || ctype == MIMEApplicationXML { if err := xml.Unmarshal(c.Body(), out); err != nil { return fmt.Errorf("failed to unmarshal: %w", err) } From 1197a22735820680ccfa241914b925f3820fcfd4 Mon Sep 17 00:00:00 2001 From: RW Date: Sun, 20 Jul 2025 15:43:40 +0200 Subject: [PATCH 31/31] Update app.go prepare release v2.52.9 --- app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.go b/app.go index a11850e5fa9..589632ceacc 100644 --- a/app.go +++ b/app.go @@ -30,7 +30,7 @@ import ( ) // Version of current fiber package -const Version = "2.52.8" +const Version = "2.52.9" // Handler defines a function to serve HTTP requests. type Handler = func(*Ctx) error