From 47e9c8fa6d3f236733d282857cdb1e4a457e7e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Thu, 27 Feb 2025 08:45:47 +0100 Subject: [PATCH 01/26] only run benchmark when golang files are changed --- .github/workflows/benchmark.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index a086223d..d91f1494 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -3,11 +3,11 @@ on: branches: - master - main - paths-ignore: - - "**/*.md" + paths: + - "**.go" pull_request: - paths-ignore: - - "**/*.md" + paths: + - "**.go" permissions: # deployments permission to deploy GitHub pages website From b568915b70c43a7ed02f5909ee72f1151ecd79e5 Mon Sep 17 00:00:00 2001 From: Giovanni Rivera Date: Wed, 26 Feb 2025 23:46:41 -0800 Subject: [PATCH 02/26] =?UTF-8?q?=F0=9F=93=9A=20docs:=20Add=20Retry=20Addo?= =?UTF-8?q?n=20documentation=20(#3330)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📚 Doc: Add addon tab to /docs * 📚 Doc: Add retry to addon docs * 📚 Doc: Update retry README.md * 🎨 Styles: Update addon/retry docs to respect markdownlint-cli2 * 📚 Doc: Update addon tab description to be singular * 📚 Doc: Use retry prefix in retry docs * 📚 Doc: Add retry addon to whats_new.md * 🎨 Styles: Update whats_new.md to respect markdownlint-cli2 --- addon/retry/README.md | 47 ++++++++++---- docs/addon/_category_.json | 9 +++ docs/addon/retry.md | 126 ++++++++++++++++++++++++++++++++++++ docs/client/_category_.json | 2 +- docs/extra/_category_.json | 2 +- docs/guide/_category_.json | 2 +- docs/whats_new.md | 54 ++++++++++++++++ 7 files changed, 228 insertions(+), 14 deletions(-) create mode 100644 docs/addon/_category_.json create mode 100644 docs/addon/retry.md diff --git a/addon/retry/README.md b/addon/retry/README.md index a3af4407..e6e1c34b 100644 --- a/addon/retry/README.md +++ b/addon/retry/README.md @@ -19,17 +19,47 @@ a jitter is a way to break synchronization across the client and avoid collision ## Signatures ```go -func NewExponentialBackoff(config ...Config) *ExponentialBackoff +func NewExponentialBackoff(config ...retry.Config) *retry.ExponentialBackoff ``` ## Examples -Firstly, import the addon from Fiber, - ```go +package main + import ( + "fmt" + "github.com/gofiber/fiber/v3/addon/retry" + "github.com/gofiber/fiber/v3/client" ) + +func main() { + expBackoff := retry.NewExponentialBackoff(retry.Config{}) + + // Local variables that will be used inside of Retry + var resp *client.Response + var err error + + // Retry a network request and return an error to signify to try again + err = expBackoff.Retry(func() error { + client := client.New() + resp, err = client.Get("https://gofiber.io") + if err != nil { + return fmt.Errorf("GET gofiber.io failed: %w", err) + } + if resp.StatusCode() != 200 { + return fmt.Errorf("GET gofiber.io did not return OK 200") + } + return nil + }) + + // If all retries failed, panic + if err != nil { + panic(err) + } + fmt.Printf("GET gofiber.io succeeded with status code %d\n", resp.StatusCode()) +} ``` ## Default Config @@ -58,28 +88,23 @@ type Config struct { // // Optional. Default: 1 * time.Second InitialInterval time.Duration - + // MaxBackoffTime defines maximum time duration for backoff algorithm. When // the algorithm is reached this time, rest of the retries will be maximum // 32 seconds. // // Optional. Default: 32 * time.Second MaxBackoffTime time.Duration - + // Multiplier defines multiplier number of the backoff algorithm. // // Optional. Default: 2.0 Multiplier float64 - + // MaxRetryCount defines maximum retry count for the backoff algorithm. // // Optional. Default: 10 MaxRetryCount int - - // currentInterval tracks the current waiting time. - // - // Optional. Default: 1 * time.Second - currentInterval time.Duration } ``` diff --git a/docs/addon/_category_.json b/docs/addon/_category_.json new file mode 100644 index 00000000..0d3a6ea3 --- /dev/null +++ b/docs/addon/_category_.json @@ -0,0 +1,9 @@ +{ + "label": "\uD83D\uDD0C Addon", + "position": 5, + "collapsed": true, + "link": { + "type": "generated-index", + "description": "Addon is an additional useful package that can be used in Fiber." + } +} diff --git a/docs/addon/retry.md b/docs/addon/retry.md new file mode 100644 index 00000000..2ed6a9f5 --- /dev/null +++ b/docs/addon/retry.md @@ -0,0 +1,126 @@ +--- +id: retry +--- + +# Retry Addon + +Retry addon for [Fiber](https://github.com/gofiber/fiber) designed to apply retry mechanism for unsuccessful network +operations. This addon uses an exponential backoff algorithm with jitter. It calls the function multiple times and tries +to make it successful. If all calls are failed, then, it returns an error. It adds a jitter at each retry step because adding +a jitter is a way to break synchronization across the client and avoid collision. + +## Table of Contents + +- [Retry Addon](#retry-addon) +- [Table of Contents](#table-of-contents) +- [Signatures](#signatures) +- [Examples](#examples) +- [Default Config](#default-config) +- [Custom Config](#custom-config) +- [Config](#config) +- [Default Config Example](#default-config-example) + +## Signatures + +```go +func NewExponentialBackoff(config ...retry.Config) *retry.ExponentialBackoff +``` + +## Examples + +```go +package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v3/addon/retry" + "github.com/gofiber/fiber/v3/client" +) + +func main() { + expBackoff := retry.NewExponentialBackoff(retry.Config{}) + + // Local variables that will be used inside of Retry + var resp *client.Response + var err error + + // Retry a network request and return an error to signify to try again + err = expBackoff.Retry(func() error { + client := client.New() + resp, err = client.Get("https://gofiber.io") + if err != nil { + return fmt.Errorf("GET gofiber.io failed: %w", err) + } + if resp.StatusCode() != 200 { + return fmt.Errorf("GET gofiber.io did not return OK 200") + } + return nil + }) + + // If all retries failed, panic + if err != nil { + panic(err) + } + fmt.Printf("GET gofiber.io succeeded with status code %d\n", resp.StatusCode()) +} +``` + +## Default Config + +```go +retry.NewExponentialBackoff() +``` + +## Custom Config + +```go +retry.NewExponentialBackoff(retry.Config{ + InitialInterval: 2 * time.Second, + MaxBackoffTime: 64 * time.Second, + Multiplier: 2.0, + MaxRetryCount: 15, +}) +``` + +## Config + +```go +// Config defines the config for addon. +type Config struct { + // InitialInterval defines the initial time interval for backoff algorithm. + // + // Optional. Default: 1 * time.Second + InitialInterval time.Duration + + // MaxBackoffTime defines maximum time duration for backoff algorithm. When + // the algorithm is reached this time, rest of the retries will be maximum + // 32 seconds. + // + // Optional. Default: 32 * time.Second + MaxBackoffTime time.Duration + + // Multiplier defines multiplier number of the backoff algorithm. + // + // Optional. Default: 2.0 + Multiplier float64 + + // MaxRetryCount defines maximum retry count for the backoff algorithm. + // + // Optional. Default: 10 + MaxRetryCount int +} +``` + +## Default Config Example + +```go +// DefaultConfig is the default config for retry. +var DefaultConfig = Config{ + InitialInterval: 1 * time.Second, + MaxBackoffTime: 32 * time.Second, + Multiplier: 2.0, + MaxRetryCount: 10, + currentInterval: 1 * time.Second, +} +``` diff --git a/docs/client/_category_.json b/docs/client/_category_.json index 61fad7ac..f5a63aa2 100644 --- a/docs/client/_category_.json +++ b/docs/client/_category_.json @@ -1,6 +1,6 @@ { "label": "\uD83C\uDF0E Client", - "position": 5, + "position": 6, "link": { "type": "generated-index", "description": "HTTP client for Fiber." diff --git a/docs/extra/_category_.json b/docs/extra/_category_.json index 3398bed5..55d68c94 100644 --- a/docs/extra/_category_.json +++ b/docs/extra/_category_.json @@ -1,6 +1,6 @@ { "label": "\uD83E\uDDE9 Extra", - "position": 6, + "position": 8, "link": { "type": "generated-index", "description": "Extra contents for Fiber." diff --git a/docs/guide/_category_.json b/docs/guide/_category_.json index 62226d5d..fd203a8b 100644 --- a/docs/guide/_category_.json +++ b/docs/guide/_category_.json @@ -1,6 +1,6 @@ { "label": "\uD83D\uDCD6 Guide", - "position": 5, + "position": 7, "link": { "type": "generated-index", "description": "Guides for Fiber." diff --git a/docs/whats_new.md b/docs/whats_new.md index 4185a7e3..5c3dd6ac 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -33,6 +33,7 @@ Here's a quick overview of the changes in Fiber `v3`: - [Filesystem](#filesystem) - [Monitor](#monitor) - [Healthcheck](#healthcheck) +- [🔌 Addons](#-addons) - [📋 Migration guide](#-migration-guide) ## Drop for old Go versions @@ -939,6 +940,59 @@ The Healthcheck middleware has been enhanced to support more than two routes, wi Refer to the [healthcheck middleware migration guide](./middleware/healthcheck.md) or the [general migration guide](#-migration-guide) to review the changes. +## 🔌 Addons + +In v3, Fiber introduced Addons. Addons are additional useful packages that can be used in Fiber. + +### Retry + +The Retry addon is a new addon that implements a retry mechanism for unsuccessful network operations. It uses an exponential backoff algorithm with jitter. +It calls the function multiple times and tries to make it successful. If all calls are failed, then, it returns an error. +It adds a jitter at each retry step because adding a jitter is a way to break synchronization across the client and avoid collision. + +
+Example + +```go +package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v3/addon/retry" + "github.com/gofiber/fiber/v3/client" +) + +func main() { + expBackoff := retry.NewExponentialBackoff(retry.Config{}) + + // Local variables that will be used inside of Retry + var resp *client.Response + var err error + + // Retry a network request and return an error to signify to try again + err = expBackoff.Retry(func() error { + client := client.New() + resp, err = client.Get("https://gofiber.io") + if err != nil { + return fmt.Errorf("GET gofiber.io failed: %w", err) + } + if resp.StatusCode() != 200 { + return fmt.Errorf("GET gofiber.io did not return OK 200") + } + return nil + }) + + // If all retries failed, panic + if err != nil { + panic(err) + } + fmt.Printf("GET gofiber.io succeeded with status code %d\n", resp.StatusCode()) +} +``` + +
+ ## 📋 Migration guide - [🚀 App](#-app-1) From 8e395fd4e33a92cefee7119d5941065cde9d5c36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 12:36:56 +0000 Subject: [PATCH 03/26] build(deps): bump codecov/codecov-action from 5.3.1 to 5.4.0 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.3.1 to 5.4.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v5.3.1...v5.4.0) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d5ddb215..914f20d0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,7 +32,7 @@ jobs: - name: Upload coverage reports to Codecov if: ${{ matrix.platform == 'ubuntu-latest' && matrix.go-version == '1.23.x' }} - uses: codecov/codecov-action@v5.3.1 + uses: codecov/codecov-action@v5.4.0 with: token: ${{ secrets.CODECOV_TOKEN }} file: ./coverage.txt From 6afba957f13913b69447701ce3658f20a1e36be6 Mon Sep 17 00:00:00 2001 From: Kashiwa <13825170+ksw2000@users.noreply.github.com> Date: Sun, 2 Mar 2025 01:14:50 +0800 Subject: [PATCH 04/26] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20sorting=20error=20i?= =?UTF-8?q?n=20sortAcceptedTypes=20(#3331)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🩹 Fix: correct sorting error in sortAcceptedTypes. * ♻️ Refactor: remove redundant branch --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> --- helpers.go | 11 +++-------- helpers_test.go | 8 ++++---- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/helpers.go b/helpers.go index 18728e56..a452e13c 100644 --- a/helpers.go +++ b/helpers.go @@ -483,7 +483,7 @@ func getOffer(header []byte, isAccepted func(spec, offer string, specParams head if len(acceptedTypes) > 1 { // Sort accepted types by quality and specificity, preserving order of equal elements - sortAcceptedTypes(&acceptedTypes) + sortAcceptedTypes(acceptedTypes) } // Find the first offer that matches the accepted types @@ -511,19 +511,14 @@ func getOffer(header []byte, isAccepted func(spec, offer string, specParams head // A type with parameters has higher priority than an equivalent one without parameters. // e.g., text/html;a=1;b=2 comes before text/html;a=1 // See: https://www.rfc-editor.org/rfc/rfc9110#name-content-negotiation-fields -func sortAcceptedTypes(acceptedTypes *[]acceptedType) { - if acceptedTypes == nil || len(*acceptedTypes) < 2 { - return - } - at := *acceptedTypes - +func sortAcceptedTypes(at []acceptedType) { for i := 1; i < len(at); i++ { lo, hi := 0, i-1 for lo <= hi { mid := (lo + hi) / 2 if at[i].quality < at[mid].quality || (at[i].quality == at[mid].quality && at[i].specificity < at[mid].specificity) || - (at[i].quality == at[mid].quality && at[i].specificity < at[mid].specificity && len(at[i].params) < len(at[mid].params)) || + (at[i].quality == at[mid].quality && at[i].specificity == at[mid].specificity && len(at[i].params) < len(at[mid].params)) || (at[i].quality == at[mid].quality && at[i].specificity == at[mid].specificity && len(at[i].params) == len(at[mid].params) && at[i].order > at[mid].order) { lo = mid + 1 } else { diff --git a/helpers_test.go b/helpers_test.go index 5664bc48..5e56bade 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -354,7 +354,6 @@ func Test_Utils_SortAcceptedTypes(t *testing.T) { {spec: "text/html", quality: 1, specificity: 3, order: 0}, {spec: "text/*", quality: 0.5, specificity: 2, order: 1}, {spec: "*/*", quality: 0.1, specificity: 1, order: 2}, - {spec: "application/json", quality: 0.999, specificity: 3, order: 3}, {spec: "application/xml", quality: 1, specificity: 3, order: 4}, {spec: "application/pdf", quality: 1, specificity: 3, order: 5}, {spec: "image/png", quality: 1, specificity: 3, order: 6}, @@ -363,8 +362,9 @@ func Test_Utils_SortAcceptedTypes(t *testing.T) { {spec: "image/gif", quality: 1, specificity: 3, order: 9}, {spec: "text/plain", quality: 1, specificity: 3, order: 10}, {spec: "application/json", quality: 0.999, specificity: 3, params: headerParams{"a": []byte("1")}, order: 11}, + {spec: "application/json", quality: 0.999, specificity: 3, order: 3}, } - sortAcceptedTypes(&acceptedTypes) + sortAcceptedTypes(acceptedTypes) require.Equal(t, []acceptedType{ {spec: "text/html", quality: 1, specificity: 3, order: 0}, {spec: "application/xml", quality: 1, specificity: 3, order: 4}, @@ -390,7 +390,7 @@ func Benchmark_Utils_SortAcceptedTypes_Sorted(b *testing.B) { acceptedTypes[0] = acceptedType{spec: "text/html", quality: 1, specificity: 1, order: 0} acceptedTypes[1] = acceptedType{spec: "text/*", quality: 0.5, specificity: 1, order: 1} acceptedTypes[2] = acceptedType{spec: "*/*", quality: 0.1, specificity: 1, order: 2} - sortAcceptedTypes(&acceptedTypes) + sortAcceptedTypes(acceptedTypes) } require.Equal(b, "text/html", acceptedTypes[0].spec) require.Equal(b, "text/*", acceptedTypes[1].spec) @@ -414,7 +414,7 @@ func Benchmark_Utils_SortAcceptedTypes_Unsorted(b *testing.B) { acceptedTypes[8] = acceptedType{spec: "image/*", quality: 1, specificity: 2, order: 8} acceptedTypes[9] = acceptedType{spec: "image/gif", quality: 1, specificity: 3, order: 9} acceptedTypes[10] = acceptedType{spec: "text/plain", quality: 1, specificity: 3, order: 10} - sortAcceptedTypes(&acceptedTypes) + sortAcceptedTypes(acceptedTypes) } require.Equal(b, []acceptedType{ {spec: "text/html", quality: 1, specificity: 3, order: 0}, From 9e6f4fd408aa1c93dff654392614995a5c73748f Mon Sep 17 00:00:00 2001 From: Kashiwa <13825170+ksw2000@users.noreply.github.com> Date: Mon, 3 Mar 2025 15:31:20 +0800 Subject: [PATCH 05/26] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20reduce?= =?UTF-8?q?=20the=20memory=20usage=20of=20RoutePatternMatch=20(#3335)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ Refactor: improve RoutePatternMatch by adding RemoveEscapeCharBytes ``` goos: linux goarch: amd64 pkg: github.com/gofiber/fiber/v3 cpu: AMD EPYC 7763 64-Core Processor │ route_pattern_match_old.txt │ route_pattern_match_new.txt │ │ sec/op │ sec/op vs base │ _RoutePatternMatch//api/v1/const_|_match_|_/api/v1/const-4 263.4n ± 2% 249.0n ± 4% -5.47% (p=0.001 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1-4 258.7n ± 4% 244.7n ± 2% -5.43% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/-4 254.6n ± 4% 246.3n ± 2% -3.26% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/something-4 265.1n ± 4% 255.6n ± 3% -3.60% (p=0.001 n=10) _RoutePatternMatch//api/:param/fixedEnd_|_match_|_/api/abc/fixedEnd-4 775.9n ± 3% 775.6n ± 2% ~ (p=0.424 n=10) _RoutePatternMatch//api/:param/fixedEnd_|_not_match_|_/api/abc/def/fixedEnd-4 796.7n ± 3% 767.1n ± 2% -3.72% (p=0.001 n=10) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity-4 916.2n ± 1% 904.8n ± 3% ~ (p=0.052 n=10) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/-4 913.8n ± 4% 909.1n ± 3% ~ (p=0.393 n=10) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/1-4 915.0n ± 3% 907.2n ± 2% ~ (p=0.165 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v-4 917.5n ± 2% 876.7n ± 2% -4.46% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v2-4 918.5n ± 2% 886.8n ± 2% -3.45% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v1/-4 935.6n ± 2% 901.9n ± 2% -3.60% (p=0.000 n=10) geomean 588.3n 570.7n -2.99% │ route_pattern_match_old.txt │ route_pattern_match_new.txt │ │ B/op │ B/op vs base │ _RoutePatternMatch//api/v1/const_|_match_|_/api/v1/const-4 168.0 ± 0% 152.0 ± 0% -9.52% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1-4 160.0 ± 0% 144.0 ± 0% -10.00% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/-4 160.0 ± 0% 144.0 ± 0% -10.00% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/something-4 176.0 ± 0% 160.0 ± 0% -9.09% (p=0.000 n=10) _RoutePatternMatch//api/:param/fixedEnd_|_match_|_/api/abc/fixedEnd-4 440.0 ± 0% 440.0 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/:param/fixedEnd_|_not_match_|_/api/abc/def/fixedEnd-4 464.0 ± 0% 440.0 ± 0% -5.17% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity-4 536.0 ± 0% 536.0 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/-4 536.0 ± 0% 536.0 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/1-4 536.0 ± 0% 536.0 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v-4 544.0 ± 0% 528.0 ± 0% -2.94% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v2-4 544.0 ± 0% 528.0 ± 0% -2.94% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v1/-4 544.0 ± 0% 528.0 ± 0% -2.94% (p=0.000 n=10) geomean 353.7 337.9 -4.47% ¹ all samples are equal │ route_pattern_match_old.txt │ route_pattern_match_new.txt │ │ allocs/op │ allocs/op vs base │ _RoutePatternMatch//api/v1/const_|_match_|_/api/v1/const-4 6.000 ± 0% 5.000 ± 0% -16.67% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1-4 6.000 ± 0% 5.000 ± 0% -16.67% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/-4 6.000 ± 0% 5.000 ± 0% -16.67% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/something-4 6.000 ± 0% 5.000 ± 0% -16.67% (p=0.000 n=10) _RoutePatternMatch//api/:param/fixedEnd_|_match_|_/api/abc/fixedEnd-4 13.00 ± 0% 13.00 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/:param/fixedEnd_|_not_match_|_/api/abc/def/fixedEnd-4 14.00 ± 0% 13.00 ± 0% -7.14% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity-4 14.00 ± 0% 14.00 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/-4 14.00 ± 0% 14.00 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/1-4 14.00 ± 0% 14.00 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v-4 15.00 ± 0% 14.00 ± 0% -6.67% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v2-4 15.00 ± 0% 14.00 ± 0% -6.67% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v1/-4 15.00 ± 0% 14.00 ± 0% -6.67% (p=0.000 n=10) geomean 10.67 9.811 -8.08% ¹ all samples are equal ``` * ♻️ Refactor: returned type of analyseParameterPart and analyseConstantPart ``` goos: linux goarch: amd64 pkg: github.com/gofiber/fiber/v3 cpu: AMD EPYC 7763 64-Core Processor │ route_pattern_match_old.txt │ route_pattern_match_new3.txt │ │ sec/op │ sec/op vs base │ _RoutePatternMatch//api/v1/const_|_match_|_/api/v1/const-4 264.3n ± 2% 253.8n ± 2% -3.95% (p=0.001 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1-4 258.5n ± 1% 247.6n ± 2% -4.24% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/-4 260.8n ± 3% 249.7n ± 4% -4.26% (p=0.003 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/something-4 265.4n ± 2% 256.1n ± 2% -3.49% (p=0.000 n=10) _RoutePatternMatch//api/:param/fixedEnd_|_match_|_/api/abc/fixedEnd-4 783.8n ± 2% 777.5n ± 3% ~ (p=0.218 n=10) _RoutePatternMatch//api/:param/fixedEnd_|_not_match_|_/api/abc/def/fixedEnd-4 797.8n ± 1% 773.6n ± 3% -3.03% (p=0.001 n=10) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity-4 920.3n ± 2% 926.0n ± 3% ~ (p=0.896 n=10) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/-4 920.4n ± 4% 908.2n ± 2% ~ (p=0.063 n=10) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/1-4 927.9n ± 2% 919.0n ± 3% ~ (p=0.579 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v-4 920.4n ± 3% 889.5n ± 3% -3.36% (p=0.007 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v2-4 916.9n ± 2% 891.9n ± 2% -2.73% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v1/-4 938.8n ± 5% 891.2n ± 2% -5.07% (p=0.000 n=10) geomean 591.7n 575.5n -2.73% │ route_pattern_match_old.txt │ route_pattern_match_new3.txt │ │ B/op │ B/op vs base │ _RoutePatternMatch//api/v1/const_|_match_|_/api/v1/const-4 168.0 ± 0% 152.0 ± 0% -9.52% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1-4 160.0 ± 0% 144.0 ± 0% -10.00% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/-4 160.0 ± 0% 144.0 ± 0% -10.00% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/something-4 176.0 ± 0% 160.0 ± 0% -9.09% (p=0.000 n=10) _RoutePatternMatch//api/:param/fixedEnd_|_match_|_/api/abc/fixedEnd-4 440.0 ± 0% 440.0 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/:param/fixedEnd_|_not_match_|_/api/abc/def/fixedEnd-4 464.0 ± 0% 440.0 ± 0% -5.17% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity-4 536.0 ± 0% 536.0 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/-4 536.0 ± 0% 536.0 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/1-4 536.0 ± 0% 536.0 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v-4 544.0 ± 0% 528.0 ± 0% -2.94% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v2-4 544.0 ± 0% 528.0 ± 0% -2.94% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v1/-4 544.0 ± 0% 528.0 ± 0% -2.94% (p=0.000 n=10) geomean 353.7 337.9 -4.47% ¹ all samples are equal │ route_pattern_match_old.txt │ route_pattern_match_new3.txt │ │ allocs/op │ allocs/op vs base │ _RoutePatternMatch//api/v1/const_|_match_|_/api/v1/const-4 6.000 ± 0% 5.000 ± 0% -16.67% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1-4 6.000 ± 0% 5.000 ± 0% -16.67% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/-4 6.000 ± 0% 5.000 ± 0% -16.67% (p=0.000 n=10) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/something-4 6.000 ± 0% 5.000 ± 0% -16.67% (p=0.000 n=10) _RoutePatternMatch//api/:param/fixedEnd_|_match_|_/api/abc/fixedEnd-4 13.00 ± 0% 13.00 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/:param/fixedEnd_|_not_match_|_/api/abc/def/fixedEnd-4 14.00 ± 0% 13.00 ± 0% -7.14% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity-4 14.00 ± 0% 14.00 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/-4 14.00 ± 0% 14.00 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/1-4 14.00 ± 0% 14.00 ± 0% ~ (p=1.000 n=10) ¹ _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v-4 15.00 ± 0% 14.00 ± 0% -6.67% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v2-4 15.00 ± 0% 14.00 ± 0% -6.67% (p=0.000 n=10) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v1/-4 15.00 ± 0% 14.00 ± 0% -6.67% (p=0.000 n=10) geomean 10.67 9.811 -8.08% ¹ all samples are equal ``` --------- Co-authored-by: RW --- path.go | 60 +++++++++++++++++++++++++++++++-------------------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/path.go b/path.go index 282073ec..e8b90eaf 100644 --- a/path.go +++ b/path.go @@ -152,11 +152,11 @@ func RoutePatternMatch(path, pattern string, cfg ...Config) bool { pattern = "/" + pattern } - patternPretty := pattern + patternPretty := []byte(pattern) // Case-sensitive routing, all to lowercase if !config.CaseSensitive { - patternPretty = utils.ToLower(patternPretty) + patternPretty = utils.ToLowerBytes(patternPretty) path = utils.ToLower(path) } // Strict routing, remove trailing slashes @@ -164,12 +164,12 @@ func RoutePatternMatch(path, pattern string, cfg ...Config) bool { patternPretty = utils.TrimRight(patternPretty, '/') } - parser := parseRoute(patternPretty) + parser := parseRoute(string(patternPretty)) - if patternPretty == "/" && path == "/" { + if string(patternPretty) == "/" && path == "/" { return true // '*' wildcard matches any path - } else if patternPretty == "/*" { + } else if string(patternPretty) == "/*" { return true } @@ -180,35 +180,28 @@ func RoutePatternMatch(path, pattern string, cfg ...Config) bool { } } // Check for a simple match - patternPretty = RemoveEscapeChar(patternPretty) - if len(patternPretty) == len(path) && patternPretty == path { - return true - } - // No match - return false + patternPretty = RemoveEscapeCharBytes(patternPretty) + + return string(patternPretty) == path } // parseRoute analyzes the route and divides it into segments for constant areas and parameters, // this information is needed later when assigning the requests to the declared routes func parseRoute(pattern string, customConstraints ...CustomConstraint) routeParser { parser := routeParser{} - part := "" + var n int + var seg *routeSegment for len(pattern) > 0 { nextParamPosition := findNextParamPosition(pattern) // handle the parameter part if nextParamPosition == 0 { - processedPart, seg := parser.analyseParameterPart(pattern, customConstraints...) - parser.params, parser.segs, part = append(parser.params, seg.ParamName), append(parser.segs, seg), processedPart + n, seg = parser.analyseParameterPart(pattern, customConstraints...) + parser.params, parser.segs = append(parser.params, seg.ParamName), append(parser.segs, seg) } else { - processedPart, seg := parser.analyseConstantPart(pattern, nextParamPosition) - parser.segs, part = append(parser.segs, seg), processedPart + n, seg = parser.analyseConstantPart(pattern, nextParamPosition) + parser.segs = append(parser.segs, seg) } - - // reduce the pattern by the processed parts - if len(part) == len(pattern) { - break - } - pattern = pattern[len(part):] + pattern = pattern[n:] } // mark last segment if len(parser.segs) > 0 { @@ -283,7 +276,7 @@ func findNextParamPosition(pattern string) int { } // analyseConstantPart find the end of the constant part and create the route segment -func (*routeParser) analyseConstantPart(pattern string, nextParamPosition int) (string, *routeSegment) { +func (*routeParser) analyseConstantPart(pattern string, nextParamPosition int) (int, *routeSegment) { // handle the constant part processedPart := pattern if nextParamPosition != -1 { @@ -291,14 +284,14 @@ func (*routeParser) analyseConstantPart(pattern string, nextParamPosition int) ( processedPart = pattern[:nextParamPosition] } constPart := RemoveEscapeChar(processedPart) - return processedPart, &routeSegment{ + return len(processedPart), &routeSegment{ Const: constPart, Length: len(constPart), } } // analyseParameterPart find the parameter end and create the route segment -func (routeParser *routeParser) analyseParameterPart(pattern string, customConstraints ...CustomConstraint) (string, *routeSegment) { +func (routeParser *routeParser) analyseParameterPart(pattern string, customConstraints ...CustomConstraint) (int, *routeSegment) { isWildCard := pattern[0] == wildcardParam isPlusParam := pattern[0] == plusParam @@ -329,6 +322,7 @@ func (routeParser *routeParser) analyseParameterPart(pattern string, customConst // cut params part processedPart := pattern[0 : parameterEndPosition+1] + n := parameterEndPosition + 1 paramName := RemoveEscapeChar(GetTrimmedParam(processedPart)) // Check has constraint @@ -402,7 +396,7 @@ func (routeParser *routeParser) analyseParameterPart(pattern string, customConst segment.Constraints = constraints } - return processedPart, segment + return n, segment } // isInCharset check is the given character in the charset list @@ -618,7 +612,7 @@ func GetTrimmedParam(param string) string { return param[start:end] } -// RemoveEscapeChar remove escape characters +// RemoveEscapeChar removes escape characters func RemoveEscapeChar(word string) string { b := []byte(word) dst := 0 @@ -632,6 +626,18 @@ func RemoveEscapeChar(word string) string { return string(b[:dst]) } +// RemoveEscapeCharBytes removes escape characters +func RemoveEscapeCharBytes(word []byte) []byte { + dst := 0 + for src := 0; src < len(word); src++ { + if word[src] != '\\' { + word[dst] = word[src] + dst++ + } + } + return word[:dst] +} + func getParamConstraintType(constraintPart string) TypeConstraint { switch constraintPart { case ConstraintInt: From 8e54c8f93809c4257b03548c16cffb8134fb5e3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Tue, 4 Mar 2025 12:46:05 +0800 Subject: [PATCH 06/26] =?UTF-8?q?=F0=9F=90=9B=20fix:=20Update=20binder=20i?= =?UTF-8?q?n=20form=5Ftest=20(#3336)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: error binder in form_test * fix: form tag * Fix: error package name --- binder/README.md | 2 +- binder/form_test.go | 10 +++++----- binder/mapping.go | 2 +- error.go | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/binder/README.md b/binder/README.md index 4d75fd1b..aded1efa 100644 --- a/binder/README.md +++ b/binder/README.md @@ -28,7 +28,7 @@ Fiber provides several default binders out of the box: ### Binding into a Struct -Fiber supports binding request data directly into a struct using [gorilla/schema](https://github.com/gorilla/schema). Here's an example: +Fiber supports binding request data directly into a struct using [gofiber/schema](https://github.com/gofiber/schema). Here's an example: ```go // Field names must start with an uppercase letter diff --git a/binder/form_test.go b/binder/form_test.go index d961f873..ac421d82 100644 --- a/binder/form_test.go +++ b/binder/form_test.go @@ -58,19 +58,19 @@ func Benchmark_FormBinder_Bind(b *testing.B) { b.ReportAllocs() b.ResetTimer() - binder := &QueryBinding{ + binder := &FormBinding{ EnableSplitting: true, } type User struct { - Name string `query:"name"` - Posts []string `query:"posts"` - Age int `query:"age"` + Name string `form:"name"` + Posts []string `form:"posts"` + Age int `form:"age"` } var user User req := fasthttp.AcquireRequest() - req.URI().SetQueryString("name=john&age=42&posts=post1,post2,post3") + req.SetBodyString("name=john&age=42&posts=post1,post2,post3") req.Header.SetContentType("application/x-www-form-urlencoded") b.ResetTimer() diff --git a/binder/mapping.go b/binder/mapping.go index bc95d028..41026a16 100644 --- a/binder/mapping.go +++ b/binder/mapping.go @@ -87,7 +87,7 @@ func parse(aliasTag string, out any, data map[string][]string, files ...map[stri return parseToStruct(aliasTag, out, data, files...) } -// Parse data into the struct with gorilla/schema +// Parse data into the struct with gofiber/schema func parseToStruct(aliasTag string, out any, data map[string][]string, files ...map[string][]*multipart.FileHeader) error { // Get decoder from pool schemaDecoder := decoderPoolMap[aliasTag].Get().(*schema.Decoder) //nolint:errcheck,forcetypeassert // not needed diff --git a/error.go b/error.go index f0375657..8d0ca635 100644 --- a/error.go +++ b/error.go @@ -40,7 +40,7 @@ var ( ErrNoHandlers = errors.New("format: at least one handler is required, but none were set") ) -// gorilla/schema errors +// gofiber/schema errors type ( // ConversionError Conversion error exposes the internal schema.ConversionError for public use. ConversionError = schema.ConversionError From 64c1771c2687f66dd231eb8939ecd0f3c13541a0 Mon Sep 17 00:00:00 2001 From: nickajacks1 <128185314+nickajacks1@users.noreply.github.com> Date: Tue, 4 Mar 2025 23:01:43 -0800 Subject: [PATCH 07/26] =?UTF-8?q?=F0=9F=94=A5=20Feature:=20Add=20Req=20and?= =?UTF-8?q?=20Res=20API=20(#2894)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔥 feat: add Req and Res interfaces Split the existing Ctx API into two separate APIs for Requests and Responses. There are two goals to this change: 1. Reduce cognitive load by making it more obvious whether a Ctx method interacts with the request or the response. 2. Increase API parity with Express. * fix(req,res): several issues * Sprinkle in calls to Req() and Res() to a few unit tests * Fix improper initialization caught by ^ * Add a few missing methods * docs: organize Ctx methods by request and response * feat(req,res): sync more missed methods --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> --- ctx.go | 14 + ctx_interface.go | 6 +- ctx_interface_gen.go | 6 + ctx_test.go | 68 +- docs/api/ctx.md | 2024 +++++++++++++++++++++--------------------- req.go | 159 ++++ req_interface_gen.go | 49 + res.go | 118 +++ res_interface_gen.go | 38 + 9 files changed, 1443 insertions(+), 1039 deletions(-) create mode 100644 req.go create mode 100644 req_interface_gen.go create mode 100644 res.go create mode 100644 res_interface_gen.go diff --git a/ctx.go b/ctx.go index aecfacdc..07ffb1db 100644 --- a/ctx.go +++ b/ctx.go @@ -54,6 +54,8 @@ type DefaultCtx struct { fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx bind *Bind // Default bind reference redirect *Redirect // Default redirect reference + req *DefaultReq // Default request api reference + res *DefaultRes // Default response api reference values [maxParams]string // Route parameter values viewBindMap sync.Map // Default view map to bind template engine method string // HTTP method @@ -1463,6 +1465,18 @@ func (c *DefaultCtx) renderExtensions(bind any) { } } +// Req returns a convenience type whose API is limited to operations +// on the incoming request. +func (c *DefaultCtx) Req() Req { + return c.req +} + +// Res returns a convenience type whose API is limited to operations +// on the outgoing response. +func (c *DefaultCtx) Res() Res { + return c.res +} + // Route returns the matched Route struct. func (c *DefaultCtx) Route() *Route { if c.route == nil { diff --git a/ctx_interface.go b/ctx_interface.go index ca438d82..32e8ee39 100644 --- a/ctx_interface.go +++ b/ctx_interface.go @@ -32,10 +32,14 @@ type CustomCtx interface { func NewDefaultCtx(app *App) *DefaultCtx { // return ctx - return &DefaultCtx{ + ctx := &DefaultCtx{ // Set app reference app: app, } + ctx.req = &DefaultReq{ctx: ctx} + ctx.res = &DefaultRes{ctx: ctx} + + return ctx } func (app *App) newCtx() Ctx { diff --git a/ctx_interface_gen.go b/ctx_interface_gen.go index 101068a2..fffe218d 100644 --- a/ctx_interface_gen.go +++ b/ctx_interface_gen.go @@ -265,6 +265,12 @@ type Ctx interface { // We support the following engines: https://github.com/gofiber/template Render(name string, bind any, layouts ...string) error renderExtensions(bind any) + // Req returns a convenience type whose API is limited to operations + // on the incoming request. + Req() Req + // Res returns a convenience type whose API is limited to operations + // on the outgoing response. + Res() Res // Route returns the matched Route struct. Route() *Route // SaveFile saves any multipart file to disk. diff --git a/ctx_test.go b/ctx_test.go index 082b0d44..ee272d24 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -46,7 +46,7 @@ func Test_Ctx_Accepts(t *testing.T) { c.Request().Header.Set(HeaderAccept, "text/html,application/xhtml+xml,application/xml;q=0.9") require.Equal(t, "", c.Accepts("")) - require.Equal(t, "", c.Accepts()) + require.Equal(t, "", c.Req().Accepts()) require.Equal(t, ".xml", c.Accepts(".xml")) require.Equal(t, "", c.Accepts(".john")) require.Equal(t, "application/xhtml+xml", c.Accepts("application/xml", "application/xml+rss", "application/yaml", "application/xhtml+xml"), "must use client-preferred mime type") @@ -57,13 +57,13 @@ func Test_Ctx_Accepts(t *testing.T) { c.Request().Header.Set(HeaderAccept, "text/*, application/json") require.Equal(t, "html", c.Accepts("html")) require.Equal(t, "text/html", c.Accepts("text/html")) - require.Equal(t, "json", c.Accepts("json", "text")) + require.Equal(t, "json", c.Req().Accepts("json", "text")) require.Equal(t, "application/json", c.Accepts("application/json")) require.Equal(t, "", c.Accepts("image/png")) require.Equal(t, "", c.Accepts("png")) c.Request().Header.Set(HeaderAccept, "text/html, application/json") - require.Equal(t, "text/*", c.Accepts("text/*")) + require.Equal(t, "text/*", c.Req().Accepts("text/*")) c.Request().Header.Set(HeaderAccept, "*/*") require.Equal(t, "html", c.Accepts("html")) @@ -968,46 +968,46 @@ func Test_Ctx_Cookie(t *testing.T) { Expires: expire, // SameSite: CookieSameSiteStrictMode, // default is "lax" } - c.Cookie(cookie) + c.Res().Cookie(cookie) expect := "username=john; expires=" + httpdate + "; path=/; SameSite=Lax" - require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + require.Equal(t, expect, c.Res().Get(HeaderSetCookie)) expect = "username=john; expires=" + httpdate + "; path=/" cookie.SameSite = CookieSameSiteDisabled - c.Cookie(cookie) - require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + c.Res().Cookie(cookie) + require.Equal(t, expect, c.Res().Get(HeaderSetCookie)) expect = "username=john; expires=" + httpdate + "; path=/; SameSite=Strict" cookie.SameSite = CookieSameSiteStrictMode - c.Cookie(cookie) - require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + c.Res().Cookie(cookie) + require.Equal(t, expect, c.Res().Get(HeaderSetCookie)) expect = "username=john; expires=" + httpdate + "; path=/; secure; SameSite=None" cookie.Secure = true cookie.SameSite = CookieSameSiteNoneMode - c.Cookie(cookie) - require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + c.Res().Cookie(cookie) + require.Equal(t, expect, c.Res().Get(HeaderSetCookie)) expect = "username=john; path=/; secure; SameSite=None" // should remove expires and max-age headers cookie.SessionOnly = true cookie.Expires = expire cookie.MaxAge = 10000 - c.Cookie(cookie) - require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + c.Res().Cookie(cookie) + require.Equal(t, expect, c.Res().Get(HeaderSetCookie)) expect = "username=john; path=/; secure; SameSite=None" // should remove expires and max-age headers when no expire and no MaxAge (default time) cookie.SessionOnly = false cookie.Expires = time.Time{} cookie.MaxAge = 0 - c.Cookie(cookie) - require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + c.Res().Cookie(cookie) + require.Equal(t, expect, c.Res().Get(HeaderSetCookie)) expect = "username=john; path=/; secure; SameSite=None; Partitioned" cookie.Partitioned = true - c.Cookie(cookie) - require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + c.Res().Cookie(cookie) + require.Equal(t, expect, c.Res().Get(HeaderSetCookie)) } // go test -v -run=^$ -bench=Benchmark_Ctx_Cookie -benchmem -count=4 @@ -1033,8 +1033,8 @@ func Test_Ctx_Cookies(t *testing.T) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().Header.Set("Cookie", "john=doe") - require.Equal(t, "doe", c.Cookies("john")) - require.Equal(t, "default", c.Cookies("unknown", "default")) + require.Equal(t, "doe", c.Req().Cookies("john")) + require.Equal(t, "default", c.Req().Cookies("unknown", "default")) } // go test -run Test_Ctx_Format @@ -1058,13 +1058,13 @@ func Test_Ctx_Format(t *testing.T) { } c.Request().Header.Set(HeaderAccept, `text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7`) - err := c.Format(formatHandlers("application/xhtml+xml", "application/xml", "foo/bar")...) + err := c.Res().Format(formatHandlers("application/xhtml+xml", "application/xml", "foo/bar")...) require.Equal(t, "application/xhtml+xml", accepted) require.Equal(t, "application/xhtml+xml", c.GetRespHeader(HeaderContentType)) require.NoError(t, err) require.NotEqual(t, StatusNotAcceptable, c.Response().StatusCode()) - err = c.Format(formatHandlers("foo/bar;a=b")...) + err = c.Res().Format(formatHandlers("foo/bar;a=b")...) require.Equal(t, "foo/bar;a=b", accepted) require.Equal(t, "foo/bar;a=b", c.GetRespHeader(HeaderContentType)) require.NoError(t, err) @@ -1165,7 +1165,7 @@ func Test_Ctx_AutoFormat(t *testing.T) { require.Equal(t, "Hello, World!", string(c.Response().Body())) c.Request().Header.Set(HeaderAccept, MIMETextHTML) - err = c.AutoFormat("Hello, World!") + err = c.Res().AutoFormat("Hello, World!") require.NoError(t, err) require.Equal(t, "

Hello, World!

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

Hello, World!

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

Hello, World!

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