From 85a5fb8cf2db3255a3116551bf6f661b2c174d66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigmund=20Xia=20=E5=A4=8F=E5=A4=A9=E7=9D=BF?= Date: Thu, 3 Oct 2024 22:19:27 +0800 Subject: [PATCH 01/31] =?UTF-8?q?=F0=9F=A9=B9Fix:=20Adaptor=20middleware?= =?UTF-8?q?=20duplicates=20cookies=20(#3151)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🩹Fix: Adaptor middleware duplicates cookies * 🩹Fix: add extra cases for Test_HTTPMiddlewareWithCookies --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> --- middleware/adaptor/adaptor.go | 2 + middleware/adaptor/adaptor_test.go | 76 ++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/middleware/adaptor/adaptor.go b/middleware/adaptor/adaptor.go index a5040b39ae..03c6287e6f 100644 --- a/middleware/adaptor/adaptor.go +++ b/middleware/adaptor/adaptor.go @@ -101,6 +101,8 @@ func HTTPMiddleware(mw func(http.Handler) http.Handler) fiber.Handler { c.Request().SetHost(r.Host) c.Request().Header.SetHost(r.Host) + // Remove all cookies before setting, see https://github.com/valyala/fasthttp/pull/1864 + c.Request().Header.DelAllCookies() for key, val := range r.Header { for _, v := range val { c.Request().Header.Set(key, v) diff --git a/middleware/adaptor/adaptor_test.go b/middleware/adaptor/adaptor_test.go index c703d38436..990d421dec 100644 --- a/middleware/adaptor/adaptor_test.go +++ b/middleware/adaptor/adaptor_test.go @@ -10,6 +10,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "strings" "testing" "github.com/gofiber/fiber/v3" @@ -200,6 +201,81 @@ func Test_HTTPMiddleware(t *testing.T) { require.Equal(t, "okay", resp.Header.Get("context_second_okay")) } +func Test_HTTPMiddlewareWithCookies(t *testing.T) { + const ( + cookieHeader = "Cookie" + setCookieHeader = "Set-Cookie" + cookieOneName = "cookieOne" + cookieTwoName = "cookieTwo" + cookieOneValue = "valueCookieOne" + cookieTwoValue = "valueCookieTwo" + ) + nethttpMW := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + next.ServeHTTP(w, r) + }) + } + + app := fiber.New() + app.Use(HTTPMiddleware(nethttpMW)) + app.Post("/", func(c fiber.Ctx) error { + // RETURNING CURRENT COOKIES TO RESPONSE + var cookies []string = strings.Split(c.Get(cookieHeader), "; ") + for _, cookie := range cookies { + c.Set(setCookieHeader, cookie) + } + return c.SendStatus(fiber.StatusOK) + }) + + // Test case for POST request with cookies + t.Run("POST request with cookies", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodPost, "/", nil) + require.NoError(t, err) + req.AddCookie(&http.Cookie{Name: cookieOneName, Value: cookieOneValue}) + req.AddCookie(&http.Cookie{Name: cookieTwoName, Value: cookieTwoValue}) + + resp, err := app.Test(req) + require.NoError(t, err) + cookies := resp.Cookies() + require.Len(t, cookies, 2) + for _, cookie := range cookies { + switch cookie.Name { + case cookieOneName: + require.Equal(t, cookieOneValue, cookie.Value) + case cookieTwoName: + require.Equal(t, cookieTwoValue, cookie.Value) + default: + t.Error("unexpected cookie key") + } + } + }) + + // New test case for GET request + t.Run("GET request", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/", nil) + require.NoError(t, err) + + resp, err := app.Test(req) + require.NoError(t, err) + require.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode) + }) + + // New test case for request without cookies + t.Run("POST request without cookies", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodPost, "/", nil) + require.NoError(t, err) + + resp, err := app.Test(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Empty(t, resp.Cookies()) + }) +} + func Test_FiberHandler(t *testing.T) { testFiberToHandlerFunc(t, false) } From 2eba89aadc01c0867b453cb8d1bfba4900969451 Mon Sep 17 00:00:00 2001 From: Muhamad Surya Iksanudin Date: Tue, 8 Oct 2024 03:23:23 +0700 Subject: [PATCH 02/31] docs: Fix typo on comment (#3158) fix typo --- bind.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bind.go b/bind.go index f7e449f6e3..e202cd85e0 100644 --- a/bind.go +++ b/bind.go @@ -160,7 +160,7 @@ func (b *Bind) MultipartForm(out any) error { // It supports decoding the following content types based on the Content-Type header: // application/json, application/xml, application/x-www-form-urlencoded, multipart/form-data // If none of the content types above are matched, it'll take a look custom binders by checking the MIMETypes() method of custom binder. -// If there're no custom binder for mşme type of body, it will return a ErrUnprocessableEntity error. +// If there're no custom binder for mime type of body, it will return a ErrUnprocessableEntity error. func (b *Bind) Body(out any) error { // Get content-type ctype := utils.ToLower(utils.UnsafeString(b.ctx.Context().Request.Header.ContentType())) From 0b6a26fb92bd18199d3cf9add31475acd49fb617 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 08:19:24 -0400 Subject: [PATCH 03/31] build(deps): bump codecov/codecov-action from 4.5.0 to 4.6.0 (#3154) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.5.0 to 4.6.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/v4.5.0...v4.6.0) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> --- .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 f7770993a4..ebf39c161c 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@v4.5.0 + uses: codecov/codecov-action@v4.6.0 with: token: ${{ secrets.CODECOV_TOKEN }} file: ./coverage.txt From c86c3c0a30a87eba3ada305fc70162ea8c875a54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Wed, 9 Oct 2024 09:41:15 +0200 Subject: [PATCH 04/31] Update documentation for monitor middleware migration --- docs/middleware/monitor.md | 94 -------------------------------------- docs/whats_new.md | 28 +++++++++--- 2 files changed, 22 insertions(+), 100 deletions(-) delete mode 100644 docs/middleware/monitor.md diff --git a/docs/middleware/monitor.md b/docs/middleware/monitor.md deleted file mode 100644 index aa3b1c89ee..0000000000 --- a/docs/middleware/monitor.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -id: monitor ---- - -# Monitor - -Monitor middleware for [Fiber](https://github.com/gofiber/fiber) that reports server metrics, inspired by [express-status-monitor](https://github.com/RafalWilinski/express-status-monitor) - -:::caution - -Monitor is still in beta, API might change in the future! - -::: - -![](https://i.imgur.com/nHAtBpJ.gif) - -## Signatures - -```go -func New() fiber.Handler -``` - -## Examples - -Import the middleware package that is part of the Fiber web framework - -```go -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/monitor" -) -``` - -After you initiate your Fiber app, you can use the following possibilities: - -```go -// Initialize default config (Assign the middleware to /metrics) -app.Get("/metrics", monitor.New()) - -// Or extend your config for customization -// Assign the middleware to /metrics -// and change the Title to `MyService Metrics Page` -app.Get("/metrics", monitor.New(monitor.Config{Title: "MyService Metrics Page"})) -``` - -You can also access the API endpoint with -`curl -X GET -H "Accept: application/json" http://localhost:3000/metrics` which returns: - -```json -{ - "pid":{ - "cpu":0.4568381746582226, - "ram":20516864, - "conns":3 - }, - "os": { - "cpu":8.759124087593099, "ram":3997155328, "conns":44, - "total_ram":8245489664, "load_avg":0.51 - } -} -``` - -## Config - -| Property | Type | Description | Default | -|:-----------|:------------------------|:--------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------| -| Title | `string` | Metrics page title | "Fiber Monitor" | -| Refresh | `time.Duration` | Refresh period | 3 seconds | -| APIOnly | `bool` | Whether the service should expose only the monitoring API | false | -| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | -| CustomHead | `string` | Custom HTML Code to Head Section(Before End) | empty | -| FontURL | `string` | FontURL for specify font resource path or URL | "[fonts.googleapis.com](https://fonts.googleapis.com/css2?family=Roboto:wght@400;900&display=swap)" | -| ChartJsURL | `string` | ChartJsURL for specify ChartJS library path or URL | "[cdn.jsdelivr.net](https://cdn.jsdelivr.net/npm/chart.js@2.9/dist/Chart.bundle.min.js)" | - -## Default Config - -```go -var ConfigDefault = Config{ - Title: defaultTitle, - Refresh: defaultRefresh, - FontURL: defaultFontURL, - ChartJsURL: defaultChartJSURL, - CustomHead: defaultCustomHead, - APIOnly: false, - Next: nil, - index: newIndex(viewBag{ - defaultTitle, - defaultRefresh, - defaultFontURL, - defaultChartJSURL, - defaultCustomHead, - }), -} -``` diff --git a/docs/whats_new.md b/docs/whats_new.md index 963d1daece..6449d24292 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -325,11 +325,7 @@ Now, static middleware can do everything that filesystem middleware and static d ### Monitor -:::caution -DRAFT section -::: - -Monitor middleware is now in Contrib package. +Monitor middleware is migrated to the [Contrib package](https://github.com/gofiber/contrib/tree/main/monitor) with [PR #1172](https://github.com/gofiber/contrib/pull/1172). ### Healthcheck @@ -526,7 +522,7 @@ app.Use(static.New("", static.Config{ })) ``` -### Healthcheck +#### Healthcheck Previously, the Healthcheck middleware was configured with a combined setup for liveliness and readiness probes: @@ -570,3 +566,23 @@ app.Get(healthcheck.DefaultStartupEndpoint, healthcheck.NewHealthChecker(healthc // Custom liveness endpoint configuration app.Get("/live", healthcheck.NewHealthChecker()) ``` + +#### Monitor + +Since v3 the Monitor middleware has been moved to the [Contrib package](https://github.com/gofiber/contrib/tree/main/monitor) + +```go +// Before +import "github.com/gofiber/fiber/v2/middleware/monitor" + +app.Use("/metrics", monitor.New()) +``` + +You only need to change the import path to the contrib package. + +```go +// After +import "github.com/gofiber/contrib/monitor" + +app.Use("/metrics", monitor.New()) +``` From 3fc1b297481d516297ae155bc08e09c92a5b572e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=2E=20Efe=20=C3=87etin?= Date: Fri, 11 Oct 2024 14:52:09 +0300 Subject: [PATCH 05/31] chore: replace vendored gorilla/schema package (#3152) --- bind_test.go | 73 +++-- binder/mapping.go | 9 +- docs/api/bind.md | 37 +++ error.go | 2 +- error_test.go | 2 +- go.mod | 1 + go.sum | 2 + internal/schema/LICENSE | 27 -- internal/schema/cache.go | 305 -------------------- internal/schema/converter.go | 145 ---------- internal/schema/decoder.go | 534 ----------------------------------- internal/schema/doc.go | 148 ---------- internal/schema/encoder.go | 202 ------------- 13 files changed, 104 insertions(+), 1383 deletions(-) delete mode 100644 internal/schema/LICENSE delete mode 100644 internal/schema/cache.go delete mode 100644 internal/schema/converter.go delete mode 100644 internal/schema/decoder.go delete mode 100644 internal/schema/doc.go delete mode 100644 internal/schema/encoder.go diff --git a/bind_test.go b/bind_test.go index 48f53f62a1..aa00e191ca 100644 --- a/bind_test.go +++ b/bind_test.go @@ -55,9 +55,11 @@ func Test_Bind_Query(t *testing.T) { type Query2 struct { Name string Hobby string + Default string `query:"default,default:hello"` FavouriteDrinks []string Empty []string Alloc []string + Defaults []string `query:"defaults,default:hello|world"` No []int64 ID int Bool bool @@ -76,13 +78,15 @@ func Test_Bind_Query(t *testing.T) { require.Equal(t, nilSlice, q2.Empty) require.Equal(t, []string{""}, q2.Alloc) require.Equal(t, []int64{1}, q2.No) + require.Equal(t, "hello", q2.Default) + require.Equal(t, []string{"hello", "world"}, q2.Defaults) type RequiredQuery struct { Name string `query:"name,required"` } rq := new(RequiredQuery) c.Request().URI().SetQueryString("") - require.Equal(t, "name is empty", c.Bind().Query(rq).Error()) + require.Equal(t, "bind: name is empty", c.Bind().Query(rq).Error()) type ArrayQuery struct { Data []string @@ -204,7 +208,7 @@ func Test_Bind_Query_Schema(t *testing.T) { c.Request().URI().SetQueryString("namex=tom&nested.age=10") q = new(Query1) - require.Equal(t, "name is empty", c.Bind().Query(q).Error()) + require.Equal(t, "bind: name is empty", c.Bind().Query(q).Error()) c.Request().URI().SetQueryString("name=tom&nested.agex=10") q = new(Query1) @@ -212,7 +216,7 @@ func Test_Bind_Query_Schema(t *testing.T) { c.Request().URI().SetQueryString("name=tom&test.age=10") q = new(Query1) - require.Equal(t, "nested is empty", c.Bind().Query(q).Error()) + require.Equal(t, "bind: nested is empty", c.Bind().Query(q).Error()) type Query2 struct { Name string `query:"name"` @@ -230,11 +234,11 @@ func Test_Bind_Query_Schema(t *testing.T) { c.Request().URI().SetQueryString("nested.agex=10") q2 = new(Query2) - require.Equal(t, "nested.age is empty", c.Bind().Query(q2).Error()) + require.Equal(t, "bind: nested.age is empty", c.Bind().Query(q2).Error()) c.Request().URI().SetQueryString("nested.agex=10") q2 = new(Query2) - require.Equal(t, "nested.age is empty", c.Bind().Query(q2).Error()) + require.Equal(t, "bind: nested.age is empty", c.Bind().Query(q2).Error()) type Node struct { Next *Node `query:"next,required"` @@ -248,7 +252,7 @@ func Test_Bind_Query_Schema(t *testing.T) { c.Request().URI().SetQueryString("next.val=2") n = new(Node) - require.Equal(t, "val is empty", c.Bind().Query(n).Error()) + require.Equal(t, "bind: val is empty", c.Bind().Query(n).Error()) c.Request().URI().SetQueryString("val=3&next.value=2") n = new(Node) @@ -354,7 +358,7 @@ func Test_Bind_Header(t *testing.T) { } rh := new(RequiredHeader) c.Request().Header.Del("name") - require.Equal(t, "name is empty", c.Bind().Header(rh).Error()) + require.Equal(t, "bind: name is empty", c.Bind().Header(rh).Error()) } // go test -run Test_Bind_Header_Map -v @@ -463,7 +467,7 @@ func Test_Bind_Header_Schema(t *testing.T) { c.Request().Header.Del("Name") q = new(Header1) - require.Equal(t, "Name is empty", c.Bind().Header(q).Error()) + require.Equal(t, "bind: Name is empty", c.Bind().Header(q).Error()) c.Request().Header.Add("Name", "tom") c.Request().Header.Del("Nested.Age") @@ -473,7 +477,7 @@ func Test_Bind_Header_Schema(t *testing.T) { c.Request().Header.Del("Nested.Agex") q = new(Header1) - require.Equal(t, "Nested is empty", c.Bind().Header(q).Error()) + require.Equal(t, "bind: Nested is empty", c.Bind().Header(q).Error()) c.Request().Header.Del("Nested.Agex") c.Request().Header.Del("Name") @@ -499,7 +503,7 @@ func Test_Bind_Header_Schema(t *testing.T) { c.Request().Header.Del("Nested.Age") c.Request().Header.Add("Nested.Agex", "10") h2 = new(Header2) - require.Equal(t, "Nested.age is empty", c.Bind().Header(h2).Error()) + require.Equal(t, "bind: Nested.age is empty", c.Bind().Header(h2).Error()) type Node struct { Next *Node `header:"Next,required"` @@ -514,7 +518,7 @@ func Test_Bind_Header_Schema(t *testing.T) { c.Request().Header.Del("Val") n = new(Node) - require.Equal(t, "Val is empty", c.Bind().Header(n).Error()) + require.Equal(t, "bind: Val is empty", c.Bind().Header(n).Error()) c.Request().Header.Add("Val", "3") c.Request().Header.Del("Next.Val") @@ -595,7 +599,7 @@ func Test_Bind_RespHeader(t *testing.T) { } rh := new(RequiredHeader) c.Response().Header.Del("name") - require.Equal(t, "name is empty", c.Bind().RespHeader(rh).Error()) + require.Equal(t, "bind: name is empty", c.Bind().RespHeader(rh).Error()) } // go test -run Test_Bind_RespHeader_Map -v @@ -648,7 +652,40 @@ func Benchmark_Bind_Query(b *testing.B) { for n := 0; n < b.N; n++ { err = c.Bind().Query(q) } + + require.NoError(b, err) + require.Equal(b, "tom", q.Name) + require.Equal(b, 1, q.ID) + require.Len(b, q.Hobby, 2) +} + +// go test -v -run=^$ -bench=Benchmark_Bind_Query_Default -benchmem -count=4 +func Benchmark_Bind_Query_Default(b *testing.B) { + var err error + + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + + type Query struct { + Name string `query:"name,default:tom"` + Hobby []string `query:"hobby,default:football|basketball"` + ID int `query:"id,default:1"` + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + c.Request().URI().SetQueryString("") + q := new(Query) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + *q = Query{} + err = c.Bind().Query(q) + } + require.NoError(b, err) + require.Equal(b, "tom", q.Name) + require.Equal(b, 1, q.ID) + require.Len(b, q.Hobby, 2) } // go test -v -run=^$ -bench=Benchmark_Bind_Query_Map -benchmem -count=4 @@ -1314,7 +1351,7 @@ func Test_Bind_Cookie(t *testing.T) { } rh := new(RequiredCookie) c.Request().Header.DelCookie("name") - require.Equal(t, "name is empty", c.Bind().Cookie(rh).Error()) + require.Equal(t, "bind: name is empty", c.Bind().Cookie(rh).Error()) } // go test -run Test_Bind_Cookie_Map -v @@ -1424,7 +1461,7 @@ func Test_Bind_Cookie_Schema(t *testing.T) { c.Request().Header.DelCookie("Name") q = new(Cookie1) - require.Equal(t, "Name is empty", c.Bind().Cookie(q).Error()) + require.Equal(t, "bind: Name is empty", c.Bind().Cookie(q).Error()) c.Request().Header.SetCookie("Name", "tom") c.Request().Header.DelCookie("Nested.Age") @@ -1434,7 +1471,7 @@ func Test_Bind_Cookie_Schema(t *testing.T) { c.Request().Header.DelCookie("Nested.Agex") q = new(Cookie1) - require.Equal(t, "Nested is empty", c.Bind().Cookie(q).Error()) + require.Equal(t, "bind: Nested is empty", c.Bind().Cookie(q).Error()) c.Request().Header.DelCookie("Nested.Agex") c.Request().Header.DelCookie("Name") @@ -1460,7 +1497,7 @@ func Test_Bind_Cookie_Schema(t *testing.T) { c.Request().Header.DelCookie("Nested.Age") c.Request().Header.SetCookie("Nested.Agex", "10") h2 = new(Cookie2) - require.Equal(t, "Nested.Age is empty", c.Bind().Cookie(h2).Error()) + require.Equal(t, "bind: Nested.Age is empty", c.Bind().Cookie(h2).Error()) type Node struct { Next *Node `cookie:"Next,required"` @@ -1475,7 +1512,7 @@ func Test_Bind_Cookie_Schema(t *testing.T) { c.Request().Header.DelCookie("Val") n = new(Node) - require.Equal(t, "Val is empty", c.Bind().Cookie(n).Error()) + require.Equal(t, "bind: Val is empty", c.Bind().Cookie(n).Error()) c.Request().Header.SetCookie("Val", "3") c.Request().Header.DelCookie("Next.Val") @@ -1591,7 +1628,7 @@ func Test_Bind_Must(t *testing.T) { c.Request().URI().SetQueryString("") err := c.Bind().Must().Query(rq) require.Equal(t, StatusBadRequest, c.Response().StatusCode()) - require.Equal(t, "Bad request: name is empty", err.Error()) + require.Equal(t, "Bad request: bind: name is empty", err.Error()) } // simple struct validator for testing diff --git a/binder/mapping.go b/binder/mapping.go index 36821be087..ea67ace200 100644 --- a/binder/mapping.go +++ b/binder/mapping.go @@ -2,6 +2,7 @@ package binder import ( "errors" + "fmt" "reflect" "strings" "sync" @@ -9,7 +10,7 @@ import ( "github.com/gofiber/utils/v2" "github.com/valyala/bytebufferpool" - "github.com/gofiber/fiber/v3/internal/schema" + "github.com/gofiber/schema" ) // ParserConfig form decoder config for SetParserDecoder @@ -94,7 +95,11 @@ func parseToStruct(aliasTag string, out any, data map[string][]string) error { // Set alias tag schemaDecoder.SetAliasTag(aliasTag) - return schemaDecoder.Decode(out, data) + if err := schemaDecoder.Decode(out, data); err != nil { + return fmt.Errorf("bind: %w", err) + } + + return nil } // Parse data into the map diff --git a/docs/api/bind.md b/docs/api/bind.md index 927d423b92..73256cbbb8 100644 --- a/docs/api/bind.md +++ b/docs/api/bind.md @@ -573,3 +573,40 @@ app.Post("/", func(c fiber.Ctx) error { } }) ``` + +## Default Fields + +You can set default values for fields in the struct by using the `default` struct tag. Supported types: + +- bool +- float variants (float32, float64) +- int variants (int, int8, int16, int32, int64) +- uint variants (uint, uint8, uint16, uint32, uint64) +- string +- a slice of the above types. As shown in the example above, **| should be used to separate between slice items**. +- a pointer to one of the above types **(pointer to slice and slice of pointers are not supported)**. + +```go title="Example" +type Person struct { + Name string `query:"name,default:john"` + Pass string `query:"pass"` + Products []string `query:"products,default:shoe|hat"` +} + +app.Get("/", func(c fiber.Ctx) error { + p := new(Person) + + if err := c.Bind().Query(p); err != nil { + return err + } + + log.Println(p.Name) // john + log.Println(p.Pass) // doe + log.Println(p.Products) // ["shoe,hat"] + + // ... +}) +// Run tests with the following curl command + +// curl "http://localhost:3000/?pass=doe" +``` diff --git a/error.go b/error.go index 2e44c27769..f037565783 100644 --- a/error.go +++ b/error.go @@ -4,7 +4,7 @@ import ( "encoding/json" "errors" - "github.com/gofiber/fiber/v3/internal/schema" + "github.com/gofiber/schema" ) // Wrap and return this for unreachable code if panicking is undesirable (i.e., in a handler). diff --git a/error_test.go b/error_test.go index e2eace101c..ec29fbfd06 100644 --- a/error_test.go +++ b/error_test.go @@ -5,7 +5,7 @@ import ( "errors" "testing" - "github.com/gofiber/fiber/v3/internal/schema" + "github.com/gofiber/schema" "github.com/stretchr/testify/require" ) diff --git a/go.mod b/go.mod index 85002a6eb3..8f3a2a43d3 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/gofiber/fiber/v3 go 1.22 require ( + github.com/gofiber/schema v1.2.0 github.com/gofiber/utils/v2 v2.0.0-beta.6 github.com/google/uuid v1.6.0 github.com/mattn/go-colorable v0.1.13 diff --git a/go.sum b/go.sum index 48867e3cf6..1d53c4560b 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1 github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gofiber/schema v1.2.0 h1:j+ZRrNnUa/0ZuWrn/6kAtAufEr4jCJ+JuTURAMxNSZg= +github.com/gofiber/schema v1.2.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c= github.com/gofiber/utils/v2 v2.0.0-beta.6 h1:ED62bOmpRXdgviPlfTmf0Q+AXzhaTUAFtdWjgx+XkYI= github.com/gofiber/utils/v2 v2.0.0-beta.6/go.mod h1:3Kz8Px3jInKFvqxDzDeoSygwEOO+3uyubTmUa6PqY+0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= diff --git a/internal/schema/LICENSE b/internal/schema/LICENSE deleted file mode 100644 index 0e5fb87280..0000000000 --- a/internal/schema/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2012 Rodrigo Moraes. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/internal/schema/cache.go b/internal/schema/cache.go deleted file mode 100644 index 85e28f174a..0000000000 --- a/internal/schema/cache.go +++ /dev/null @@ -1,305 +0,0 @@ -// Copyright 2012 The Gorilla Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package schema - -import ( - "errors" - "reflect" - "strconv" - "strings" - "sync" -) - -var errInvalidPath = errors.New("schema: invalid path") - -// newCache returns a new cache. -func newCache() *cache { - c := cache{ - m: make(map[reflect.Type]*structInfo), - regconv: make(map[reflect.Type]Converter), - tag: "schema", - } - return &c -} - -// cache caches meta-data about a struct. -type cache struct { - m map[reflect.Type]*structInfo - regconv map[reflect.Type]Converter - tag string - l sync.RWMutex -} - -// registerConverter registers a converter function for a custom type. -func (c *cache) registerConverter(value any, converterFunc Converter) { - c.regconv[reflect.TypeOf(value)] = converterFunc -} - -// parsePath parses a path in dotted notation verifying that it is a valid -// path to a struct field. -// -// It returns "path parts" which contain indices to fields to be used by -// reflect.Value.FieldByString(). Multiple parts are required for slices of -// structs. -func (c *cache) parsePath(p string, t reflect.Type) ([]pathPart, error) { - var struc *structInfo - var field *fieldInfo - var index64 int64 - var err error - parts := make([]pathPart, 0) - path := make([]string, 0) - keys := strings.Split(p, ".") - for i := 0; i < len(keys); i++ { - if t.Kind() != reflect.Struct { - return nil, errInvalidPath - } - if struc = c.get(t); struc == nil { - return nil, errInvalidPath - } - if field = struc.get(keys[i]); field == nil { - return nil, errInvalidPath - } - // Valid field. Append index. - path = append(path, field.name) - if field.isSliceOfStructs && (!field.unmarshalerInfo.IsValid || (field.unmarshalerInfo.IsValid && field.unmarshalerInfo.IsSliceElement)) { - // Parse a special case: slices of structs. - // i+1 must be the slice index. - // - // Now that struct can implements TextUnmarshaler interface, - // we don't need to force the struct's fields to appear in the path. - // So checking i+2 is not necessary anymore. - i++ - if i+1 > len(keys) { - return nil, errInvalidPath - } - if index64, err = strconv.ParseInt(keys[i], 10, 0); err != nil { - return nil, errInvalidPath - } - parts = append(parts, pathPart{ - path: path, - field: field, - index: int(index64), - }) - path = make([]string, 0) - - // Get the next struct type, dropping ptrs. - if field.typ.Kind() == reflect.Ptr { - t = field.typ.Elem() - } else { - t = field.typ - } - if t.Kind() == reflect.Slice { - t = t.Elem() - if t.Kind() == reflect.Ptr { - t = t.Elem() - } - } - } else if field.typ.Kind() == reflect.Ptr { - t = field.typ.Elem() - } else { - t = field.typ - } - } - // Add the remaining. - parts = append(parts, pathPart{ - path: path, - field: field, - index: -1, - }) - return parts, nil -} - -// get returns a cached structInfo, creating it if necessary. -func (c *cache) get(t reflect.Type) *structInfo { - c.l.RLock() - info := c.m[t] - c.l.RUnlock() - if info == nil { - info = c.create(t, "") - c.l.Lock() - c.m[t] = info - c.l.Unlock() - } - return info -} - -// create creates a structInfo with meta-data about a struct. -func (c *cache) create(t reflect.Type, parentAlias string) *structInfo { - info := &structInfo{} - var anonymousInfos []*structInfo - for i := 0; i < t.NumField(); i++ { - if f := c.createField(t.Field(i), parentAlias); f != nil { - info.fields = append(info.fields, f) - if ft := indirectType(f.typ); ft.Kind() == reflect.Struct && f.isAnonymous { - anonymousInfos = append(anonymousInfos, c.create(ft, f.canonicalAlias)) - } - } - } - for i, a := range anonymousInfos { - others := []*structInfo{info} - others = append(others, anonymousInfos[:i]...) - others = append(others, anonymousInfos[i+1:]...) - for _, f := range a.fields { - if !containsAlias(others, f.alias) { - info.fields = append(info.fields, f) - } - } - } - return info -} - -// createField creates a fieldInfo for the given field. -func (c *cache) createField(field reflect.StructField, parentAlias string) *fieldInfo { - alias, options := fieldAlias(field, c.tag) - if alias == "-" { - // Ignore this field. - return nil - } - canonicalAlias := alias - if parentAlias != "" { - canonicalAlias = parentAlias + "." + alias - } - // Check if the type is supported and don't cache it if not. - // First let's get the basic type. - isSlice, isStruct := false, false - ft := field.Type - m := isTextUnmarshaler(reflect.Zero(ft)) - if ft.Kind() == reflect.Ptr { - ft = ft.Elem() - } - if isSlice = ft.Kind() == reflect.Slice; isSlice { - ft = ft.Elem() - if ft.Kind() == reflect.Ptr { - ft = ft.Elem() - } - } - if ft.Kind() == reflect.Array { - ft = ft.Elem() - if ft.Kind() == reflect.Ptr { - ft = ft.Elem() - } - } - if isStruct = ft.Kind() == reflect.Struct; !isStruct { - if c.converter(ft) == nil && builtinConverters[ft.Kind()] == nil { - // Type is not supported. - return nil - } - } - - return &fieldInfo{ - typ: field.Type, - name: field.Name, - alias: alias, - canonicalAlias: canonicalAlias, - unmarshalerInfo: m, - isSliceOfStructs: isSlice && isStruct, - isAnonymous: field.Anonymous, - isRequired: options.Contains("required"), - } -} - -// converter returns the converter for a type. -func (c *cache) converter(t reflect.Type) Converter { - return c.regconv[t] -} - -// ---------------------------------------------------------------------------- - -type structInfo struct { - fields []*fieldInfo -} - -func (i *structInfo) get(alias string) *fieldInfo { - for _, field := range i.fields { - if strings.EqualFold(field.alias, alias) { - return field - } - } - return nil -} - -func containsAlias(infos []*structInfo, alias string) bool { - for _, info := range infos { - if info.get(alias) != nil { - return true - } - } - return false -} - -type fieldInfo struct { - typ reflect.Type - // name is the field name in the struct. - name string - alias string - // canonicalAlias is almost the same as the alias, but is prefixed with - // an embedded struct field alias in dotted notation if this field is - // promoted from the struct. - // For instance, if the alias is "N" and this field is an embedded field - // in a struct "X", canonicalAlias will be "X.N". - canonicalAlias string - // unmarshalerInfo contains information regarding the - // encoding.TextUnmarshaler implementation of the field type. - unmarshalerInfo unmarshaler - // isSliceOfStructs indicates if the field type is a slice of structs. - isSliceOfStructs bool - // isAnonymous indicates whether the field is embedded in the struct. - isAnonymous bool - isRequired bool -} - -func (f *fieldInfo) paths(prefix string) []string { - if f.alias == f.canonicalAlias { - return []string{prefix + f.alias} - } - return []string{prefix + f.alias, prefix + f.canonicalAlias} -} - -type pathPart struct { - field *fieldInfo - path []string // path to the field: walks structs using field names. - index int // struct index in slices of structs. -} - -// ---------------------------------------------------------------------------- - -func indirectType(typ reflect.Type) reflect.Type { - if typ.Kind() == reflect.Ptr { - return typ.Elem() - } - return typ -} - -// fieldAlias parses a field tag to get a field alias. -func fieldAlias(field reflect.StructField, tagName string) (alias string, options tagOptions) { - if tag := field.Tag.Get(tagName); tag != "" { - alias, options = parseTag(tag) - } - if alias == "" { - alias = field.Name - } - return alias, options -} - -// tagOptions is the string following a comma in a struct field's tag, or -// the empty string. It does not include the leading comma. -type tagOptions []string - -// parseTag splits a struct field's url tag into its name and comma-separated -// options. -func parseTag(tag string) (string, tagOptions) { - s := strings.Split(tag, ",") - return s[0], s[1:] -} - -// Contains checks whether the tagOptions contains the specified option. -func (o tagOptions) Contains(option string) bool { - for _, s := range o { - if s == option { - return true - } - } - return false -} diff --git a/internal/schema/converter.go b/internal/schema/converter.go deleted file mode 100644 index 4f2116a15e..0000000000 --- a/internal/schema/converter.go +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright 2012 The Gorilla Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package schema - -import ( - "reflect" - "strconv" -) - -type Converter func(string) reflect.Value - -var ( - invalidValue = reflect.Value{} - boolType = reflect.Bool - float32Type = reflect.Float32 - float64Type = reflect.Float64 - intType = reflect.Int - int8Type = reflect.Int8 - int16Type = reflect.Int16 - int32Type = reflect.Int32 - int64Type = reflect.Int64 - stringType = reflect.String - uintType = reflect.Uint - uint8Type = reflect.Uint8 - uint16Type = reflect.Uint16 - uint32Type = reflect.Uint32 - uint64Type = reflect.Uint64 -) - -// Default converters for basic types. -var builtinConverters = map[reflect.Kind]Converter{ - boolType: convertBool, - float32Type: convertFloat32, - float64Type: convertFloat64, - intType: convertInt, - int8Type: convertInt8, - int16Type: convertInt16, - int32Type: convertInt32, - int64Type: convertInt64, - stringType: convertString, - uintType: convertUint, - uint8Type: convertUint8, - uint16Type: convertUint16, - uint32Type: convertUint32, - uint64Type: convertUint64, -} - -func convertBool(value string) reflect.Value { - if value == "on" { - return reflect.ValueOf(true) - } else if v, err := strconv.ParseBool(value); err == nil { - return reflect.ValueOf(v) - } - return invalidValue -} - -func convertFloat32(value string) reflect.Value { - if v, err := strconv.ParseFloat(value, 32); err == nil { - return reflect.ValueOf(float32(v)) - } - return invalidValue -} - -func convertFloat64(value string) reflect.Value { - if v, err := strconv.ParseFloat(value, 64); err == nil { - return reflect.ValueOf(v) - } - return invalidValue -} - -func convertInt(value string) reflect.Value { - if v, err := strconv.ParseInt(value, 10, 0); err == nil { - return reflect.ValueOf(int(v)) - } - return invalidValue -} - -func convertInt8(value string) reflect.Value { - if v, err := strconv.ParseInt(value, 10, 8); err == nil { - return reflect.ValueOf(int8(v)) - } - return invalidValue -} - -func convertInt16(value string) reflect.Value { - if v, err := strconv.ParseInt(value, 10, 16); err == nil { - return reflect.ValueOf(int16(v)) - } - return invalidValue -} - -func convertInt32(value string) reflect.Value { - if v, err := strconv.ParseInt(value, 10, 32); err == nil { - return reflect.ValueOf(int32(v)) - } - return invalidValue -} - -func convertInt64(value string) reflect.Value { - if v, err := strconv.ParseInt(value, 10, 64); err == nil { - return reflect.ValueOf(v) - } - return invalidValue -} - -func convertString(value string) reflect.Value { - return reflect.ValueOf(value) -} - -func convertUint(value string) reflect.Value { - if v, err := strconv.ParseUint(value, 10, 0); err == nil { - return reflect.ValueOf(uint(v)) - } - return invalidValue -} - -func convertUint8(value string) reflect.Value { - if v, err := strconv.ParseUint(value, 10, 8); err == nil { - return reflect.ValueOf(uint8(v)) - } - return invalidValue -} - -func convertUint16(value string) reflect.Value { - if v, err := strconv.ParseUint(value, 10, 16); err == nil { - return reflect.ValueOf(uint16(v)) - } - return invalidValue -} - -func convertUint32(value string) reflect.Value { - if v, err := strconv.ParseUint(value, 10, 32); err == nil { - return reflect.ValueOf(uint32(v)) - } - return invalidValue -} - -func convertUint64(value string) reflect.Value { - if v, err := strconv.ParseUint(value, 10, 64); err == nil { - return reflect.ValueOf(v) - } - return invalidValue -} diff --git a/internal/schema/decoder.go b/internal/schema/decoder.go deleted file mode 100644 index 310b783e38..0000000000 --- a/internal/schema/decoder.go +++ /dev/null @@ -1,534 +0,0 @@ -// Copyright 2012 The Gorilla Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package schema - -import ( - "encoding" - "errors" - "fmt" - "reflect" - "strings" -) - -// NewDecoder returns a new Decoder. -func NewDecoder() *Decoder { - return &Decoder{cache: newCache()} -} - -// Decoder decodes values from a map[string][]string to a struct. -type Decoder struct { - cache *cache - zeroEmpty bool - ignoreUnknownKeys bool -} - -// SetAliasTag changes the tag used to locate custom field aliases. -// The default tag is "schema". -func (d *Decoder) SetAliasTag(tag string) { - d.cache.tag = tag -} - -// ZeroEmpty controls the behaviour when the decoder encounters empty values -// in a map. -// If z is true and a key in the map has the empty string as a value -// then the corresponding struct field is set to the zero value. -// If z is false then empty strings are ignored. -// -// The default value is false, that is empty values do not change -// the value of the struct field. -func (d *Decoder) ZeroEmpty(z bool) { - d.zeroEmpty = z -} - -// IgnoreUnknownKeys controls the behaviour when the decoder encounters unknown -// keys in the map. -// If i is true and an unknown field is encountered, it is ignored. This is -// similar to how unknown keys are handled by encoding/json. -// If i is false then Decode will return an error. Note that any valid keys -// will still be decoded in to the target struct. -// -// To preserve backwards compatibility, the default value is false. -func (d *Decoder) IgnoreUnknownKeys(i bool) { - d.ignoreUnknownKeys = i -} - -// RegisterConverter registers a converter function for a custom type. -func (d *Decoder) RegisterConverter(value any, converterFunc Converter) { - d.cache.registerConverter(value, converterFunc) -} - -// Decode decodes a map[string][]string to a struct. -// -// The first parameter must be a pointer to a struct. -// -// The second parameter is a map, typically url.Values from an HTTP request. -// Keys are "paths" in dotted notation to the struct fields and nested structs. -// -// See the package documentation for a full explanation of the mechanics. -func (d *Decoder) Decode(dst any, src map[string][]string) error { - v := reflect.ValueOf(dst) - if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { - return errors.New("schema: interface must be a pointer to struct") - } - v = v.Elem() - t := v.Type() - multiError := MultiError{} - for path, values := range src { - if parts, err := d.cache.parsePath(path, t); err == nil { - if err = d.decode(v, path, parts, values); err != nil { - multiError[path] = err - } - } else if !d.ignoreUnknownKeys { - multiError[path] = UnknownKeyError{Key: path} - } - } - multiError.merge(d.checkRequired(t, src)) - if len(multiError) > 0 { - return multiError - } - return nil -} - -// checkRequired checks whether required fields are empty -// -// check type t recursively if t has struct fields. -// -// src is the source map for decoding, we use it here to see if those required fields are included in src -func (d *Decoder) checkRequired(t reflect.Type, src map[string][]string) MultiError { - m, errs := d.findRequiredFields(t, "", "") - for key, fields := range m { - if isEmptyFields(fields, src) { - errs[key] = EmptyFieldError{Key: key} - } - } - return errs -} - -// findRequiredFields recursively searches the struct type t for required fields. -// -// canonicalPrefix and searchPrefix are used to resolve full paths in dotted notation -// for nested struct fields. canonicalPrefix is a complete path which never omits -// any embedded struct fields. searchPrefix is a user-friendly path which may omit -// some embedded struct fields to point promoted fields. -func (d *Decoder) findRequiredFields(t reflect.Type, canonicalPrefix, searchPrefix string) (map[string][]fieldWithPrefix, MultiError) { - struc := d.cache.get(t) - if struc == nil { - // unexpect, cache.get never return nil - return nil, MultiError{canonicalPrefix + "*": errors.New("cache fail")} - } - - m := map[string][]fieldWithPrefix{} - errs := MultiError{} - for _, f := range struc.fields { - if f.typ.Kind() == reflect.Struct { - fcprefix := canonicalPrefix + f.canonicalAlias + "." - for _, fspath := range f.paths(searchPrefix) { - fm, ferrs := d.findRequiredFields(f.typ, fcprefix, fspath+".") - for key, fields := range fm { - m[key] = append(m[key], fields...) - } - errs.merge(ferrs) - } - } - if f.isRequired { - key := canonicalPrefix + f.canonicalAlias - m[key] = append(m[key], fieldWithPrefix{ - fieldInfo: f, - prefix: searchPrefix, - }) - } - } - return m, errs -} - -type fieldWithPrefix struct { - *fieldInfo - prefix string -} - -// isEmptyFields returns true if all of specified fields are empty. -func isEmptyFields(fields []fieldWithPrefix, src map[string][]string) bool { - for _, f := range fields { - for _, path := range f.paths(f.prefix) { - v, ok := src[path] - if ok && !isEmpty(f.typ, v) { - return false - } - for key := range src { - // issue references: - // https://github.com/gofiber/fiber/issues/1414 - // https://github.com/gorilla/schema/issues/176 - nested := strings.IndexByte(key, '.') != -1 - - // for non required nested structs - c1 := strings.HasSuffix(f.prefix, ".") && key == path - - // for required nested structs - c2 := f.prefix == "" && nested && strings.HasPrefix(key, path) - - // for non nested fields - c3 := f.prefix == "" && !nested && key == path - if !isEmpty(f.typ, src[key]) && (c1 || c2 || c3) { - return false - } - } - } - } - return true -} - -// isEmpty returns true if value is empty for specific type -func isEmpty(t reflect.Type, value []string) bool { - if len(value) == 0 { - return true - } - switch t.Kind() { - case boolType, float32Type, float64Type, intType, int8Type, int32Type, int64Type, stringType, uint8Type, uint16Type, uint32Type, uint64Type: - return len(value[0]) == 0 - } - return false -} - -// decode fills a struct field using a parsed path. -func (d *Decoder) decode(v reflect.Value, path string, parts []pathPart, values []string) error { - // Get the field walking the struct fields by index. - for _, name := range parts[0].path { - if v.Type().Kind() == reflect.Ptr { - if v.IsNil() { - v.Set(reflect.New(v.Type().Elem())) - } - v = v.Elem() - } - - // alloc embedded structs - if v.Type().Kind() == reflect.Struct { - for i := 0; i < v.NumField(); i++ { - field := v.Field(i) - if field.Type().Kind() == reflect.Ptr && field.IsNil() && v.Type().Field(i).Anonymous { - field.Set(reflect.New(field.Type().Elem())) - } - } - } - - v = v.FieldByName(name) - } - // Don't even bother for unexported fields. - if !v.CanSet() { - return nil - } - - // Dereference if needed. - t := v.Type() - if t.Kind() == reflect.Ptr { - t = t.Elem() - if v.IsNil() { - v.Set(reflect.New(t)) - } - v = v.Elem() - } - - // Slice of structs. Let's go recursive. - if len(parts) > 1 { - idx := parts[0].index - if v.IsNil() || v.Len() < idx+1 { - value := reflect.MakeSlice(t, idx+1, idx+1) - if v.Len() < idx+1 { - // Resize it. - reflect.Copy(value, v) - } - v.Set(value) - } - return d.decode(v.Index(idx), path, parts[1:], values) - } - - // Get the converter early in case there is one for a slice type. - conv := d.cache.converter(t) - m := isTextUnmarshaler(v) - if conv == nil && t.Kind() == reflect.Slice && m.IsSliceElement { - var items []reflect.Value - elemT := t.Elem() - isPtrElem := elemT.Kind() == reflect.Ptr - if isPtrElem { - elemT = elemT.Elem() - } - - // Try to get a converter for the element type. - conv := d.cache.converter(elemT) - if conv == nil { - conv = builtinConverters[elemT.Kind()] - if conv == nil { - // As we are not dealing with slice of structs here, we don't need to check if the type - // implements TextUnmarshaler interface - return fmt.Errorf("schema: converter not found for %v", elemT) - } - } - - for key, value := range values { - if value == "" { - if d.zeroEmpty { - items = append(items, reflect.Zero(elemT)) - } - } else if m.IsValid { - u := reflect.New(elemT) - if m.IsSliceElementPtr { - u = reflect.New(reflect.PtrTo(elemT).Elem()) - } - if err := u.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value)); err != nil { - return ConversionError{ - Key: path, - Type: t, - Index: key, - Err: err, - } - } - if m.IsSliceElementPtr { - items = append(items, u.Elem().Addr()) - } else if u.Kind() == reflect.Ptr { - items = append(items, u.Elem()) - } else { - items = append(items, u) - } - } else if item := conv(value); item.IsValid() { - if isPtrElem { - ptr := reflect.New(elemT) - ptr.Elem().Set(item) - item = ptr - } - if item.Type() != elemT && !isPtrElem { - item = item.Convert(elemT) - } - items = append(items, item) - } else { - if strings.Contains(value, ",") { - values := strings.Split(value, ",") - for _, value := range values { - if value == "" { - if d.zeroEmpty { - items = append(items, reflect.Zero(elemT)) - } - } else if item := conv(value); item.IsValid() { - if isPtrElem { - ptr := reflect.New(elemT) - ptr.Elem().Set(item) - item = ptr - } - if item.Type() != elemT && !isPtrElem { - item = item.Convert(elemT) - } - items = append(items, item) - } else { - return ConversionError{ - Key: path, - Type: elemT, - Index: key, - } - } - } - } else { - return ConversionError{ - Key: path, - Type: elemT, - Index: key, - } - } - } - } - value := reflect.Append(reflect.MakeSlice(t, 0, 0), items...) - v.Set(value) - } else { - val := "" - // Use the last value provided if any values were provided - if len(values) > 0 { - val = values[len(values)-1] - } - - if conv != nil { - if value := conv(val); value.IsValid() { - v.Set(value.Convert(t)) - } else { - return ConversionError{ - Key: path, - Type: t, - Index: -1, - } - } - } else if m.IsValid { - if m.IsPtr { - u := reflect.New(v.Type()) - if err := u.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(val)); err != nil { - return ConversionError{ - Key: path, - Type: t, - Index: -1, - Err: err, - } - } - v.Set(reflect.Indirect(u)) - } else { - // If the value implements the encoding.TextUnmarshaler interface - // apply UnmarshalText as the converter - if err := m.Unmarshaler.UnmarshalText([]byte(val)); err != nil { - return ConversionError{ - Key: path, - Type: t, - Index: -1, - Err: err, - } - } - } - } else if val == "" { - if d.zeroEmpty { - v.Set(reflect.Zero(t)) - } - } else if conv := builtinConverters[t.Kind()]; conv != nil { - if value := conv(val); value.IsValid() { - v.Set(value.Convert(t)) - } else { - return ConversionError{ - Key: path, - Type: t, - Index: -1, - } - } - } else { - return fmt.Errorf("schema: converter not found for %v", t) - } - } - return nil -} - -func isTextUnmarshaler(v reflect.Value) unmarshaler { - // Create a new unmarshaller instance - m := unmarshaler{} - if m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler); m.IsValid { - return m - } - // As the UnmarshalText function should be applied to the pointer of the - // type, we check that type to see if it implements the necessary - // method. - if m.Unmarshaler, m.IsValid = reflect.New(v.Type()).Interface().(encoding.TextUnmarshaler); m.IsValid { - m.IsPtr = true - return m - } - - // if v is []T or *[]T create new T - t := v.Type() - if t.Kind() == reflect.Ptr { - t = t.Elem() - } - if t.Kind() == reflect.Slice { - // Check if the slice implements encoding.TextUnmarshaller - if m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler); m.IsValid { - return m - } - // If t is a pointer slice, check if its elements implement - // encoding.TextUnmarshaler - m.IsSliceElement = true - if t = t.Elem(); t.Kind() == reflect.Ptr { - t = reflect.PtrTo(t.Elem()) - v = reflect.Zero(t) - m.IsSliceElementPtr = true - m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler) - return m - } - } - - v = reflect.New(t) - m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler) - return m -} - -// TextUnmarshaler helpers ---------------------------------------------------- -// unmarshaller contains information about a TextUnmarshaler type -type unmarshaler struct { - Unmarshaler encoding.TextUnmarshaler - // IsValid indicates whether the resolved type indicated by the other - // flags implements the encoding.TextUnmarshaler interface. - IsValid bool - // IsPtr indicates that the resolved type is the pointer of the original - // type. - IsPtr bool - // IsSliceElement indicates that the resolved type is a slice element of - // the original type. - IsSliceElement bool - // IsSliceElementPtr indicates that the resolved type is a pointer to a - // slice element of the original type. - IsSliceElementPtr bool -} - -// Errors --------------------------------------------------------------------- - -// ConversionError stores information about a failed conversion. -type ConversionError struct { - Type reflect.Type // expected type of elem - Err error // low-level error (when it exists) - Key string // key from the source map. - Index int // index for multi-value fields; -1 for single-value fields. -} - -func (e ConversionError) Error() string { - var output string - - if e.Index < 0 { - output = fmt.Sprintf("schema: error converting value for %q", e.Key) - } else { - output = fmt.Sprintf("schema: error converting value for index %d of %q", - e.Index, e.Key) - } - - if e.Err != nil { - output = fmt.Sprintf("%s. Details: %s", output, e.Err) - } - - return output -} - -// UnknownKeyError stores information about an unknown key in the source map. -type UnknownKeyError struct { - Key string // key from the source map. -} - -func (e UnknownKeyError) Error() string { - return fmt.Sprintf("schema: invalid path %q", e.Key) -} - -// EmptyFieldError stores information about an empty required field. -type EmptyFieldError struct { - Key string // required key in the source map. -} - -func (e EmptyFieldError) Error() string { - return fmt.Sprintf("%v is empty", e.Key) -} - -// MultiError stores multiple decoding errors. -// -// Borrowed from the App Engine SDK. -type MultiError map[string]error - -func (e MultiError) Error() string { - s := "" - for _, err := range e { - s = err.Error() - break - } - switch len(e) { - case 0: - return "(0 errors)" - case 1: - return s - case 2: - return s + " (and 1 other error)" - } - return fmt.Sprintf("%s (and %d other errors)", s, len(e)-1) -} - -func (e MultiError) merge(errors MultiError) { - for key, err := range errors { - if e[key] == nil { - e[key] = err - } - } -} diff --git a/internal/schema/doc.go b/internal/schema/doc.go deleted file mode 100644 index fff0fe7616..0000000000 --- a/internal/schema/doc.go +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright 2012 The Gorilla Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -/* -Package gorilla/schema fills a struct with form values. - -The basic usage is really simple. Given this struct: - - type Person struct { - Name string - Phone string - } - -...we can fill it passing a map to the Decode() function: - - values := map[string][]string{ - "Name": {"John"}, - "Phone": {"999-999-999"}, - } - person := new(Person) - decoder := schema.NewDecoder() - decoder.Decode(person, values) - -This is just a simple example and it doesn't make a lot of sense to create -the map manually. Typically it will come from a http.Request object and -will be of type url.Values, http.Request.Form, or http.Request.MultipartForm: - - func MyHandler(w http.ResponseWriter, r *http.Request) { - err := r.ParseForm() - - if err != nil { - // Handle error - } - - decoder := schema.NewDecoder() - // r.PostForm is a map of our POST form values - err := decoder.Decode(person, r.PostForm) - - if err != nil { - // Handle error - } - - // Do something with person.Name or person.Phone - } - -Note: it is a good idea to set a Decoder instance as a package global, -because it caches meta-data about structs, and an instance can be shared safely: - - var decoder = schema.NewDecoder() - -To define custom names for fields, use a struct tag "schema". To not populate -certain fields, use a dash for the name and it will be ignored: - - type Person struct { - Name string `schema:"name"` // custom name - Phone string `schema:"phone"` // custom name - Admin bool `schema:"-"` // this field is never set - } - -The supported field types in the destination struct are: - - - bool - - float variants (float32, float64) - - int variants (int, int8, int16, int32, int64) - - string - - uint variants (uint, uint8, uint16, uint32, uint64) - - struct - - a pointer to one of the above types - - a slice or a pointer to a slice of one of the above types - -Non-supported types are simply ignored, however custom types can be registered -to be converted. - -To fill nested structs, keys must use a dotted notation as the "path" for the -field. So for example, to fill the struct Person below: - - type Phone struct { - Label string - Number string - } - - type Person struct { - Name string - Phone Phone - } - -...the source map must have the keys "Name", "Phone.Label" and "Phone.Number". -This means that an HTML form to fill a Person struct must look like this: - -
- - - -
- -Single values are filled using the first value for a key from the source map. -Slices are filled using all values for a key from the source map. So to fill -a Person with multiple Phone values, like: - - type Person struct { - Name string - Phones []Phone - } - -...an HTML form that accepts three Phone values would look like this: - -
- - - - - - - -
- -Notice that only for slices of structs the slice index is required. -This is needed for disambiguation: if the nested struct also had a slice -field, we could not translate multiple values to it if we did not use an -index for the parent struct. - -There's also the possibility to create a custom type that implements the -TextUnmarshaler interface, and in this case there's no need to register -a converter, like: - - type Person struct { - Emails []Email - } - - type Email struct { - *mail.Address - } - - func (e *Email) UnmarshalText(text []byte) (err error) { - e.Address, err = mail.ParseAddress(string(text)) - return - } - -...an HTML form that accepts three Email values would look like this: - -
- - - -
-*/ -package schema diff --git a/internal/schema/encoder.go b/internal/schema/encoder.go deleted file mode 100644 index 849d7c0fec..0000000000 --- a/internal/schema/encoder.go +++ /dev/null @@ -1,202 +0,0 @@ -package schema - -import ( - "errors" - "fmt" - "reflect" - "strconv" -) - -type encoderFunc func(reflect.Value) string - -// Encoder encodes values from a struct into url.Values. -type Encoder struct { - cache *cache - regenc map[reflect.Type]encoderFunc -} - -// NewEncoder returns a new Encoder with defaults. -func NewEncoder() *Encoder { - return &Encoder{cache: newCache(), regenc: make(map[reflect.Type]encoderFunc)} -} - -// Encode encodes a struct into map[string][]string. -// -// Intended for use with url.Values. -func (e *Encoder) Encode(src any, dst map[string][]string) error { - v := reflect.ValueOf(src) - - return e.encode(v, dst) -} - -// RegisterEncoder registers a converter for encoding a custom type. -func (e *Encoder) RegisterEncoder(value any, encoder func(reflect.Value) string) { - e.regenc[reflect.TypeOf(value)] = encoder -} - -// SetAliasTag changes the tag used to locate custom field aliases. -// The default tag is "schema". -func (e *Encoder) SetAliasTag(tag string) { - e.cache.tag = tag -} - -// isValidStructPointer test if input value is a valid struct pointer. -func isValidStructPointer(v reflect.Value) bool { - return v.Type().Kind() == reflect.Ptr && v.Elem().IsValid() && v.Elem().Type().Kind() == reflect.Struct -} - -func isZero(v reflect.Value) bool { - switch v.Kind() { - case reflect.Func: - case reflect.Map, reflect.Slice: - return v.IsNil() || v.Len() == 0 - case reflect.Array: - z := true - for i := 0; i < v.Len(); i++ { - z = z && isZero(v.Index(i)) - } - return z - case reflect.Struct: - type zero interface { - IsZero() bool - } - if v.Type().Implements(reflect.TypeOf((*zero)(nil)).Elem()) { - iz := v.MethodByName("IsZero").Call([]reflect.Value{})[0] - return iz.Interface().(bool) - } - z := true - for i := 0; i < v.NumField(); i++ { - z = z && isZero(v.Field(i)) - } - return z - } - // Compare other types directly: - z := reflect.Zero(v.Type()) - return v.Interface() == z.Interface() -} - -func (e *Encoder) encode(v reflect.Value, dst map[string][]string) error { - if v.Kind() == reflect.Ptr { - v = v.Elem() - } - if v.Kind() != reflect.Struct { - return errors.New("schema: interface must be a struct") - } - t := v.Type() - - errors := MultiError{} - - for i := 0; i < v.NumField(); i++ { - name, opts := fieldAlias(t.Field(i), e.cache.tag) - if name == "-" { - continue - } - - // Encode struct pointer types if the field is a valid pointer and a struct. - if isValidStructPointer(v.Field(i)) { - _ = e.encode(v.Field(i).Elem(), dst) - continue - } - - encFunc := typeEncoder(v.Field(i).Type(), e.regenc) - - // Encode non-slice types and custom implementations immediately. - if encFunc != nil { - value := encFunc(v.Field(i)) - if opts.Contains("omitempty") && isZero(v.Field(i)) { - continue - } - - dst[name] = append(dst[name], value) - continue - } - - if v.Field(i).Type().Kind() == reflect.Struct { - _ = e.encode(v.Field(i), dst) - continue - } - - if v.Field(i).Type().Kind() == reflect.Slice { - encFunc = typeEncoder(v.Field(i).Type().Elem(), e.regenc) - } - - if encFunc == nil { - errors[v.Field(i).Type().String()] = fmt.Errorf("schema: encoder not found for %v", v.Field(i)) - continue - } - - // Encode a slice. - if v.Field(i).Len() == 0 && opts.Contains("omitempty") { - continue - } - - dst[name] = []string{} - for j := 0; j < v.Field(i).Len(); j++ { - dst[name] = append(dst[name], encFunc(v.Field(i).Index(j))) - } - } - - if len(errors) > 0 { - return errors - } - return nil -} - -func typeEncoder(t reflect.Type, reg map[reflect.Type]encoderFunc) encoderFunc { - if f, ok := reg[t]; ok { - return f - } - - switch t.Kind() { - case reflect.Bool: - return encodeBool - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return encodeInt - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - return encodeUint - case reflect.Float32: - return encodeFloat32 - case reflect.Float64: - return encodeFloat64 - case reflect.Ptr: - f := typeEncoder(t.Elem(), reg) - return func(v reflect.Value) string { - if v.IsNil() { - return "null" - } - return f(v.Elem()) - } - case reflect.String: - return encodeString - default: - return nil - } -} - -func encodeBool(v reflect.Value) string { - return strconv.FormatBool(v.Bool()) -} - -func encodeInt(v reflect.Value) string { - return strconv.FormatInt(int64(v.Int()), 10) -} - -func encodeUint(v reflect.Value) string { - return strconv.FormatUint(uint64(v.Uint()), 10) -} - -func encodeFloat(v reflect.Value, bits int) string { - return strconv.FormatFloat(v.Float(), 'f', 6, bits) -} - -func encodeFloat32(v reflect.Value) string { - return encodeFloat(v, 32) -} - -func encodeFloat64(v reflect.Value) string { - return encodeFloat(v, 64) -} - -func encodeString(v reflect.Value) string { - return v.String() -} From 079d301c5006f633066eb0b59e8250a818cb802f Mon Sep 17 00:00:00 2001 From: Aaron Zingerle Date: Fri, 11 Oct 2024 14:02:36 +0200 Subject: [PATCH 06/31] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20Middleware/CORS=20R?= =?UTF-8?q?emove=20Scheme=20Restriction=20(#3163)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🩹 Fix: middleware/cors remove scheme restriction (gofiber#3160) Co-authored-by: Aaron Zingerle Co-authored-by: M. Efe Çetin --- middleware/cors/utils.go | 5 ----- middleware/cors/utils_test.go | 1 + 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/middleware/cors/utils.go b/middleware/cors/utils.go index f5338dccc6..66ed9248ff 100644 --- a/middleware/cors/utils.go +++ b/middleware/cors/utils.go @@ -37,11 +37,6 @@ func normalizeOrigin(origin string) (bool, string) { return false, "" } - // Validate the scheme is either http or https - if parsedOrigin.Scheme != "http" && parsedOrigin.Scheme != "https" { - return false, "" - } - // Don't allow a wildcard with a protocol // wildcards cannot be used within any other value. For example, the following header is not valid: // Access-Control-Allow-Origin: https://* diff --git a/middleware/cors/utils_test.go b/middleware/cors/utils_test.go index 84f217e5d1..3fc4853558 100644 --- a/middleware/cors/utils_test.go +++ b/middleware/cors/utils_test.go @@ -17,6 +17,7 @@ func Test_NormalizeOrigin(t *testing.T) { {origin: "http://example.com/", expectedValid: true, expectedOrigin: "http://example.com"}, // Trailing slash should be removed. {origin: "http://example.com:3000", expectedValid: true, expectedOrigin: "http://example.com:3000"}, // Port should be preserved. {origin: "http://example.com:3000/", expectedValid: true, expectedOrigin: "http://example.com:3000"}, // Trailing slash should be removed. + {origin: "app://example.com/", expectedValid: true, expectedOrigin: "app://example.com"}, // App scheme should be accepted. {origin: "http://", expectedValid: false, expectedOrigin: ""}, // Invalid origin should not be accepted. {origin: "file:///etc/passwd", expectedValid: false, expectedOrigin: ""}, // File scheme should not be accepted. {origin: "https://*example.com", expectedValid: false, expectedOrigin: ""}, // Wildcard domain should not be accepted. From 72e97c555d4af4edf113a68d65e88971d041b17e Mon Sep 17 00:00:00 2001 From: Santiago Diaz Date: Fri, 11 Oct 2024 22:56:05 -0300 Subject: [PATCH 07/31] fix: typo in hooks documentation --- docs/api/hooks.md | 4 ++-- hooks.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/api/hooks.md b/docs/api/hooks.md index 2ef3a4619a..828d68a359 100644 --- a/docs/api/hooks.md +++ b/docs/api/hooks.md @@ -34,7 +34,7 @@ type OnMountHandler = func(*App) error ## OnRoute -OnRoute is a hook to execute user functions on each route registeration. Also you can get route properties by **route** parameter. +OnRoute is a hook to execute user functions on each route registration. Also you can get route properties by **route** parameter. ```go title="Signature" func (h *Hooks) OnRoute(handler ...OnRouteHandler) @@ -104,7 +104,7 @@ func main() { ## OnGroup -OnGroup is a hook to execute user functions on each group registeration. Also you can get group properties by **group** parameter. +OnGroup is a hook to execute user functions on each group registration. Also you can get group properties by **group** parameter. ```go title="Signature" func (h *Hooks) OnGroup(handler ...OnGroupHandler) diff --git a/hooks.go b/hooks.go index 37310a4a16..3da5c671ff 100644 --- a/hooks.go +++ b/hooks.go @@ -53,7 +53,7 @@ func newHooks(app *App) *Hooks { } } -// OnRoute is a hook to execute user functions on each route registeration. +// OnRoute is a hook to execute user functions on each route registration. // Also you can get route properties by route parameter. func (h *Hooks) OnRoute(handler ...OnRouteHandler) { h.app.mutex.Lock() @@ -71,7 +71,7 @@ func (h *Hooks) OnName(handler ...OnNameHandler) { h.app.mutex.Unlock() } -// OnGroup is a hook to execute user functions on each group registeration. +// OnGroup is a hook to execute user functions on each group registration. // Also you can get group properties by group parameter. func (h *Hooks) OnGroup(handler ...OnGroupHandler) { h.app.mutex.Lock() From 9dd3d94ff24735c4e018e0b94db5bba22d8f45f3 Mon Sep 17 00:00:00 2001 From: Maria Niranjan <130844959+s19835@users.noreply.github.com> Date: Sat, 12 Oct 2024 19:35:23 +0530 Subject: [PATCH 08/31] Update README.md (#3165) Missing a pointer reference when passing the context object in the route handler function. In Fiber, the context (c) is a pointer, so it should be *fiber.Ctx instead of fiber.Ctx. --- .github/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/README.md b/.github/README.md index 68fbd9c9bf..dbfee36383 100644 --- a/.github/README.md +++ b/.github/README.md @@ -73,7 +73,7 @@ func main() { app := fiber.New() // Define a route for the GET method on the root path '/' - app.Get("/", func(c fiber.Ctx) error { + app.Get("/", func(c *fiber.Ctx) error { // Send a string response to the client return c.SendString("Hello, World 👋!") }) From 7b3a36f22fc1166ceb9cb78cf69b3a2f95d077da Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Sun, 13 Oct 2024 05:04:02 -0400 Subject: [PATCH 09/31] Revert "Update README.md (#3165)" (#3166) This reverts commit 9dd3d94ff24735c4e018e0b94db5bba22d8f45f3. --- .github/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/README.md b/.github/README.md index dbfee36383..68fbd9c9bf 100644 --- a/.github/README.md +++ b/.github/README.md @@ -73,7 +73,7 @@ func main() { app := fiber.New() // Define a route for the GET method on the root path '/' - app.Get("/", func(c *fiber.Ctx) error { + app.Get("/", func(c fiber.Ctx) error { // Send a string response to the client return c.SendString("Hello, World 👋!") }) From 298975a98242ca66a0689175e9c8f7b43e9409c5 Mon Sep 17 00:00:00 2001 From: xEricL <37921711+xEricL@users.noreply.github.com> Date: Thu, 17 Oct 2024 02:29:03 -0400 Subject: [PATCH 10/31] =?UTF-8?q?=F0=9F=94=A5Feature:=20Add=20support=20fo?= =?UTF-8?q?r=20TrustProxy=20(#3170)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔥 Feature: Add `TrustProxyConfig` and rename `EnableTrustedProxyCheck` to `TrustProxy` * 📚 Doc: Document TrustProxyConfig usage and migration * 🚨 Test: Validate and Benchmark use of TrustProxyConfig * 🩹 Fix: typo in RequestMethods docstring * 🩹 Fix: typos in TrustProxy docstring and JSON tags * 🩹 Fix: Move `TrustProxyConfig.Loopback` to beginning of if-statement * 🎨 Style: Cleanup spacing for Test_Ctx_IsProxyTrusted * 📚 Doc: Replace `whitelist` with `allowlist` for clarity * 📚 Doc: Improve `TrustProxy` doc wording * 🩹 Fix: validate IP addresses in `App.handleTrustedProxy` * 🩹 Fix: grammatical errors and capitalize "TLS" --- .github/README.md | 6 +- app.go | 73 +++-- ctx.go | 24 +- ctx_interface_gen.go | 10 +- ctx_test.go | 416 ++++++++++++++++++++----- docs/api/ctx.md | 15 +- docs/api/fiber.md | 78 ++--- docs/middleware/cors.md | 6 +- docs/middleware/earlydata.md | 2 +- docs/whats_new.md | 31 ++ middleware/cors/config.go | 2 +- middleware/earlydata/earlydata_test.go | 14 +- 12 files changed, 510 insertions(+), 167 deletions(-) diff --git a/.github/README.md b/.github/README.md index 68fbd9c9bf..61159412e8 100644 --- a/.github/README.md +++ b/.github/README.md @@ -561,8 +561,10 @@ import ( func main() { app := fiber.New(fiber.Config{ - EnableTrustedProxyCheck: true, - TrustedProxies: []string{"0.0.0.0", "1.1.1.1/30"}, // IP address or IP address range + TrustProxy: true, + TrustProxyConfig: fiber.TrustProxyConfig{ + Proxies: []string{"0.0.0.0", "1.1.1.1/30"}, // IP address or IP address range + }, ProxyHeader: fiber.HeaderXForwardedFor, }) diff --git a/app.go b/app.go index e0240d3c16..38a3d17319 100644 --- a/app.go +++ b/app.go @@ -330,29 +330,31 @@ type Config struct { //nolint:govet // Aligning the struct fields is not necessa // For example, the Host HTTP header is usually used to return the requested host. // But when you’re behind a proxy, the actual host may be stored in an X-Forwarded-Host header. // - // If you are behind a proxy, you should enable TrustedProxyCheck to prevent header spoofing. - // If you enable EnableTrustedProxyCheck and leave TrustedProxies empty Fiber will skip + // If you are behind a proxy, you should enable TrustProxy to prevent header spoofing. + // If you enable TrustProxy and do not provide a TrustProxyConfig, Fiber will skip // all headers that could be spoofed. - // If request ip in TrustedProxies whitelist then: + // If the request IP is in the TrustProxyConfig.Proxies allowlist, then: // 1. c.Scheme() get value from X-Forwarded-Proto, X-Forwarded-Protocol, X-Forwarded-Ssl or X-Url-Scheme header // 2. c.IP() get value from ProxyHeader header. // 3. c.Host() and c.Hostname() get value from X-Forwarded-Host header - // But if request ip NOT in Trusted Proxies whitelist then: - // 1. c.Scheme() WON't get value from X-Forwarded-Proto, X-Forwarded-Protocol, X-Forwarded-Ssl or X-Url-Scheme header, - // will return https in case when tls connection is handled by the app, of http otherwise + // But if the request IP is NOT in the TrustProxyConfig.Proxies allowlist, then: + // 1. c.Scheme() WON'T get value from X-Forwarded-Proto, X-Forwarded-Protocol, X-Forwarded-Ssl or X-Url-Scheme header, + // will return https when a TLS connection is handled by the app, or http otherwise. // 2. c.IP() WON'T get value from ProxyHeader header, will return RemoteIP() from fasthttp context // 3. c.Host() and c.Hostname() WON'T get value from X-Forwarded-Host header, fasthttp.Request.URI().Host() // will be used to get the hostname. // + // To automatically trust all loopback, link-local, or private IP addresses, + // without manually adding them to the TrustProxyConfig.Proxies allowlist, + // you can set TrustProxyConfig.Loopback, TrustProxyConfig.LinkLocal, or TrustProxyConfig.Private to true. + // // Default: false - EnableTrustedProxyCheck bool `json:"enable_trusted_proxy_check"` + TrustProxy bool `json:"trust_proxy"` - // Read EnableTrustedProxyCheck doc. + // Read TrustProxy doc. // - // Default: []string - TrustedProxies []string `json:"trusted_proxies"` - trustedProxiesMap map[string]struct{} - trustedProxyRanges []*net.IPNet + // Default: DefaultTrustProxyConfig + TrustProxyConfig TrustProxyConfig `json:"trust_proxy_config"` // If set to true, c.IP() and c.IPs() will validate IP addresses before returning them. // Also, c.IP() will return only the first valid IP rather than just the raw header @@ -372,7 +374,7 @@ type Config struct { //nolint:govet // Aligning the struct fields is not necessa // Default: nil StructValidator StructValidator - // RequestMethods provides customizibility for HTTP methods. You can add/remove methods as you wish. + // RequestMethods provides customizability for HTTP methods. You can add/remove methods as you wish. // // Optional. Default: DefaultMethods RequestMethods []string @@ -385,6 +387,36 @@ type Config struct { //nolint:govet // Aligning the struct fields is not necessa EnableSplittingOnParsers bool `json:"enable_splitting_on_parsers"` } +// Default TrustProxyConfig +var DefaultTrustProxyConfig = TrustProxyConfig{} + +// TrustProxyConfig is a struct for configuring trusted proxies if Config.TrustProxy is true. +type TrustProxyConfig struct { + ips map[string]struct{} + + // Proxies is a list of trusted proxy IP addresses or CIDR ranges. + // + // Default: []string + Proxies []string `json:"proxies"` + + ranges []*net.IPNet + + // LinkLocal enables trusting all link-local IP ranges (e.g., 169.254.0.0/16, fe80::/10). + // + // Default: false + LinkLocal bool `json:"link_local"` + + // Loopback enables trusting all loopback IP ranges (e.g., 127.0.0.0/8, ::1/128). + // + // Default: false + Loopback bool `json:"loopback"` + + // Private enables trusting all private IP ranges (e.g., 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7). + // + // Default: false + Private bool `json:"private"` +} + // RouteMessage is some message need to be print when server starts type RouteMessage struct { name string @@ -510,8 +542,8 @@ func New(config ...Config) *App { app.config.RequestMethods = DefaultMethods } - app.config.trustedProxiesMap = make(map[string]struct{}, len(app.config.TrustedProxies)) - for _, ipAddress := range app.config.TrustedProxies { + app.config.TrustProxyConfig.ips = make(map[string]struct{}, len(app.config.TrustProxyConfig.Proxies)) + for _, ipAddress := range app.config.TrustProxyConfig.Proxies { app.handleTrustedProxy(ipAddress) } @@ -529,17 +561,22 @@ func New(config ...Config) *App { return app } -// Adds an ip address to trustedProxyRanges or trustedProxiesMap based on whether it is an IP range or not +// Adds an ip address to TrustProxyConfig.ranges or TrustProxyConfig.ips based on whether it is an IP range or not func (app *App) handleTrustedProxy(ipAddress string) { if strings.Contains(ipAddress, "/") { _, ipNet, err := net.ParseCIDR(ipAddress) if err != nil { log.Warnf("IP range %q could not be parsed: %v", ipAddress, err) } else { - app.config.trustedProxyRanges = append(app.config.trustedProxyRanges, ipNet) + app.config.TrustProxyConfig.ranges = append(app.config.TrustProxyConfig.ranges, ipNet) } } else { - app.config.trustedProxiesMap[ipAddress] = struct{}{} + ip := net.ParseIP(ipAddress) + if ip == nil { + log.Warnf("IP address %q could not be parsed", ipAddress) + } else { + app.config.TrustProxyConfig.ips[ipAddress] = struct{}{} + } } } diff --git a/ctx.go b/ctx.go index 607a678ff8..9e61d0903e 100644 --- a/ctx.go +++ b/ctx.go @@ -155,7 +155,7 @@ type TLSHandler struct { // GetClientInfo Callback function to set ClientHelloInfo // Must comply with the method structure of https://cs.opensource.google/go/go/+/refs/tags/go1.20:src/crypto/tls/common.go;l=554-563 -// Since we overlay the method of the tls config in the listener method +// Since we overlay the method of the TLS config in the listener method func (t *TLSHandler) GetClientInfo(info *tls.ClientHelloInfo) (*tls.Certificate, error) { t.clientHelloInfo = info return nil, nil //nolint:nilnil // Not returning anything useful here is probably fine @@ -684,7 +684,7 @@ func (c *DefaultCtx) GetReqHeaders() map[string][]string { // while `Hostname` refers specifically to the name assigned to a device on a network, excluding any port information. // Example: URL: https://example.com:8080 -> Host: example.com:8080 // Make copies or use the Immutable setting instead. -// Please use Config.EnableTrustedProxyCheck to prevent header spoofing, in case when your app is behind the proxy. +// Please use Config.TrustProxy to prevent header spoofing, in case when your app is behind the proxy. func (c *DefaultCtx) Host() string { if c.IsProxyTrusted() { if host := c.Get(HeaderXForwardedHost); len(host) > 0 { @@ -702,7 +702,7 @@ func (c *DefaultCtx) Host() string { // Returned value is only valid within the handler. Do not store any references. // Example: URL: https://example.com:8080 -> Hostname: example.com // Make copies or use the Immutable setting instead. -// Please use Config.EnableTrustedProxyCheck to prevent header spoofing, in case when your app is behind the proxy. +// Please use Config.TrustProxy to prevent header spoofing, in case when your app is behind the proxy. func (c *DefaultCtx) Hostname() string { addr, _ := parseAddr(c.Host()) @@ -720,7 +720,7 @@ func (c *DefaultCtx) Port() string { // IP returns the remote IP address of the request. // If ProxyHeader and IP Validation is configured, it will parse that header and return the first valid IP address. -// Please use Config.EnableTrustedProxyCheck to prevent header spoofing, in case when your app is behind the proxy. +// Please use Config.TrustProxy to prevent header spoofing, in case when your app is behind the proxy. func (c *DefaultCtx) IP() string { if c.IsProxyTrusted() && len(c.app.config.ProxyHeader) > 0 { return c.extractIPFromHeader(c.app.config.ProxyHeader) @@ -1116,7 +1116,7 @@ func (c *DefaultCtx) Path(override ...string) string { } // Scheme contains the request protocol string: http or https for TLS requests. -// Please use Config.EnableTrustedProxyCheck to prevent header spoofing, in case when your app is behind the proxy. +// Please use Config.TrustProxy to prevent header spoofing, in case when your app is behind the proxy. func (c *DefaultCtx) Scheme() string { if c.fasthttp.IsTLS() { return schemeHTTPS @@ -1819,20 +1819,26 @@ func (c *DefaultCtx) configDependentPaths() { } // IsProxyTrusted checks trustworthiness of remote ip. -// If EnableTrustedProxyCheck false, it returns true +// If Config.TrustProxy false, it returns true // IsProxyTrusted can check remote ip by proxy ranges and ip map. func (c *DefaultCtx) IsProxyTrusted() bool { - if !c.app.config.EnableTrustedProxyCheck { + if !c.app.config.TrustProxy { return true } ip := c.fasthttp.RemoteIP() - if _, trusted := c.app.config.trustedProxiesMap[ip.String()]; trusted { + if (c.app.config.TrustProxyConfig.Loopback && ip.IsLoopback()) || + (c.app.config.TrustProxyConfig.Private && ip.IsPrivate()) || + (c.app.config.TrustProxyConfig.LinkLocal && ip.IsLinkLocalUnicast()) { return true } - for _, ipNet := range c.app.config.trustedProxyRanges { + if _, trusted := c.app.config.TrustProxyConfig.ips[ip.String()]; trusted { + return true + } + + for _, ipNet := range c.app.config.TrustProxyConfig.ranges { if ipNet.Contains(ip) { return true } diff --git a/ctx_interface_gen.go b/ctx_interface_gen.go index 62f2d368ad..aa317ecb00 100644 --- a/ctx_interface_gen.go +++ b/ctx_interface_gen.go @@ -127,19 +127,19 @@ type Ctx interface { // while `Hostname` refers specifically to the name assigned to a device on a network, excluding any port information. // Example: URL: https://example.com:8080 -> Host: example.com:8080 // Make copies or use the Immutable setting instead. - // Please use Config.EnableTrustedProxyCheck to prevent header spoofing, in case when your app is behind the proxy. + // Please use Config.TrustProxy to prevent header spoofing, in case when your app is behind the proxy. Host() string // Hostname contains the hostname derived from the X-Forwarded-Host or Host HTTP header using the c.Host() method. // Returned value is only valid within the handler. Do not store any references. // Example: URL: https://example.com:8080 -> Hostname: example.com // Make copies or use the Immutable setting instead. - // Please use Config.EnableTrustedProxyCheck to prevent header spoofing, in case when your app is behind the proxy. + // Please use Config.TrustProxy to prevent header spoofing, in case when your app is behind the proxy. Hostname() string // Port returns the remote port of the request. Port() string // IP returns the remote IP address of the request. // If ProxyHeader and IP Validation is configured, it will parse that header and return the first valid IP address. - // Please use Config.EnableTrustedProxyCheck to prevent header spoofing, in case when your app is behind the proxy. + // Please use Config.TrustProxy to prevent header spoofing, in case when your app is behind the proxy. IP() string // extractIPsFromHeader will return a slice of IPs it found given a header name in the order they appear. // When IP validation is enabled, any invalid IPs will be omitted. @@ -209,7 +209,7 @@ type Ctx interface { // Optionally, you could override the path. Path(override ...string) string // Scheme contains the request protocol string: http or https for TLS requests. - // Please use Config.EnableTrustedProxyCheck to prevent header spoofing, in case when your app is behind the proxy. + // Please use Config.TrustProxy to prevent header spoofing, in case when your app is behind the proxy. Scheme() string // Protocol returns the HTTP protocol of request: HTTP/1.1 and HTTP/2. Protocol() string @@ -315,7 +315,7 @@ type Ctx interface { // here the features for caseSensitive, decoded paths, strict paths are evaluated configDependentPaths() // IsProxyTrusted checks trustworthiness of remote ip. - // If EnableTrustedProxyCheck false, it returns true + // If Config.TrustProxy false, it returns true // IsProxyTrusted can check remote ip by proxy ranges and ip map. IsProxyTrusted() bool // IsFromLocal will return true if request came from local. diff --git a/ctx_test.go b/ctx_test.go index a94e4cb42b..eef29a97ad 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -1572,7 +1572,7 @@ func Test_Ctx_Host_UntrustedProxy(t *testing.T) { t.Parallel() // Don't trust any proxy { - app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{}}) + app := New(Config{TrustProxy: true, TrustProxyConfig: TrustProxyConfig{Proxies: []string{}}}) c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().SetRequestURI("http://google.com/test") c.Request().Header.Set(HeaderXForwardedHost, "google1.com") @@ -1581,7 +1581,7 @@ func Test_Ctx_Host_UntrustedProxy(t *testing.T) { } // Trust to specific proxy list { - app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.8.0.0", "0.8.0.1"}}) + app := New(Config{TrustProxy: true, TrustProxyConfig: TrustProxyConfig{Proxies: []string{"0.8.0.0", "0.8.0.1"}}}) c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().SetRequestURI("http://google.com/test") c.Request().Header.Set(HeaderXForwardedHost, "google1.com") @@ -1594,7 +1594,7 @@ func Test_Ctx_Host_UntrustedProxy(t *testing.T) { func Test_Ctx_Host_TrustedProxy(t *testing.T) { t.Parallel() { - app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.0.0.0", "0.8.0.1"}}) + app := New(Config{TrustProxy: true, TrustProxyConfig: TrustProxyConfig{Proxies: []string{"0.0.0.0", "0.8.0.1"}}}) c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().SetRequestURI("http://google.com/test") c.Request().Header.Set(HeaderXForwardedHost, "google1.com") @@ -1607,7 +1607,7 @@ func Test_Ctx_Host_TrustedProxy(t *testing.T) { func Test_Ctx_Host_TrustedProxyRange(t *testing.T) { t.Parallel() - app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.0.0.0/30"}}) + app := New(Config{TrustProxy: true, TrustProxyConfig: TrustProxyConfig{Proxies: []string{"0.0.0.0/30"}}}) c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().SetRequestURI("http://google.com/test") c.Request().Header.Set(HeaderXForwardedHost, "google1.com") @@ -1619,7 +1619,7 @@ func Test_Ctx_Host_TrustedProxyRange(t *testing.T) { func Test_Ctx_Host_UntrustedProxyRange(t *testing.T) { t.Parallel() - app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"1.0.0.0/30"}}) + app := New(Config{TrustProxy: true, TrustProxyConfig: TrustProxyConfig{Proxies: []string{"1.0.0.0/30"}}}) c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().SetRequestURI("http://google.com/test") c.Request().Header.Set(HeaderXForwardedHost, "google1.com") @@ -1653,7 +1653,7 @@ func Test_Ctx_IsProxyTrusted(t *testing.T) { } { app := New(Config{ - EnableTrustedProxyCheck: false, + TrustProxy: false, }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) require.True(t, c.IsProxyTrusted()) @@ -1661,26 +1661,26 @@ func Test_Ctx_IsProxyTrusted(t *testing.T) { { app := New(Config{ - EnableTrustedProxyCheck: true, + TrustProxy: true, }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) require.False(t, c.IsProxyTrusted()) } { app := New(Config{ - EnableTrustedProxyCheck: true, - - TrustedProxies: []string{}, + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + Proxies: []string{}, + }, }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) require.False(t, c.IsProxyTrusted()) } { app := New(Config{ - EnableTrustedProxyCheck: true, - - TrustedProxies: []string{ - "127.0.0.1", + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + Proxies: []string{"127.0.0.1"}, }, }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) @@ -1688,10 +1688,9 @@ func Test_Ctx_IsProxyTrusted(t *testing.T) { } { app := New(Config{ - EnableTrustedProxyCheck: true, - - TrustedProxies: []string{ - "127.0.0.1/8", + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + Proxies: []string{"127.0.0.1/8"}, }, }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) @@ -1699,10 +1698,9 @@ func Test_Ctx_IsProxyTrusted(t *testing.T) { } { app := New(Config{ - EnableTrustedProxyCheck: true, - - TrustedProxies: []string{ - "0.0.0.0", + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + Proxies: []string{"0.0.0.0"}, }, }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) @@ -1710,10 +1708,9 @@ func Test_Ctx_IsProxyTrusted(t *testing.T) { } { app := New(Config{ - EnableTrustedProxyCheck: true, - - TrustedProxies: []string{ - "0.0.0.1/31", + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + Proxies: []string{"0.0.0.1/31"}, }, }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) @@ -1721,10 +1718,39 @@ func Test_Ctx_IsProxyTrusted(t *testing.T) { } { app := New(Config{ - EnableTrustedProxyCheck: true, - - TrustedProxies: []string{ - "0.0.0.1/31junk", + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + Proxies: []string{"0.0.0.1/31junk"}, + }, + }) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + require.False(t, c.IsProxyTrusted()) + } + { + app := New(Config{ + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + Private: true, + }, + }) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + require.False(t, c.IsProxyTrusted()) + } + { + app := New(Config{ + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + Loopback: true, + }, + }) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + require.False(t, c.IsProxyTrusted()) + } + { + app := New(Config{ + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + LinkLocal: true, }, }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) @@ -1758,7 +1784,10 @@ func Benchmark_Ctx_Hostname(b *testing.B) { } // Trust to specific proxy list { - app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.8.0.0", "0.8.0.1"}}) + app := New(Config{ + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{Proxies: []string{"0.8.0.0", "0.8.0.1"}}, + }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().SetRequestURI("http://google.com/test") c.Request().Header.Set(HeaderXForwardedHost, "google1.com") @@ -1771,7 +1800,10 @@ func Benchmark_Ctx_Hostname(b *testing.B) { func Test_Ctx_Hostname_TrustedProxy(t *testing.T) { t.Parallel() { - app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.0.0.0", "0.8.0.1"}}) + app := New(Config{ + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{Proxies: []string{"0.0.0.0", "0.8.0.1"}}, + }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().SetRequestURI("http://google.com/test") c.Request().Header.Set(HeaderXForwardedHost, "google1.com") @@ -1784,7 +1816,10 @@ func Test_Ctx_Hostname_TrustedProxy(t *testing.T) { func Test_Ctx_Hostname_TrustedProxy_Multiple(t *testing.T) { t.Parallel() { - app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.0.0.0", "0.8.0.1"}}) + app := New(Config{ + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{Proxies: []string{"0.0.0.0", "0.8.0.1"}}, + }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().SetRequestURI("http://google.com/test") c.Request().Header.Set(HeaderXForwardedHost, "google1.com, google2.com") @@ -1797,7 +1832,10 @@ func Test_Ctx_Hostname_TrustedProxy_Multiple(t *testing.T) { func Test_Ctx_Hostname_TrustedProxyRange(t *testing.T) { t.Parallel() - app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.0.0.0/30"}}) + app := New(Config{ + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{Proxies: []string{"0.0.0.0/30"}}, + }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().SetRequestURI("http://google.com/test") c.Request().Header.Set(HeaderXForwardedHost, "google1.com") @@ -1809,7 +1847,10 @@ func Test_Ctx_Hostname_TrustedProxyRange(t *testing.T) { func Test_Ctx_Hostname_UntrustedProxyRange(t *testing.T) { t.Parallel() - app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"1.0.0.0/30"}}) + app := New(Config{ + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{Proxies: []string{"1.0.0.0/30"}}, + }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().SetRequestURI("http://google.com/test") c.Request().Header.Set(HeaderXForwardedHost, "google1.com") @@ -1927,7 +1968,11 @@ func Test_Ctx_IP_ProxyHeader_With_IP_Validation(t *testing.T) { // go test -run Test_Ctx_IP_UntrustedProxy func Test_Ctx_IP_UntrustedProxy(t *testing.T) { t.Parallel() - app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.8.0.1"}, ProxyHeader: HeaderXForwardedFor}) + app := New(Config{ + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{Proxies: []string{"0.8.0.1"}}, + ProxyHeader: HeaderXForwardedFor, + }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().Header.Set(HeaderXForwardedFor, "0.0.0.1") require.Equal(t, "0.0.0.0", c.IP()) @@ -1936,7 +1981,11 @@ func Test_Ctx_IP_UntrustedProxy(t *testing.T) { // go test -run Test_Ctx_IP_TrustedProxy func Test_Ctx_IP_TrustedProxy(t *testing.T) { t.Parallel() - app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.0.0.0"}, ProxyHeader: HeaderXForwardedFor}) + app := New(Config{ + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{Proxies: []string{"0.0.0.0"}}, + ProxyHeader: HeaderXForwardedFor, + }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().Header.Set(HeaderXForwardedFor, "0.0.0.1") require.Equal(t, "0.0.0.1", c.IP()) @@ -2613,7 +2662,7 @@ func Benchmark_Ctx_Scheme(b *testing.B) { // go test -run Test_Ctx_Scheme_TrustedProxy func Test_Ctx_Scheme_TrustedProxy(t *testing.T) { t.Parallel() - app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.0.0.0"}}) + app := New(Config{TrustProxy: true, TrustProxyConfig: TrustProxyConfig{Proxies: []string{"0.0.0.0"}}}) c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().Header.Set(HeaderXForwardedProto, schemeHTTPS) @@ -2638,7 +2687,10 @@ func Test_Ctx_Scheme_TrustedProxy(t *testing.T) { // go test -run Test_Ctx_Scheme_TrustedProxyRange func Test_Ctx_Scheme_TrustedProxyRange(t *testing.T) { t.Parallel() - app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.0.0.0/30"}}) + app := New(Config{ + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{Proxies: []string{"0.0.0.0/30"}}, + }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().Header.Set(HeaderXForwardedProto, schemeHTTPS) @@ -2663,7 +2715,10 @@ func Test_Ctx_Scheme_TrustedProxyRange(t *testing.T) { // go test -run Test_Ctx_Scheme_UntrustedProxyRange func Test_Ctx_Scheme_UntrustedProxyRange(t *testing.T) { t.Parallel() - app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"1.1.1.1/30"}}) + app := New(Config{ + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{Proxies: []string{"1.1.1.1/30"}}, + }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().Header.Set(HeaderXForwardedProto, schemeHTTPS) @@ -2688,7 +2743,10 @@ func Test_Ctx_Scheme_UntrustedProxyRange(t *testing.T) { // go test -run Test_Ctx_Scheme_UnTrustedProxy func Test_Ctx_Scheme_UnTrustedProxy(t *testing.T) { t.Parallel() - app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.8.0.1"}}) + app := New(Config{ + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{Proxies: []string{"0.8.0.1"}}, + }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().Header.Set(HeaderXForwardedProto, schemeHTTPS) @@ -6173,7 +6231,7 @@ func Benchmark_Ctx_IsProxyTrusted(b *testing.B) { // Scenario with trusted proxy check simple b.Run("WithProxyCheckSimple", func(b *testing.B) { app := New(Config{ - EnableTrustedProxyCheck: true, + TrustProxy: true, }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().SetRequestURI("http://google.com/test") @@ -6189,7 +6247,7 @@ func Benchmark_Ctx_IsProxyTrusted(b *testing.B) { // Scenario with trusted proxy check simple in parallel b.Run("WithProxyCheckSimpleParallel", func(b *testing.B) { app := New(Config{ - EnableTrustedProxyCheck: true, + TrustProxy: true, }) b.ReportAllocs() b.ResetTimer() @@ -6207,8 +6265,10 @@ func Benchmark_Ctx_IsProxyTrusted(b *testing.B) { // Scenario with trusted proxy check b.Run("WithProxyCheck", func(b *testing.B) { app := New(Config{ - EnableTrustedProxyCheck: true, - TrustedProxies: []string{"0.0.0.0"}, + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + Proxies: []string{"0.0.0.0"}, + }, }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().SetRequestURI("http://google.com/test") @@ -6224,8 +6284,198 @@ func Benchmark_Ctx_IsProxyTrusted(b *testing.B) { // Scenario with trusted proxy check in parallel b.Run("WithProxyCheckParallel", func(b *testing.B) { app := New(Config{ - EnableTrustedProxyCheck: true, - TrustedProxies: []string{"0.0.0.0"}, + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + Proxies: []string{"0.0.0.0"}, + }, + }) + b.ReportAllocs() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().SetRequestURI("http://google.com/") + c.Request().Header.Set(HeaderXForwardedHost, "google1.com") + for pb.Next() { + c.IsProxyTrusted() + } + app.ReleaseCtx(c) + }) + }) + + // Scenario with trusted proxy check allow private + b.Run("WithProxyCheckAllowPrivate", func(b *testing.B) { + app := New(Config{ + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + Private: true, + }, + }) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().SetRequestURI("http://google.com/test") + c.Request().Header.Set(HeaderXForwardedHost, "google1.com") + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + c.IsProxyTrusted() + } + app.ReleaseCtx(c) + }) + + // Scenario with trusted proxy check allow private in parallel + b.Run("WithProxyCheckAllowPrivateParallel", func(b *testing.B) { + app := New(Config{ + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + Private: true, + }, + }) + b.ReportAllocs() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().SetRequestURI("http://google.com/") + c.Request().Header.Set(HeaderXForwardedHost, "google1.com") + for pb.Next() { + c.IsProxyTrusted() + } + app.ReleaseCtx(c) + }) + }) + + // Scenario with trusted proxy check allow private as subnets + b.Run("WithProxyCheckAllowPrivateAsSubnets", func(b *testing.B) { + app := New(Config{ + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + Proxies: []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "fc00::/7"}, + }, + }) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().SetRequestURI("http://google.com/test") + c.Request().Header.Set(HeaderXForwardedHost, "google1.com") + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + c.IsProxyTrusted() + } + app.ReleaseCtx(c) + }) + + // Scenario with trusted proxy check allow private as subnets in parallel + b.Run("WithProxyCheckAllowPrivateAsSubnetsParallel", func(b *testing.B) { + app := New(Config{ + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + Proxies: []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "fc00::/7"}, + }, + }) + b.ReportAllocs() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().SetRequestURI("http://google.com/") + c.Request().Header.Set(HeaderXForwardedHost, "google1.com") + for pb.Next() { + c.IsProxyTrusted() + } + app.ReleaseCtx(c) + }) + }) + + // Scenario with trusted proxy check allow private, loopback, and link-local + b.Run("WithProxyCheckAllowAll", func(b *testing.B) { + app := New(Config{ + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + Private: true, + Loopback: true, + LinkLocal: true, + }, + }) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().SetRequestURI("http://google.com/test") + c.Request().Header.Set(HeaderXForwardedHost, "google1.com") + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + c.IsProxyTrusted() + } + app.ReleaseCtx(c) + }) + + // Scenario with trusted proxy check allow private, loopback, and link-local in parallel + b.Run("WithProxyCheckAllowAllParallel", func(b *testing.B) { + app := New(Config{ + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + Private: true, + Loopback: true, + LinkLocal: true, + }, + }) + b.ReportAllocs() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().SetRequestURI("http://google.com/") + c.Request().Header.Set(HeaderXForwardedHost, "google1.com") + for pb.Next() { + c.IsProxyTrusted() + } + app.ReleaseCtx(c) + }) + }) + + // Scenario with trusted proxy check allow private, loopback, and link-local as subnets + b.Run("WithProxyCheckAllowAllowAllAsSubnets", func(b *testing.B) { + app := New(Config{ + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + Proxies: []string{ + // Link-local + "169.254.0.0/16", + "fe80::/10", + // Loopback + "127.0.0.0/8", + "::1/128", + // Private + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "fc00::/7", + }, + }, + }) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().SetRequestURI("http://google.com/test") + c.Request().Header.Set(HeaderXForwardedHost, "google1.com") + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + c.IsProxyTrusted() + } + app.ReleaseCtx(c) + }) + + // Scenario with trusted proxy check allow private, loopback, and link-local as subnets in parallel + b.Run("WithProxyCheckAllowAllowAllAsSubnetsParallel", func(b *testing.B) { + app := New(Config{ + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + Proxies: []string{ + // Link-local + "169.254.0.0/16", + "fe80::/10", + // Loopback + "127.0.0.0/8", + "::1/128", + // Private + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "fc00::/7", + }, + }, }) b.ReportAllocs() b.ResetTimer() @@ -6243,8 +6493,10 @@ func Benchmark_Ctx_IsProxyTrusted(b *testing.B) { // Scenario with trusted proxy check with subnet b.Run("WithProxyCheckSubnet", func(b *testing.B) { app := New(Config{ - EnableTrustedProxyCheck: true, - TrustedProxies: []string{"0.0.0.0/8"}, + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + Proxies: []string{"0.0.0.0/8"}, + }, }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().SetRequestURI("http://google.com/test") @@ -6260,8 +6512,10 @@ func Benchmark_Ctx_IsProxyTrusted(b *testing.B) { // Scenario with trusted proxy check with subnet in parallel b.Run("WithProxyCheckParallelSubnet", func(b *testing.B) { app := New(Config{ - EnableTrustedProxyCheck: true, - TrustedProxies: []string{"0.0.0.0/8"}, + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + Proxies: []string{"0.0.0.0/8"}, + }, }) b.ReportAllocs() b.ResetTimer() @@ -6279,8 +6533,10 @@ func Benchmark_Ctx_IsProxyTrusted(b *testing.B) { // Scenario with trusted proxy check with multiple subnet b.Run("WithProxyCheckMultipleSubnet", func(b *testing.B) { app := New(Config{ - EnableTrustedProxyCheck: true, - TrustedProxies: []string{"192.168.0.0/24", "10.0.0.0/16", "0.0.0.0/8"}, + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + Proxies: []string{"192.168.0.0/24", "10.0.0.0/16", "0.0.0.0/8"}, + }, }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().SetRequestURI("http://google.com/test") @@ -6296,8 +6552,10 @@ func Benchmark_Ctx_IsProxyTrusted(b *testing.B) { // Scenario with trusted proxy check with multiple subnet in parallel b.Run("WithProxyCheckParallelMultipleSubnet", func(b *testing.B) { app := New(Config{ - EnableTrustedProxyCheck: true, - TrustedProxies: []string{"192.168.0.0/24", "10.0.0.0/16", "0.0.0.0/8"}, + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + Proxies: []string{"192.168.0.0/24", "10.0.0.0/16", "0.0.0.0/8"}, + }, }) b.ReportAllocs() b.ResetTimer() @@ -6315,17 +6573,19 @@ func Benchmark_Ctx_IsProxyTrusted(b *testing.B) { // Scenario with trusted proxy check with all subnets b.Run("WithProxyCheckAllSubnets", func(b *testing.B) { app := New(Config{ - EnableTrustedProxyCheck: true, - TrustedProxies: []string{ - "127.0.0.0/8", // Loopback addresses - "169.254.0.0/16", // Link-Local addresses - "fe80::/10", // Link-Local addresses - "192.168.0.0/16", // Private Network addresses - "172.16.0.0/12", // Private Network addresses - "10.0.0.0/8", // Private Network addresses - "fc00::/7", // Unique Local addresses - "173.245.48.0/20", // My custom range - "0.0.0.0/8", // All IPv4 addresses + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + Proxies: []string{ + "127.0.0.0/8", // Loopback addresses + "169.254.0.0/16", // Link-Local addresses + "fe80::/10", // Link-Local addresses + "192.168.0.0/16", // Private Network addresses + "172.16.0.0/12", // Private Network addresses + "10.0.0.0/8", // Private Network addresses + "fc00::/7", // Unique Local addresses + "173.245.48.0/20", // My custom range + "0.0.0.0/8", // All IPv4 addresses + }, }, }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) @@ -6342,17 +6602,19 @@ func Benchmark_Ctx_IsProxyTrusted(b *testing.B) { // Scenario with trusted proxy check with all subnets in parallel b.Run("WithProxyCheckParallelAllSubnets", func(b *testing.B) { app := New(Config{ - EnableTrustedProxyCheck: true, - TrustedProxies: []string{ - "127.0.0.0/8", // Loopback addresses - "169.254.0.0/16", // Link-Local addresses - "fe80::/10", // Link-Local addresses - "192.168.0.0/16", // Private Network addresses - "172.16.0.0/12", // Private Network addresses - "10.0.0.0/8", // Private Network addresses - "fc00::/7", // Unique Local addresses - "173.245.48.0/20", // My custom range - "0.0.0.0/8", // All IPv4 addresses + TrustProxy: true, + TrustProxyConfig: TrustProxyConfig{ + Proxies: []string{ + "127.0.0.0/8", // Loopback addresses + "169.254.0.0/16", // Link-Local addresses + "fe80::/10", // Link-Local addresses + "192.168.0.0/16", // Private Network addresses + "172.16.0.0/12", // Private Network addresses + "10.0.0.0/8", // Private Network addresses + "fc00::/7", // Unique Local addresses + "173.245.48.0/20", // My custom range + "0.0.0.0/8", // All IPv4 addresses + }, }, }) b.ReportAllocs() diff --git a/docs/api/ctx.md b/docs/api/ctx.md index 00c2422cd9..b771d37eab 100644 --- a/docs/api/ctx.md +++ b/docs/api/ctx.md @@ -831,7 +831,7 @@ app.Get("/", func(c fiber.Ctx) error { ## IsProxyTrusted Checks trustworthiness of remote ip. -If [`EnableTrustedProxyCheck`](fiber.md#enabletrustedproxycheck) false, it returns true +If [`TrustProxy`](fiber.md#trustproxy) false, it returns true IsProxyTrusted can check remote ip by proxy ranges and ip map. ```go title="Signature" @@ -841,10 +841,13 @@ func (c Ctx) IsProxyTrusted() bool ```go title="Example" app := fiber.New(fiber.Config{ - // EnableTrustedProxyCheck enables the trusted proxy check - EnableTrustedProxyCheck: true, - // TrustedProxies is a list of trusted proxy IP addresses - TrustedProxies: []string{"0.8.0.0", "0.8.0.1"}, + // TrustProxy enables the trusted proxy check + TrustProxy: true, + // TrustProxyConfig allows for configuring trusted proxies. + // Proxies is a list of trusted proxy IP ranges/addresses + TrustProxyConfig: fiber.TrustProxyConfig{ + Proxies: []string{"0.8.0.0", "0.8.0.1"}, + } }) @@ -1640,7 +1643,7 @@ app.Post("/", func(c fiber.Ctx) error { Contains the request protocol string: http or https for TLS requests. :::info -Please use [`Config.EnableTrustedProxyCheck`](fiber.md#enabletrustedproxycheck) to prevent header spoofing, in case when your app is behind the proxy. +Please use [`Config.TrustProxy`](fiber.md#trustproxy) to prevent header spoofing, in case when your app is behind the proxy. ::: ```go title="Signature" diff --git a/docs/api/fiber.md b/docs/api/fiber.md index 16f23b9e61..6892225e11 100644 --- a/docs/api/fiber.md +++ b/docs/api/fiber.md @@ -42,45 +42,45 @@ app := fiber.New(fiber.Config{ #### Config fields -| Property | Type | Description | Default | -|---------------------------------------------------------------------------------------|-------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| -| AppName | `string` | This allows to setup app name for the app | `""` | -| BodyLimit | `int` | Sets the maximum allowed size for a request body, if the size exceeds the configured limit, it sends `413 - Request Entity Too Large` response. | `4 * 1024 * 1024` | -| CaseSensitive | `bool` | When enabled, `/Foo` and `/foo` are different routes. When disabled, `/Foo`and `/foo` are treated the same. | `false` | -| ColorScheme | [`Colors`](https://github.com/gofiber/fiber/blob/master/color.go) | You can define custom color scheme. They'll be used for startup message, route list and some middlewares. | [`DefaultColors`](https://github.com/gofiber/fiber/blob/master/color.go) | -| CompressedFileSuffixes | `map[string]string` | Adds a suffix to the original file name and tries saving the resulting compressed file under the new file name. | `{"gzip": ".fiber.gz", "br": ".fiber.br", "zstd": ".fiber.zst"}` | -| Concurrency | `int` | Maximum number of concurrent connections. | `256 * 1024` | -| DisableDefaultContentType | `bool` | When set to true, causes the default Content-Type header to be excluded from the Response. | `false` | -| DisableDefaultDate | `bool` | When set to true causes the default date header to be excluded from the response. | `false` | -| DisableHeaderNormalizing | `bool` | By default all header names are normalized: conteNT-tYPE -> Content-Type | `false` | -| DisableKeepalive | `bool` | Disable keep-alive connections, the server will close incoming connections after sending the first response to the client | `false` | -| DisablePreParseMultipartForm | `bool` | Will not pre parse Multipart Form data if set to true. This option is useful for servers that desire to treat multipart form data as a binary blob, or choose when to parse the data. | `false` | -| EnableIPValidation | `bool` | If set to true, `c.IP()` and `c.IPs()` will validate IP addresses before returning them. Also, `c.IP()` will return only the first valid IP rather than just the raw header value that may be a comma separated string.

**WARNING:** There is a small performance cost to doing this validation. Keep disabled if speed is your only concern and your application is behind a trusted proxy that already validates this header. | `false` | -| EnableSplittingOnParsers | `bool` | EnableSplittingOnParsers splits the query/body/header parameters by comma when it's true.

For example, you can use it to parse multiple values from a query parameter like this: `/api?foo=bar,baz == foo[]=bar&foo[]=baz` | `false` | -| EnableTrustedProxyCheck | `bool` | When set to true, fiber will check whether proxy is trusted, using TrustedProxies list.

By default `c.Protocol()` will get value from X-Forwarded-Proto, X-Forwarded-Protocol, X-Forwarded-Ssl or X-Url-Scheme header, `c.IP()` will get value from `ProxyHeader` header, `c.Hostname()` will get value from X-Forwarded-Host header.
If `EnableTrustedProxyCheck` is true, and `RemoteIP` is in the list of `TrustedProxies` `c.Protocol()`, `c.IP()`, and `c.Hostname()` will have the same behaviour when `EnableTrustedProxyCheck` disabled, if `RemoteIP` isn't in the list, `c.Protocol()` will return https in case when tls connection is handled by the app, or http otherwise, `c.IP()` will return RemoteIP() from fasthttp context, `c.Hostname()` will return `fasthttp.Request.URI().Host()` | `false` | -| ErrorHandler | `ErrorHandler` | ErrorHandler is executed when an error is returned from fiber.Handler. Mounted fiber error handlers are retained by the top-level app and applied on prefix associated requests. | `DefaultErrorHandler` | -| GETOnly | `bool` | Rejects all non-GET requests if set to true. This option is useful as anti-DoS protection for servers accepting only GET requests. The request size is limited by ReadBufferSize if GETOnly is set. | `false` | -| IdleTimeout | `time.Duration` | The maximum amount of time to wait for the next request when keep-alive is enabled. If IdleTimeout is zero, the value of ReadTimeout is used. | `nil` | -| Immutable | `bool` | When enabled, all values returned by context methods are immutable. By default, they are valid until you return from the handler; see issue [\#185](https://github.com/gofiber/fiber/issues/185). | `false` | -| JSONDecoder | `utils.JSONUnmarshal` | Allowing for flexibility in using another json library for decoding. | `json.Unmarshal` | -| JSONEncoder | `utils.JSONMarshal` | Allowing for flexibility in using another json library for encoding. | `json.Marshal` | -| PassLocalsToViews | `bool` | PassLocalsToViews Enables passing of the locals set on a fiber.Ctx to the template engine. See our **Template Middleware** for supported engines. | `false` | -| ProxyHeader | `string` | This will enable `c.IP()` to return the value of the given header key. By default `c.IP()`will return the Remote IP from the TCP connection, this property can be useful if you are behind a load balancer e.g. _X-Forwarded-\*_. | `""` | -| ReadBufferSize | `int` | per-connection buffer size for requests' reading. This also limits the maximum header size. Increase this buffer if your clients send multi-KB RequestURIs and/or multi-KB headers \(for example, BIG cookies\). | `4096` | -| ReadTimeout | `time.Duration` | The amount of time allowed to read the full request, including the body. The default timeout is unlimited. | `nil` | -| ReduceMemoryUsage | `bool` | Aggressively reduces memory usage at the cost of higher CPU usage if set to true. | `false` | -| RequestMethods | `[]string` | RequestMethods provides customizibility for HTTP methods. You can add/remove methods as you wish. | `DefaultMethods` | -| ServerHeader | `string` | Enables the `Server` HTTP header with the given value. | `""` | -| StreamRequestBody | `bool` | StreamRequestBody enables request body streaming, and calls the handler sooner when given body is larger than the current limit. | `false` | -| StrictRouting | `bool` | When enabled, the router treats `/foo` and `/foo/` as different. Otherwise, the router treats `/foo` and `/foo/` as the same. | `false` | -| StructValidator | `StructValidator` | If you want to validate header/form/query... automatically when to bind, you can define struct validator. Fiber doesn't have default validator, so it'll skip validator step if you don't use any validator. | `nil` | -| TrustedProxies | `[]string` | Contains the list of trusted proxy IP's. Look at `EnableTrustedProxyCheck` doc.

It can take IP or IP range addresses. | `nil` | -| UnescapePath | `bool` | Converts all encoded characters in the route back before setting the path for the context, so that the routing can also work with URL encoded special characters | `false` | -| Views | `Views` | Views is the interface that wraps the Render function. See our **Template Middleware** for supported engines. | `nil` | -| ViewsLayout | `string` | Views Layout is the global layout for all template render until override on Render function. See our **Template Middleware** for supported engines. | `""` | -| WriteBufferSize | `int` | Per-connection buffer size for responses' writing. | `4096` | -| WriteTimeout | `time.Duration` | The maximum duration before timing out writes of the response. The default timeout is unlimited. | `nil` | -| XMLEncoder | `utils.XMLMarshal` | Allowing for flexibility in using another XML library for encoding. | `xml.Marshal` | +| Property | Type | Description | Default | +|---------------------------------------------------------------------------------------|-------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| +| AppName | `string` | This allows to setup app name for the app | `""` | +| BodyLimit | `int` | Sets the maximum allowed size for a request body, if the size exceeds the configured limit, it sends `413 - Request Entity Too Large` response. | `4 * 1024 * 1024` | +| CaseSensitive | `bool` | When enabled, `/Foo` and `/foo` are different routes. When disabled, `/Foo`and `/foo` are treated the same. | `false` | +| ColorScheme | [`Colors`](https://github.com/gofiber/fiber/blob/master/color.go) | You can define custom color scheme. They'll be used for startup message, route list and some middlewares. | [`DefaultColors`](https://github.com/gofiber/fiber/blob/master/color.go) | +| CompressedFileSuffixes | `map[string]string` | Adds a suffix to the original file name and tries saving the resulting compressed file under the new file name. | `{"gzip": ".fiber.gz", "br": ".fiber.br", "zstd": ".fiber.zst"}` | +| Concurrency | `int` | Maximum number of concurrent connections. | `256 * 1024` | +| DisableDefaultContentType | `bool` | When set to true, causes the default Content-Type header to be excluded from the Response. | `false` | +| DisableDefaultDate | `bool` | When set to true causes the default date header to be excluded from the response. | `false` | +| DisableHeaderNormalizing | `bool` | By default all header names are normalized: conteNT-tYPE -> Content-Type | `false` | +| DisableKeepalive | `bool` | Disable keep-alive connections, the server will close incoming connections after sending the first response to the client | `false` | +| DisablePreParseMultipartForm | `bool` | Will not pre parse Multipart Form data if set to true. This option is useful for servers that desire to treat multipart form data as a binary blob, or choose when to parse the data. | `false` | +| EnableIPValidation | `bool` | If set to true, `c.IP()` and `c.IPs()` will validate IP addresses before returning them. Also, `c.IP()` will return only the first valid IP rather than just the raw header value that may be a comma separated string.

**WARNING:** There is a small performance cost to doing this validation. Keep disabled if speed is your only concern and your application is behind a trusted proxy that already validates this header. | `false` | +| EnableSplittingOnParsers | `bool` | EnableSplittingOnParsers splits the query/body/header parameters by comma when it's true.

For example, you can use it to parse multiple values from a query parameter like this: `/api?foo=bar,baz == foo[]=bar&foo[]=baz` | `false` | +| TrustProxy | `bool` | When set to true, fiber will check whether proxy is trusted, using TrustProxyConfig.Proxies list.

By default `c.Protocol()` will get value from X-Forwarded-Proto, X-Forwarded-Protocol, X-Forwarded-Ssl or X-Url-Scheme header, `c.IP()` will get value from `ProxyHeader` header, `c.Hostname()` will get value from X-Forwarded-Host header.
If `TrustProxy` is true, and `RemoteIP` is in the list of `TrustProxyConfig.Proxies` `c.Protocol()`, `c.IP()`, and `c.Hostname()` will have the same behaviour when `TrustProxy` disabled, if `RemoteIP` isn't in the list, `c.Protocol()` will return https when a TLS connection is handled by the app, or http otherwise, `c.IP()` will return RemoteIP() from fasthttp context, `c.Hostname()` will return `fasthttp.Request.URI().Host()` | `false` | +| ErrorHandler | `ErrorHandler` | ErrorHandler is executed when an error is returned from fiber.Handler. Mounted fiber error handlers are retained by the top-level app and applied on prefix associated requests. | `DefaultErrorHandler` | +| GETOnly | `bool` | Rejects all non-GET requests if set to true. This option is useful as anti-DoS protection for servers accepting only GET requests. The request size is limited by ReadBufferSize if GETOnly is set. | `false` | +| IdleTimeout | `time.Duration` | The maximum amount of time to wait for the next request when keep-alive is enabled. If IdleTimeout is zero, the value of ReadTimeout is used. | `nil` | +| Immutable | `bool` | When enabled, all values returned by context methods are immutable. By default, they are valid until you return from the handler; see issue [\#185](https://github.com/gofiber/fiber/issues/185). | `false` | +| JSONDecoder | `utils.JSONUnmarshal` | Allowing for flexibility in using another json library for decoding. | `json.Unmarshal` | +| JSONEncoder | `utils.JSONMarshal` | Allowing for flexibility in using another json library for encoding. | `json.Marshal` | +| PassLocalsToViews | `bool` | PassLocalsToViews Enables passing of the locals set on a fiber.Ctx to the template engine. See our **Template Middleware** for supported engines. | `false` | +| ProxyHeader | `string` | This will enable `c.IP()` to return the value of the given header key. By default `c.IP()`will return the Remote IP from the TCP connection, this property can be useful if you are behind a load balancer e.g. _X-Forwarded-\*_. | `""` | +| ReadBufferSize | `int` | per-connection buffer size for requests' reading. This also limits the maximum header size. Increase this buffer if your clients send multi-KB RequestURIs and/or multi-KB headers \(for example, BIG cookies\). | `4096` | +| ReadTimeout | `time.Duration` | The amount of time allowed to read the full request, including the body. The default timeout is unlimited. | `nil` | +| ReduceMemoryUsage | `bool` | Aggressively reduces memory usage at the cost of higher CPU usage if set to true. | `false` | +| RequestMethods | `[]string` | RequestMethods provides customizability for HTTP methods. You can add/remove methods as you wish. | `DefaultMethods` | +| ServerHeader | `string` | Enables the `Server` HTTP header with the given value. | `""` | +| StreamRequestBody | `bool` | StreamRequestBody enables request body streaming, and calls the handler sooner when given body is larger than the current limit. | `false` | +| StrictRouting | `bool` | When enabled, the router treats `/foo` and `/foo/` as different. Otherwise, the router treats `/foo` and `/foo/` as the same. | `false` | +| StructValidator | `StructValidator` | If you want to validate header/form/query... automatically when to bind, you can define struct validator. Fiber doesn't have default validator, so it'll skip validator step if you don't use any validator. | `nil` | +| TrustProxyConfig | `TrustProxyConfig` | Configure trusted proxy IP's. Look at `TrustProxy` doc.

`TrustProxyConfig.Proxies` can take IP or IP range addresses. | `nil` | +| UnescapePath | `bool` | Converts all encoded characters in the route back before setting the path for the context, so that the routing can also work with URL encoded special characters | `false` | +| Views | `Views` | Views is the interface that wraps the Render function. See our **Template Middleware** for supported engines. | `nil` | +| ViewsLayout | `string` | Views Layout is the global layout for all template render until override on Render function. See our **Template Middleware** for supported engines. | `""` | +| WriteBufferSize | `int` | Per-connection buffer size for responses' writing. | `4096` | +| WriteTimeout | `time.Duration` | The maximum duration before timing out writes of the response. The default timeout is unlimited. | `nil` | +| XMLEncoder | `utils.XMLMarshal` | Allowing for flexibility in using another XML library for encoding. | `xml.Marshal` | ## Server listening diff --git a/docs/middleware/cors.md b/docs/middleware/cors.md index 6c6d31cd80..1c5124a992 100644 --- a/docs/middleware/cors.md +++ b/docs/middleware/cors.md @@ -118,7 +118,7 @@ panic: [CORS] Configuration error: When 'AllowCredentials' is set to true, 'Allo | AllowOrigins | `[]string` | AllowOrigins defines a list of origins that may access the resource. This supports subdomain matching, so you can use a value like "https://*.example.com" to allow any subdomain of example.com to submit requests. If the special wildcard `"*"` is present in the list, all origins will be allowed. | `["*"]` | | AllowOriginsFunc | `func(origin string) bool` | `AllowOriginsFunc` is a function that dynamically determines whether to allow a request based on its origin. If this function returns `true`, the 'Access-Control-Allow-Origin' response header will be set to the request's 'origin' header. This function is only used if the request's origin doesn't match any origin in `AllowOrigins`. | `nil` | | AllowPrivateNetwork | `bool` | Indicates whether the `Access-Control-Allow-Private-Network` response header should be set to `true`, allowing requests from private networks. This aligns with modern security practices for web applications interacting with private networks. | `false` | -| ExposeHeaders | `string` | ExposeHeaders defines whitelist headers that clients are allowed to access. | `[]` | +| ExposeHeaders | `string` | ExposeHeaders defines an allowlist of headers that clients are allowed to access. | `[]` | | MaxAge | `int` | MaxAge indicates how long (in seconds) the results of a preflight request can be cached. If you pass MaxAge 0, the Access-Control-Max-Age header will not be added and the browser will use 5 seconds by default. To disable caching completely, pass MaxAge value negative. It will set the Access-Control-Max-Age header to 0. | `0` | | Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | @@ -191,7 +191,7 @@ The `AllowHeaders` option specifies which headers are allowed in the actual requ The `AllowCredentials` option indicates whether the response to the request can be exposed when the credentials flag is true. If `AllowCredentials` is set to `true`, the middleware adds the header `Access-Control-Allow-Credentials: true` to the response. To prevent security vulnerabilities, `AllowCredentials` cannot be set to `true` if `AllowOrigins` is set to a wildcard (`*`). -The `ExposeHeaders` option defines a whitelist of headers that clients are allowed to access. If `ExposeHeaders` is set to `"X-Custom-Header"`, the middleware adds the header `Access-Control-Expose-Headers: X-Custom-Header` to the response. +The `ExposeHeaders` option defines an allowlist of headers that clients are allowed to access. If `ExposeHeaders` is set to `"X-Custom-Header"`, the middleware adds the header `Access-Control-Expose-Headers: X-Custom-Header` to the response. The `MaxAge` option indicates how long the results of a preflight request can be cached. If `MaxAge` is set to `3600`, the middleware adds the header `Access-Control-Max-Age: 3600` to the response. @@ -207,7 +207,7 @@ When configuring CORS, misconfiguration can potentially expose your application - **Use Credentials Carefully**: If your application needs to support credentials in cross-origin requests, ensure `AllowCredentials` is set to `true` and specify exact origins in `AllowOrigins`. Do not use a wildcard origin in this case. -- **Limit Exposed Headers**: Only whitelist headers that are necessary for the client-side application by setting `ExposeHeaders` appropriately. This minimizes the risk of exposing sensitive information. +- **Limit Exposed Headers**: Only allowlist headers that are necessary for the client-side application by setting `ExposeHeaders` appropriately. This minimizes the risk of exposing sensitive information. ### Common Pitfalls diff --git a/docs/middleware/earlydata.md b/docs/middleware/earlydata.md index b0e39b2f2f..287b22e031 100644 --- a/docs/middleware/earlydata.md +++ b/docs/middleware/earlydata.md @@ -7,7 +7,7 @@ id: earlydata The Early Data middleware for [Fiber](https://github.com/gofiber/fiber) adds support for TLS 1.3's early data ("0-RTT") feature. Citing [RFC 8446](https://datatracker.ietf.org/doc/html/rfc8446#section-2-3), when a client and server share a PSK, TLS 1.3 allows clients to send data on the first flight ("early data") to speed up the request, effectively reducing the regular 1-RTT request to a 0-RTT request. -Make sure to enable fiber's `EnableTrustedProxyCheck` config option before using this middleware in order to not trust bogus HTTP request headers of the client. +Make sure to enable fiber's `TrustProxy` config option before using this middleware in order to not trust bogus HTTP request headers of the client. Also be aware that enabling support for early data in your reverse proxy (e.g. nginx, as done with a simple `ssl_early_data on;`) makes requests replayable. Refer to the following documents before continuing: diff --git a/docs/whats_new.md b/docs/whats_new.md index 6449d24292..e040f367d5 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -55,6 +55,8 @@ We have made several changes to the Fiber app, including: - EnablePrefork -> previously Prefork - EnablePrintRoutes - ListenerNetwork -> previously Network +- app.Config.EnabledTrustedProxyCheck -> has been moved to app.Config.TrustProxy + - TrustedProxies -> has been moved to TrustProxyConfig.Proxies ### new methods @@ -386,6 +388,35 @@ app.Get("*", static.New("./public/index.html")) You have to put `*` to the end of the route if you don't define static route with `app.Use`. ::: +#### Trusted Proxies + +We've renamed `EnableTrustedProxyCheck` to `TrustProxy` and moved `TrustedProxies` to `TrustProxyConfig`. + +```go +// Before +app := fiber.New(fiber.Config{ + // EnableTrustedProxyCheck enables the trusted proxy check. + EnableTrustedProxyCheck: true, + // TrustedProxies is a list of trusted proxy IP ranges/addresses. + TrustedProxies: []string{"0.8.0.0", "127.0.0.0/8", "::1/128"}, +}) +``` + +```go +// After +app := fiber.New(fiber.Config{ + // TrustProxy enables the trusted proxy check + TrustProxy: true, + // TrustProxyConfig allows for configuring trusted proxies. + TrustProxyConfig: fiber.TrustProxyConfig{ + // Proxies is a list of trusted proxy IP ranges/addresses. + Proxies: []string{"0.8.0.0"}, + // Trust all loop-back IP addresses (127.0.0.0/8, ::1/128) + Loopback: true, + } +}) +``` + ### 🗺 Router The signatures for [`Add`](#middleware-registration) and [`Route`](#route-chaining) have been changed. diff --git a/middleware/cors/config.go b/middleware/cors/config.go index 2613bab943..7432c4a9d5 100644 --- a/middleware/cors/config.go +++ b/middleware/cors/config.go @@ -41,7 +41,7 @@ type Config struct { // Optional. Default value []string{} AllowHeaders []string - // ExposeHeaders defines a whitelist headers that clients are allowed to + // ExposeHeaders defines an allowlist of headers that clients are allowed to // access. // // Optional. Default value []string{}. diff --git a/middleware/earlydata/earlydata_test.go b/middleware/earlydata/earlydata_test.go index 9a62bf2524..55e800ff2b 100644 --- a/middleware/earlydata/earlydata_test.go +++ b/middleware/earlydata/earlydata_test.go @@ -173,17 +173,19 @@ func Test_EarlyData(t *testing.T) { trustedRun(t, app) }) - t.Run("config with EnableTrustedProxyCheck", func(t *testing.T) { + t.Run("config with TrustProxy", func(t *testing.T) { app := appWithConfig(t, &fiber.Config{ - EnableTrustedProxyCheck: true, + TrustProxy: true, }) untrustedRun(t, app) }) - t.Run("config with EnableTrustedProxyCheck and trusted TrustedProxies", func(t *testing.T) { + t.Run("config with TrustProxy and trusted TrustProxyConfig.Proxies", func(t *testing.T) { app := appWithConfig(t, &fiber.Config{ - EnableTrustedProxyCheck: true, - TrustedProxies: []string{ - "0.0.0.0", + TrustProxy: true, + TrustProxyConfig: fiber.TrustProxyConfig{ + Proxies: []string{ + "0.0.0.0", + }, }, }) trustedRun(t, app) From 54714172d2a9b7c30411ad9ff34a54025ce18063 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 12:35:18 +0000 Subject: [PATCH 11/31] build(deps): bump github.com/gofiber/utils/v2 Bumps [github.com/gofiber/utils/v2](https://github.com/gofiber/utils) from 2.0.0-beta.6 to 2.0.0-beta.7. - [Release notes](https://github.com/gofiber/utils/releases) - [Commits](https://github.com/gofiber/utils/compare/v2.0.0-beta.6...v2.0.0-beta.7) --- updated-dependencies: - dependency-name: github.com/gofiber/utils/v2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8f3a2a43d3..2b5e60a1bf 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.22 require ( github.com/gofiber/schema v1.2.0 - github.com/gofiber/utils/v2 v2.0.0-beta.6 + github.com/gofiber/utils/v2 v2.0.0-beta.7 github.com/google/uuid v1.6.0 github.com/mattn/go-colorable v0.1.13 github.com/mattn/go-isatty v0.0.20 diff --git a/go.sum b/go.sum index 1d53c4560b..42768451af 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,12 @@ github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1 github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gofiber/schema v1.2.0 h1:j+ZRrNnUa/0ZuWrn/6kAtAufEr4jCJ+JuTURAMxNSZg= github.com/gofiber/schema v1.2.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c= -github.com/gofiber/utils/v2 v2.0.0-beta.6 h1:ED62bOmpRXdgviPlfTmf0Q+AXzhaTUAFtdWjgx+XkYI= -github.com/gofiber/utils/v2 v2.0.0-beta.6/go.mod h1:3Kz8Px3jInKFvqxDzDeoSygwEOO+3uyubTmUa6PqY+0= +github.com/gofiber/utils/v2 v2.0.0-beta.7 h1:NnHFrRHvhrufPABdWajcKZejz9HnCWmT/asoxRsiEbQ= +github.com/gofiber/utils/v2 v2.0.0-beta.7/go.mod h1:J/M03s+HMdZdvhAeyh76xT72IfVqBzuz/OJkrMa7cwU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= @@ -29,6 +31,8 @@ github.com/valyala/fasthttp v1.56.0 h1:bEZdJev/6LCBlpdORfrLu/WOZXXxvrUQSiyniuaoW github.com/valyala/fasthttp v1.56.0/go.mod h1:sReBt3XZVnudxuLOx4J/fMrJVorWRiWY2koQKgABiVI= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From 941ab1178915eeec1be39cc959bc3b65cf3e0d9d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:29:25 +0000 Subject: [PATCH 12/31] build(deps): bump benchmark-action/github-action-benchmark Bumps [benchmark-action/github-action-benchmark](https://github.com/benchmark-action/github-action-benchmark) from 1.20.3 to 1.20.4. - [Release notes](https://github.com/benchmark-action/github-action-benchmark/releases) - [Changelog](https://github.com/benchmark-action/github-action-benchmark/blob/master/CHANGELOG.md) - [Commits](https://github.com/benchmark-action/github-action-benchmark/compare/v1.20.3...v1.20.4) --- updated-dependencies: - dependency-name: benchmark-action/github-action-benchmark dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/benchmark.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index e8531e1cb8..075d1db6cc 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -56,7 +56,7 @@ jobs: # This will only run if we have Benchmark Results from main branch - name: Compare PR Benchmark Results with main branch - uses: benchmark-action/github-action-benchmark@v1.20.3 + uses: benchmark-action/github-action-benchmark@v1.20.4 if: steps.cache.outputs.cache-hit == 'true' with: tool: 'go' @@ -72,7 +72,7 @@ jobs: alert-threshold: "150%" - name: Store Benchmark Results for main branch - uses: benchmark-action/github-action-benchmark@v1.20.3 + uses: benchmark-action/github-action-benchmark@v1.20.4 if: ${{ github.ref_name == 'main' }} with: tool: 'go' @@ -86,7 +86,7 @@ jobs: alert-threshold: "150%" - name: Publish Benchmark Results to GitHub Pages - uses: benchmark-action/github-action-benchmark@v1.20.3 + uses: benchmark-action/github-action-benchmark@v1.20.4 if: ${{ github.ref_name == 'main' }} with: tool: 'go' From e3232c1505184ddae0c8666c65c08a51dc08d5ad Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Fri, 25 Oct 2024 03:36:30 -0300 Subject: [PATCH 13/31] feat!(middleware/session): re-write session middleware with handler (#3016) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat!(middleware/session): re-write session middleware with handler * test(middleware/session): refactor to IdleTimeout * fix: lint errors * test: Save session after setting or deleting raw data in CSRF middleware * Update middleware/session/middleware.go Co-authored-by: Renan Bastos * fix: mutex and globals order * feat: Re-Add read lock to session Get method * feat: Migrate New() to return middleware * chore: Refactor session middleware to improve session handling * chore: Private get on store * chore: Update session middleware to use saveSession instead of save * chore: Update session middleware to use getSession instead of get * chore: Remove unused error handler in session middleware config * chore: Update session middleware to use NewWithStore in CSRF tests * test: add test * fix: destroyed session and GHSA-98j2-3j3p-fw2v * chore: Refactor session_test.go to use newStore() instead of New() * feat: Improve session middleware test coverage and error handling This commit improves the session middleware test coverage by adding assertions for the presence of the Set-Cookie header and the token value. It also enhances error handling by checking for the expected number of parts in the Set-Cookie header. * chore: fix lint issues * chore: Fix session middleware locking issue and improve error handling * test: improve middleware test coverage and error handling * test: Add idle timeout test case to session middleware test * feat: add GetSession(id string) (*Session, error) * chore: lint * docs: Update session middleware docs * docs: Security Note to examples * docs: Add recommendation for CSRF protection in session middleware * chore: markdown lint * docs: Update session middleware docs * docs: makrdown lint * test(middleware/session): Add unit tests for session config.go * test(middleware/session): Add unit tests for store.go * test(middleware/session): Add data.go unit tests * refactor(middleware/session): session tests and add session release test - Refactor session tests to improve readability and maintainability. - Add a new test case to ensure proper session release functionality. - Update session.md * refactor: session data locking in middleware/session/data.go * refactor(middleware/session): Add unit test for session middleware store * test: fix session_test.go and store_test.go unit tests * refactor(docs): Update session.md with v3 changes to Expiration * refactor(middleware/session): Improve data pool handling and locking * chore(middleware/session): TODO for Expiration field in session config * refactor(middleware/session): Improve session data pool handling and locking * refactor(middleware/session): Improve session data pool handling and locking * test(middleware/csrf): add session middleware coverage * chroe(middleware/session): TODO for unregistered session middleware * refactor(middleware/session): Update session middleware for v3 changes * refactor(middleware/session): Update session middleware for v3 changes * refactor(middleware/session): Update session middleware idle timeout - Update the default idle timeout for session middleware from 24 hours to 30 minutes. - Add a note in the session middleware documentation about the importance of the middleware order. * docws(middleware/session): Add note about IdleTimeout requiring save using legacy approach * refactor(middleware/session): Update session middleware idle timeout Update the idle timeout for the session middleware to 30 minutes. This ensures that the session expires after a period of inactivity. The previous value was 24 hours, which is too long for most use cases. This change improves the security and efficiency of the session management. * docs(middleware/session): Update session middleware idle timeout and configuration * test(middleware/session): Fix tests for updated panics * refactor(middleware/session): Update session middleware initialization and saving * refactor(middleware/session): Remove unnecessary comment about negative IdleTimeout value * refactor(middleware/session): Update session middleware make NewStore public * refactor(middleware/session): Update session middleware Set, Get, and Delete methods Refactor the Set, Get, and Delete methods in the session middleware to use more descriptive parameter names. Instead of using "middlewareContextKey", the methods now use "key" to represent the key of the session value. This improves the readability and clarity of the code. * feat(middleware/session): AbsoluteTimeout and key any * fix(middleware/session): locking issues and lint errors * chore(middleware/session): Regenerate code in data_msgp.go * refactor(middleware/session): rename GetSessionByID to GetByID This commit also includes changes to the session_test.go and store_test.go files to add test cases for the new GetByID method. * docs(middleware/session): AbsoluteTimeout * refactor(middleware/csrf): Rename Expiration to IdleTimeout * docs(whats-new): CSRF Rename Expiration to IdleTimeout and remove SessionKey field * refactor(middleware/session): Rename expirationKeyType to absExpirationKeyType and update related functions * refactor(middleware/session): rename Test_Session_Save_Absolute to Test_Session_Save_AbsoluteTimeout * chore(middleware/session): update as per PR comments * docs(middlware/session): fix indent lint * fix(middleware/session): Address EfeCtn Comments * refactor(middleware/session): Move bytesBuffer to it's own pool * test(middleware/session): add decodeSessionData error coverage * refactor(middleware/session): Update absolute timeout handling - Update absolute timeout handling in getSession function - Set absolute expiration time in getSession function - Delete expired session in GetByID function * refactor(session/middleware): fix *Session nil ctx when using Store.GetByID * refactor(middleware/session): Remove unnecessary line in session_test.go * fix(middleware/session): *Session lifecycle issues * docs(middleware/session): Update GetByID method documentation * docs(middleware/session): Update GetByID method documentation * docs(middleware/session): markdown lint * refactor(middleware/session): Simplify error handling in DefaultErrorHandler * fix( middleware/session/config.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * add ctx releases for the test cases --------- Co-authored-by: Renan Bastos Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Co-authored-by: René --- docs/middleware/csrf.md | 15 +- docs/middleware/session.md | 505 +++++++++++++++++++++----- docs/whats_new.md | 35 +- middleware/csrf/config.go | 21 +- middleware/csrf/csrf.go | 11 +- middleware/csrf/csrf_test.go | 67 +++- middleware/csrf/session_manager.go | 80 ++-- middleware/session/config.go | 144 ++++++-- middleware/session/config_test.go | 59 +++ middleware/session/data.go | 97 ++++- middleware/session/data_msgp.go | 112 +----- middleware/session/data_test.go | 204 +++++++++++ middleware/session/middleware.go | 301 +++++++++++++++ middleware/session/middleware_test.go | 469 ++++++++++++++++++++++++ middleware/session/session.go | 353 ++++++++++++++---- middleware/session/session_test.go | 423 +++++++++++++++++++-- middleware/session/store.go | 209 ++++++++++- middleware/session/store_test.go | 117 +++++- 18 files changed, 2797 insertions(+), 425 deletions(-) create mode 100644 middleware/session/config_test.go create mode 100644 middleware/session/data_test.go create mode 100644 middleware/session/middleware.go create mode 100644 middleware/session/middleware_test.go diff --git a/docs/middleware/csrf.md b/docs/middleware/csrf.md index a034f9dfd7..8127432438 100644 --- a/docs/middleware/csrf.md +++ b/docs/middleware/csrf.md @@ -34,7 +34,7 @@ app.Use(csrf.New(csrf.Config{ KeyLookup: "header:X-Csrf-Token", CookieName: "csrf_", CookieSameSite: "Lax", - Expiration: 1 * time.Hour, + IdleTimeout: 30 * time.Minute, KeyGenerator: utils.UUIDv4, Extractor: func(c fiber.Ctx) (string, error) { ... }, })) @@ -106,15 +106,14 @@ func (h *Handler) DeleteToken(c fiber.Ctx) error | CookieSecure | `bool` | Indicates if the CSRF cookie is secure. | false | | CookieHTTPOnly | `bool` | Indicates if the CSRF cookie is HTTP-only. | false | | CookieSameSite | `string` | Value of SameSite cookie. | "Lax" | -| CookieSessionOnly | `bool` | Decides whether the cookie should last for only the browser session. Ignores Expiration if set to true. | false | -| Expiration | `time.Duration` | Expiration is the duration before the CSRF token will expire. | 1 * time.Hour | +| CookieSessionOnly | `bool` | Decides whether the cookie should last for only the browser session. (cookie expires on close). | false | +| IdleTimeout | `time.Duration` | IdleTimeout is the duration of inactivity before the CSRF token will expire. | 30 * time.Minute | | KeyGenerator | `func() string` | KeyGenerator creates a new CSRF token. | utils.UUID | | ErrorHandler | `fiber.ErrorHandler` | ErrorHandler is executed when an error is returned from fiber.Handler. | DefaultErrorHandler | | Extractor | `func(fiber.Ctx) (string, error)` | Extractor returns the CSRF token. If set, this will be used in place of an Extractor based on KeyLookup. | Extractor based on KeyLookup | | SingleUseToken | `bool` | SingleUseToken indicates if the CSRF token be destroyed and a new one generated on each use. (See TokenLifecycle) | false | | Storage | `fiber.Storage` | Store is used to store the state of the middleware. | `nil` | | Session | `*session.Store` | Session is used to store the state of the middleware. Overrides Storage if set. | `nil` | -| SessionKey | `string` | SessionKey is the key used to store the token in the session. | "csrfToken" | | TrustedOrigins | `[]string` | TrustedOrigins is a list of trusted origins for unsafe requests. This supports subdomain matching, so you can use a value like "https://*.example.com" to allow any subdomain of example.com to submit requests. | `[]` | ### Default Config @@ -124,11 +123,10 @@ var ConfigDefault = Config{ KeyLookup: "header:" + HeaderName, CookieName: "csrf_", CookieSameSite: "Lax", - Expiration: 1 * time.Hour, + IdleTimeout: 30 * time.Minute, KeyGenerator: utils.UUIDv4, ErrorHandler: defaultErrorHandler, Extractor: FromHeader(HeaderName), - SessionKey: "csrfToken", } ``` @@ -144,12 +142,11 @@ var ConfigDefault = Config{ CookieSecure: true, CookieSessionOnly: true, CookieHTTPOnly: true, - Expiration: 1 * time.Hour, + IdleTimeout: 30 * time.Minute, KeyGenerator: utils.UUIDv4, ErrorHandler: defaultErrorHandler, Extractor: FromHeader(HeaderName), Session: session.Store, - SessionKey: "csrfToken", } ``` @@ -304,7 +301,7 @@ The Referer header is automatically included in requests by all modern browsers, ## Token Lifecycle -Tokens are valid until they expire or until they are deleted. By default, tokens are valid for 1 hour, and each subsequent request extends the expiration by 1 hour. The token only expires if the user doesn't make a request for the duration of the expiration time. +Tokens are valid until they expire or until they are deleted. By default, tokens are valid for 30 minutes, and each subsequent request extends the expiration by the idle timeout. The token only expires if the user doesn't make a request for the duration of the idle timeout. ### Token Reuse diff --git a/docs/middleware/session.md b/docs/middleware/session.md index 39b9ccc801..ff73ff6094 100644 --- a/docs/middleware/session.md +++ b/docs/middleware/session.md @@ -2,142 +2,481 @@ id: session --- -# Session +# Session Middleware for [Fiber](https://github.com/gofiber/fiber) -Session middleware for [Fiber](https://github.com/gofiber/fiber). +The `session` middleware provides session management for Fiber applications, utilizing the [Storage](https://github.com/gofiber/storage) package for multi-database support via a unified interface. By default, session data is stored in memory, but custom storage options are easily configurable (see examples below). + +As of v3, we recommend using the middleware handler for session management. However, for backward compatibility, v2's session methods are still available, allowing you to continue using the session management techniques from earlier versions of Fiber. Both methods are demonstrated in the examples. + +## Table of Contents + +- [Migration Guide](#migration-guide) + - [v2 to v3](#v2-to-v3) +- [Types](#types) + - [Config](#config) + - [Middleware](#middleware) + - [Session](#session) + - [Store](#store) +- [Signatures](#signatures) + - [Session Package Functions](#session-package-functions) + - [Config Methods](#config-methods) + - [Middleware Methods](#middleware-methods) + - [Session Methods](#session-methods) + - [Store Methods](#store-methods) +- [Examples](#examples) + - [Middleware Handler (Recommended)](#middleware-handler-recommended) + - [Custom Storage Example](#custom-storage-example) + - [Session Without Middleware Handler](#session-without-middleware-handler) + - [Custom Types in Session Data](#custom-types-in-session-data) +- [Config](#config) +- [Default Config](#default-config) + +## Migration Guide + +### v2 to v3 + +- **Function Signature Change**: In v3, the `New` function now returns a middleware handler instead of a `*Store`. To access the store, use the `Store` method on `*Middleware` (obtained from `session.FromContext(c)` in a handler) or use `NewStore` or `NewWithStore`. + +- **Session Lifecycle Management**: The `*Store.Save` method no longer releases the instance automatically. You must manually call `sess.Release()` after using the session to manage its lifecycle properly. + +- **Expiration Handling**: Previously, the `Expiration` field represented the maximum session duration before expiration. However, it would extend every time the session was saved, making its behavior a mix between session duration and session idle timeout. The `Expiration` field has been removed and replaced with `IdleTimeout` and `AbsoluteTimeout` fields, which explicitly defines the session's idle and absolute timeout periods. + + - **Idle Timeout**: The new `IdleTimeout`, handles session inactivity. If the session is idle for the specified duration, it will expire. The idle timeout is updated when the session is saved. If you are using the middleware handler, the idle timeout will be updated automatically. + + - **Absolute Timeout**: The `AbsoluteTimeout` field has been added. If you need to set an absolute session timeout, you can use this field to define the duration. The session will expire after the specified duration, regardless of activity. + +For more details about Fiber v3, see [What’s New](https://github.com/gofiber/fiber/blob/main/docs/whats_new.md). + +### Migrating v2 to v3 Example (Legacy Approach) + +To convert a v2 example to use the v3 legacy approach, follow these steps: + +1. **Initialize with Store**: Use `session.NewStore()` to obtain a store. +2. **Retrieve Session**: Access the session store using the `store.Get(c)` method. +3. **Release Session**: Ensure that you call `sess.Release()` after you are done with the session to manage its lifecycle. :::note -This middleware uses our [Storage](https://github.com/gofiber/storage) package to support various databases through a single interface. The default configuration for this middleware saves data to memory, see the examples below for other databases. +When using the legacy approach, the IdleTimeout will be updated when the session is saved. ::: +#### Example Conversion + +**v2 Example:** + +```go +store := session.New() + +app.Get("/", func(c *fiber.Ctx) error { + sess, err := store.Get(c) + if err != nil { + return err + } + + key, ok := sess.Get("key").(string) + if !ok { + return c.SendStatus(fiber.StatusInternalServerError) + } + + sess.Set("key", "value") + + err = sess.Save() + if err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + + return nil +}) +``` + +**v3 Legacy Approach:** + +```go +store := session.NewStore() + +app.Get("/", func(c fiber.Ctx) error { + sess, err := store.Get(c) + if err != nil { + return err + } + defer sess.Release() // Important: Release the session + + key, ok := sess.Get("key").(string) + if !ok { + return c.SendStatus(fiber.StatusInternalServerError) + } + + sess.Set("key", "value") + + err = sess.Save() + if err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + + return nil +}) +``` + +### v3 Example (Recommended Middleware Handler) + +Do not call `sess.Release()` when using the middleware handler. `sess.Save()` is also not required, as the middleware automatically saves the session data. + +For the recommended approach, use the middleware handler. See the [Middleware Handler (Recommended)](#middleware-handler-recommended) section for details. + +## Types + +### Config + +Defines the configuration options for the session middleware. + +```go +type Config struct { + Storage fiber.Storage + Next func(fiber.Ctx) bool + Store *Store + ErrorHandler func(fiber.Ctx, error) + KeyGenerator func() string + KeyLookup string + CookieDomain string + CookiePath string + CookieSameSite string + IdleTimeout time.Duration + AbsoluteTimeout time.Duration + CookieSecure bool + CookieHTTPOnly bool + CookieSessionOnly bool +} +``` + +### Middleware + +The `Middleware` struct encapsulates the session middleware configuration and storage, created via `New` or `NewWithStore`. + +```go +type Middleware struct { + Session *Session +} +``` + +### Session + +Represents a user session, accessible through `FromContext` or `Store.Get`. + +```go +type Session struct {} +``` + +### Store + +Handles session data management and is created using `NewStore`, `NewWithStore` or by accessing the `Store` method of a middleware instance. + +```go +type Store struct { + Config +} +``` + ## Signatures +### Session Package Functions + ```go -func New(config ...Config) *Store -func (s *Store) RegisterType(i any) -func (s *Store) Get(c fiber.Ctx) (*Session, error) -func (s *Store) Delete(id string) error -func (s *Store) Reset() error +func New(config ...Config) *Middleware +func NewWithStore(config ...Config) (fiber.Handler, *Store) +func FromContext(c fiber.Ctx) *Middleware +``` + +### Config Methods + +```go +func DefaultErrorHandler(fiber.Ctx, err error) +``` + +### Middleware Methods + +```go +func (m *Middleware) Set(key string, value any) +func (m *Middleware) Get(key string) any +func (m *Middleware) Delete(key string) +func (m *Middleware) Destroy() error +func (m *Middleware) Reset() error +func (m *Middleware) Store() *Store +``` +### Session Methods + +```go +func (s *Session) Fresh() bool +func (s *Session) ID() string func (s *Session) Get(key string) any func (s *Session) Set(key string, val any) -func (s *Session) Delete(key string) func (s *Session) Destroy() error -func (s *Session) Reset() error func (s *Session) Regenerate() error +func (s *Session) Release() +func (s *Session) Reset() error func (s *Session) Save() error -func (s *Session) Fresh() bool -func (s *Session) ID() string func (s *Session) Keys() []string -func (s *Session) SetExpiry(exp time.Duration) +func (s *Session) SetIdleTimeout(idleTimeout time.Duration) +``` + +### Store Methods + +```go +func (*Store) RegisterType(i any) +func (s *Store) Get(c fiber.Ctx) (*Session, error) +func (s *Store) GetByID(id string) (*Session, error) +func (s *Store) Reset() error +func (s *Store) Delete(id string) error ``` -:::caution -Storing `any` values are limited to built-ins Go types. +:::note + +#### `GetByID` Method + +The `GetByID` method retrieves a session from storage using its session ID. Unlike `Get`, which ties the session to a `fiber.Ctx` (request-response cycle), `GetByID` operates independently of any HTTP context. This makes it ideal for scenarios such as background processing, scheduled tasks, or non-HTTP-related session management. + +##### Key Features + +- **Context Independence**: Sessions retrieved via `GetByID` are not bound to `fiber.Ctx`. This means the session can be manipulated in contexts that aren't tied to an active HTTP request-response cycle. +- **Background Task Suitability**: Use this method when you need to manage sessions outside of the standard HTTP workflow, such as in scheduled jobs, background tasks, or any non-HTTP context where session data needs to be accessed or modified. + +##### Usage Considerations + +- **Manual Persistence**: Since there is no associated `fiber.Ctx`, changes made to the session (e.g., modifying data) will **not** automatically be saved to storage. You **must** call `session.Save()` explicitly to persist any updates to storage. +- **No Automatic Cookie Handling**: Any updates made to the session will **not** affect the client-side cookies. If the session changes need to be reflected in the client (e.g., in a future HTTP response), you will need to handle this manually by setting the cookies via other methods. +- **Resource Management**: After using a session retrieved by `GetByID`, you should call `session.Release()` to properly release the session back to the pool and free up resources. + +##### Example Use Cases + +- **Scheduled Jobs**: Retrieve and update session data periodically without triggering an HTTP request. +- **Background Processing**: Manage sessions for tasks running in the background, such as user inactivity checks or batch processing. + ::: ## Examples -Import the middleware package that is part of the Fiber web framework +:::note +**Security Notice**: For robust security, especially during sensitive operations like account changes or transactions, consider using CSRF protection. Fiber provides a [CSRF Middleware](https://docs.gofiber.io/api/middleware/csrf) that can be used with sessions to prevent CSRF attacks. +::: + +:::note +**Middleware Order**: The order of middleware matters. The session middleware should come before any handler or middleware that uses the session (for example, the CSRF middleware). +::: + +### Middleware Handler (Recommended) ```go +package main + import ( "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/csrf" "github.com/gofiber/fiber/v3/middleware/session" ) + +func main() { + app := fiber.New() + + sessionMiddleware, sessionStore := session.NewWithStore() + + app.Use(sessionMiddleware) + app.Use(csrf.New(csrf.Config{ + Store: sessionStore, + })) + + app.Get("/", func(c fiber.Ctx) error { + sess := session.FromContext(c) + if sess == nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + + name, ok := sess.Get("name").(string) + if !ok { + return c.SendString("Welcome anonymous user!") + } + + return c.SendString("Welcome " + name) + }) + + app.Listen(":3000") +} ``` -After you initiate your Fiber app, you can use the following possibilities: +### Custom Storage Example ```go -// Initialize default config -// This stores all of your app's sessions -store := session.New() +package main -app.Get("/", func(c fiber.Ctx) error { - // Get session from storage - sess, err := store.Get(c) - if err != nil { - panic(err) - } +import ( + "github.com/gofiber/fiber/v3" + "github.com/gofiber/storage/sqlite3" + "github.com/gofiber/fiber/v3/middleware/csrf" + "github.com/gofiber/fiber/v3/middleware/session" +) - // Get value - name := sess.Get("name") +func main() { + app := fiber.New() - // Set key/value - sess.Set("name", "john") + storage := sqlite3.New() + sessionMiddleware, sessionStore := session.NewWithStore(session.Config{ + Storage: storage, + }) - // Get all Keys - keys := sess.Keys() + app.Use(sessionMiddleware) + app.Use(csrf.New(csrf.Config{ + Store: sessionStore, + })) - // Delete key - sess.Delete("name") + app.Listen(":3000") +} +``` - // Destroy session - if err := sess.Destroy(); err != nil { - panic(err) - } +### Session Without Middleware Handler - // Sets a specific expiration for this session - sess.SetExpiry(time.Second * 2) +```go +package main - // Save session - if err := sess.Save(); err != nil { - panic(err) - } +import ( + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/csrf" + "github.com/gofiber/fiber/v3/middleware/session" +) - return c.SendString(fmt.Sprintf("Welcome %v", name)) -}) -``` +func main() { + app := fiber.New() -## Config + sessionStore := session.NewStore() -| Property | Type | Description | Default | -|:------------------------|:----------------|:------------------------------------------------------------------------------------------------------------|:----------------------| -| Expiration | `time.Duration` | Allowed session duration. | `24 * time.Hour` | -| Storage | `fiber.Storage` | Storage interface to store the session data. | `memory.New()` | -| KeyLookup | `string` | KeyLookup is a string in the form of "`:`" that is used to extract session id from the request. | `"cookie:session_id"` | -| CookieDomain | `string` | Domain of the cookie. | `""` | -| CookiePath | `string` | Path of the cookie. | `""` | -| CookieSecure | `bool` | Indicates if cookie is secure. | `false` | -| CookieHTTPOnly | `bool` | Indicates if cookie is HTTP only. | `false` | -| CookieSameSite | `string` | Value of SameSite cookie. | `"Lax"` | -| CookieSessionOnly | `bool` | Decides whether cookie should last for only the browser session. Ignores Expiration if set to true. | `false` | -| KeyGenerator | `func() string` | KeyGenerator generates the session key. | `utils.UUIDv4` | -| CookieName (Deprecated) | `string` | Deprecated: Please use KeyLookup. The session name. | `""` | + app.Use(csrf.New(csrf.Config{ + Store: sessionStore, + })) -## Default Config + app.Get("/", func(c fiber.Ctx) error { + sess, err := sessionStore.Get(c) + if err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + defer sess.Release() -```go -var ConfigDefault = Config{ - Expiration: 24 * time.Hour, - KeyLookup: "cookie:session_id", - KeyGenerator: utils.UUIDv4, - source: "cookie", - sessionName: "session_id", + name, ok := sess.Get("name").(string) + if !ok { + return c.SendString("Welcome anonymous user!") + } + + return c.SendString("Welcome " + name) + }) + + app.Post("/login", func(c fiber.Ctx) error { + sess, err := sessionStore.Get(c) + if err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + defer sess.Release() + + if !sess.Fresh() { + if err := sess.Regenerate(); err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + } + + sess.Set("name", "John Doe") + + err = sess.Save() + if err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + + return c.SendString("Logged in!") + }) + + app.Listen(":3000") } ``` -## Constants +### Custom Types in Session Data + +Session data can only be of the following types by default: + +- `string` +- `int` +- `int8` +- `int16` +- `int32` +- `int64` +- `uint` +- `uint8` +- `uint16` +- `uint32` +- `uint64` +- `bool` +- `float32` +- `float64` +- `[]byte` +- `complex64` +- `complex128` +- `interface{}` + +To support other types in session data, you can register custom types. Here is an example of how to register a custom type: ```go -const ( - SourceCookie Source = "cookie" - SourceHeader Source = "header" - SourceURLQuery Source = "query" +package main + +import ( + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/session" ) -``` -### Custom Storage/Database +type User struct { + Name string + Age int +} -You can use any storage from our [storage](https://github.com/gofiber/storage/) package. +func main() { + app := fiber.New() -```go -storage := sqlite3.New() // From github.com/gofiber/storage/sqlite3 + sessionMiddleware, sessionStore := session.NewWithStore() + sessionStore.RegisterType(User{}) -store := session.New(session.Config{ - Storage: storage, -}) + app.Use(sessionMiddleware) + + app.Listen(":3000") +} ``` -To use the store, see the [Examples](#examples). +## Config + +| Property | Type | Description | Default | +|-----------------------|--------------------------------|--------------------------------------------------------------------------------------------|---------------------------| +| **Storage** | `fiber.Storage` | Defines where session data is stored. | `nil` (in-memory storage) | +| **Next** | `func(c fiber.Ctx) bool` | Function to skip this middleware under certain conditions. | `nil` | +| **ErrorHandler** | `func(c fiber.Ctx, err error)` | Custom error handler for session middleware errors. | `nil` | +| **KeyGenerator** | `func() string` | Function to generate session IDs. | `UUID()` | +| **KeyLookup** | `string` | Key used to store session ID in cookie or header. | `"cookie:session_id"` | +| **CookieDomain** | `string` | The domain scope of the session cookie. | `""` | +| **CookiePath** | `string` | The path scope of the session cookie. | `"/"` | +| **CookieSameSite** | `string` | The SameSite attribute of the session cookie. | `"Lax"` | +| **IdleTimeout** | `time.Duration` | Maximum duration of inactivity before session expires. | `30 * time.Minute` | +| **AbsoluteTimeout** | `time.Duration` | Maximum duration before session expires. | `0` (no expiration) | +| **CookieSecure** | `bool` | Ensures session cookie is only sent over HTTPS. | `false` | +| **CookieHTTPOnly** | `bool` | Ensures session cookie is not accessible to JavaScript (HTTP only). | `true` | +| **CookieSessionOnly** | `bool` | Prevents session cookie from being saved after the session ends (cookie expires on close). | `false` | + +## Default Config + +```go +session.Config{ + Storage: memory.New(), + Next: nil, + Store: nil, + ErrorHandler: nil, + KeyGenerator: utils.UUIDv4, + KeyLookup: "cookie:session_id", + CookieDomain: "", + CookiePath: "", + CookieSameSite: "Lax", + IdleTimeout: 30 * time.Minute, + AbsoluteTimeout: 0, + CookieSecure: false, + CookieHTTPOnly: false, + CookieSessionOnly: false, +} +``` diff --git a/docs/whats_new.md b/docs/whats_new.md index e040f367d5..53c2f9d8e6 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -30,6 +30,7 @@ Here's a quick overview of the changes in Fiber `v3`: - [🧰 Generic functions](#-generic-functions) - [🧬 Middlewares](#-middlewares) - [CORS](#cors) + - [CSRF](#csrf) - [Session](#session) - [Filesystem](#filesystem) - [Monitor](#monitor) @@ -316,9 +317,19 @@ Added support for specifying Key length when using `encryptcookie.GenerateKey(le ### Session -:::caution -DRAFT section -::: +The Session middleware has undergone key changes in v3 to improve functionality and flexibility. While v2 methods remain available for backward compatibility, we now recommend using the new middleware handler for session management. + +#### Key Updates + +- **New Middleware Handler**: The `New` function now returns a middleware handler instead of a `*Store`. To access the session store, use the `Store` method on the middleware, or opt for `NewStore` or `NewWithStore` for custom store integration. + +- **Manual Session Release**: Session instances are no longer automatically released after being saved. To ensure proper lifecycle management, you must manually call `sess.Release()`. + +- **Idle Timeout**: The `Expiration` field has been replaced with `IdleTimeout`, which handles session inactivity. If the session is idle for the specified duration, it will expire. The idle timeout is updated when the session is saved. If you are using the middleware handler, the idle timeout will be updated automatically. + +- **Absolute Timeout**: The `AbsoluteTimeout` field has been added. If you need to set an absolute session timeout, you can use this field to define the duration. The session will expire after the specified duration, regardless of activity. + +For more details on these changes and migration instructions, check the [Session Middleware Migration Guide](./middleware/session.md#migration-guide). ### Filesystem @@ -521,6 +532,24 @@ app.Use(cors.New(cors.Config{ })) ``` +#### CSRF + +- **Field Renaming**: The `Expiration` field in the CSRF middleware configuration has been renamed to `IdleTimeout` to better describe its functionality. Additionally, the default value has been reduced from 1 hour to 30 minutes. Update your code as follows: + +```go +// Before +app.Use(csrf.New(csrf.Config{ + Expiration: 10 * time.Minute, +})) + +// After +app.Use(csrf.New(csrf.Config{ + IdleTimeout: 10 * time.Minute, +})) +``` + +- **Session Key Removal**: The `SessionKey` field has been removed from the CSRF middleware configuration. The session key is now an unexported constant within the middleware to avoid potential key collisions in the session store. + #### Filesystem You need to move filesystem middleware to static middleware due to it has been removed from the core. diff --git a/middleware/csrf/config.go b/middleware/csrf/config.go index d37c33a58e..e718b15874 100644 --- a/middleware/csrf/config.go +++ b/middleware/csrf/config.go @@ -78,11 +78,6 @@ type Config struct { // Optional. Default value "Lax". CookieSameSite string - // SessionKey is the key used to store the token in the session - // - // Default: "csrfToken" - SessionKey string - // TrustedOrigins is a list of trusted origins for unsafe requests. // For requests that use the Origin header, the origin must match the // Host header or one of the TrustedOrigins. @@ -96,10 +91,10 @@ type Config struct { // Optional. Default: [] TrustedOrigins []string - // Expiration is the duration before csrf token will expire + // IdleTimeout is the duration of time the CSRF token is valid. // - // Optional. Default: 1 * time.Hour - Expiration time.Duration + // Optional. Default: 30 * time.Minute + IdleTimeout time.Duration // Indicates if CSRF cookie is secure. // Optional. Default value false. @@ -127,11 +122,10 @@ var ConfigDefault = Config{ KeyLookup: "header:" + HeaderName, CookieName: "csrf_", CookieSameSite: "Lax", - Expiration: 1 * time.Hour, + IdleTimeout: 30 * time.Minute, KeyGenerator: utils.UUIDv4, ErrorHandler: defaultErrorHandler, Extractor: FromHeader(HeaderName), - SessionKey: "csrfToken", } // default ErrorHandler that process return error from fiber.Handler @@ -153,8 +147,8 @@ func configDefault(config ...Config) Config { if cfg.KeyLookup == "" { cfg.KeyLookup = ConfigDefault.KeyLookup } - if int(cfg.Expiration.Seconds()) <= 0 { - cfg.Expiration = ConfigDefault.Expiration + if cfg.IdleTimeout <= 0 { + cfg.IdleTimeout = ConfigDefault.IdleTimeout } if cfg.CookieName == "" { cfg.CookieName = ConfigDefault.CookieName @@ -168,9 +162,6 @@ func configDefault(config ...Config) Config { if cfg.ErrorHandler == nil { cfg.ErrorHandler = ConfigDefault.ErrorHandler } - if cfg.SessionKey == "" { - cfg.SessionKey = ConfigDefault.SessionKey - } // Generate the correct extractor to get the token from the correct location selectors := strings.Split(cfg.KeyLookup, ":") diff --git a/middleware/csrf/csrf.go b/middleware/csrf/csrf.go index d417730416..dedfe6bd55 100644 --- a/middleware/csrf/csrf.go +++ b/middleware/csrf/csrf.go @@ -49,10 +49,7 @@ func New(config ...Config) fiber.Handler { var sessionManager *sessionManager var storageManager *storageManager if cfg.Session != nil { - // Register the Token struct in the session store - cfg.Session.RegisterType(Token{}) - - sessionManager = newSessionManager(cfg.Session, cfg.SessionKey) + sessionManager = newSessionManager(cfg.Session) } else { storageManager = newStorageManager(cfg.Storage) } @@ -220,9 +217,9 @@ func getRawFromStorage(c fiber.Ctx, token string, cfg Config, sessionManager *se // createOrExtendTokenInStorage creates or extends the token in the storage func createOrExtendTokenInStorage(c fiber.Ctx, token string, cfg Config, sessionManager *sessionManager, storageManager *storageManager) { if cfg.Session != nil { - sessionManager.setRaw(c, token, dummyValue, cfg.Expiration) + sessionManager.setRaw(c, token, dummyValue, cfg.IdleTimeout) } else { - storageManager.setRaw(token, dummyValue, cfg.Expiration) + storageManager.setRaw(token, dummyValue, cfg.IdleTimeout) } } @@ -237,7 +234,7 @@ func deleteTokenFromStorage(c fiber.Ctx, token string, cfg Config, sessionManage // Update CSRF cookie // if expireCookie is true, the cookie will expire immediately func updateCSRFCookie(c fiber.Ctx, cfg Config, token string) { - setCSRFCookie(c, cfg, token, cfg.Expiration) + setCSRFCookie(c, cfg, token, cfg.IdleTimeout) } func expireCSRFCookie(c fiber.Ctx, cfg Config) { diff --git a/middleware/csrf/csrf_test.go b/middleware/csrf/csrf_test.go index 82252549bd..090082f4d8 100644 --- a/middleware/csrf/csrf_test.go +++ b/middleware/csrf/csrf_test.go @@ -70,7 +70,7 @@ func Test_CSRF_WithSession(t *testing.T) { t.Parallel() // session store - store := session.New(session.Config{ + store := session.NewStore(session.Config{ KeyLookup: "cookie:_session", }) @@ -156,13 +156,68 @@ func Test_CSRF_WithSession(t *testing.T) { } } +// go test -run Test_CSRF_WithSession_Middleware +func Test_CSRF_WithSession_Middleware(t *testing.T) { + t.Parallel() + app := fiber.New() + + // session mw + smh, sstore := session.NewWithStore() + + // csrf mw + cmh := New(Config{ + Session: sstore, + }) + + app.Use(smh) + + app.Use(cmh) + + app.Get("/", func(c fiber.Ctx) error { + sess := session.FromContext(c) + sess.Set("hello", "world") + return c.SendStatus(fiber.StatusOK) + }) + + app.Post("/", func(c fiber.Ctx) error { + sess := session.FromContext(c) + if sess.Get("hello") != "world" { + return c.SendStatus(fiber.StatusInternalServerError) + } + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + // Generate CSRF token and session_id + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) + csrfTokenParts := strings.Split(string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)), ";") + require.Greater(t, len(csrfTokenParts), 2) + csrfToken := strings.Split(csrfTokenParts[0], "=")[1] + require.NotEmpty(t, csrfToken) + sessionID := strings.Split(csrfTokenParts[1], "=")[1] + require.NotEmpty(t, sessionID) + + // Use the CSRF token and session_id + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(HeaderName, csrfToken) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, csrfToken) + ctx.Request.Header.SetCookie("session_id", sessionID) + h(ctx) + require.Equal(t, 200, ctx.Response.StatusCode()) +} + // go test -run Test_CSRF_ExpiredToken func Test_CSRF_ExpiredToken(t *testing.T) { t.Parallel() app := fiber.New() app.Use(New(Config{ - Expiration: 1 * time.Second, + IdleTimeout: 1 * time.Second, })) app.Post("/", func(c fiber.Ctx) error { @@ -205,7 +260,7 @@ func Test_CSRF_ExpiredToken_WithSession(t *testing.T) { t.Parallel() // session store - store := session.New(session.Config{ + store := session.NewStore(session.Config{ KeyLookup: "cookie:_session", }) @@ -229,8 +284,8 @@ func Test_CSRF_ExpiredToken_WithSession(t *testing.T) { // middleware config config := Config{ - Session: store, - Expiration: 1 * time.Second, + Session: store, + IdleTimeout: 1 * time.Second, } // middleware @@ -1076,7 +1131,7 @@ func Test_CSRF_DeleteToken_WithSession(t *testing.T) { t.Parallel() // session store - store := session.New(session.Config{ + store := session.NewStore(session.Config{ KeyLookup: "cookie:_session", }) diff --git a/middleware/csrf/session_manager.go b/middleware/csrf/session_manager.go index 3bbf173a26..8961c6a542 100644 --- a/middleware/csrf/session_manager.go +++ b/middleware/csrf/session_manager.go @@ -10,28 +10,46 @@ import ( type sessionManager struct { session *session.Store - key string } -func newSessionManager(s *session.Store, k string) *sessionManager { +type sessionKeyType int + +const ( + sessionKey sessionKeyType = 0 +) + +func newSessionManager(s *session.Store) *sessionManager { // Create new storage handler - sessionManager := &sessionManager{ - key: k, - } + sessionManager := new(sessionManager) if s != nil { // Use provided storage if provided sessionManager.session = s + + // Register the sessionKeyType and Token type + s.RegisterType(sessionKeyType(0)) + s.RegisterType(Token{}) } return sessionManager } // get token from session func (m *sessionManager) getRaw(c fiber.Ctx, key string, raw []byte) []byte { - sess, err := m.session.Get(c) - if err != nil { - return nil + sess := session.FromContext(c) + var token Token + var ok bool + + if sess != nil { + token, ok = sess.Get(sessionKey).(Token) + } else { + // Try to get the session from the store + storeSess, err := m.session.Get(c) + if err != nil { + // Handle error + return nil + } + token, ok = storeSess.Get(sessionKey).(Token) } - token, ok := sess.Get(m.key).(Token) + if ok { if token.Expiration.Before(time.Now()) || key != token.Key || !compareTokens(raw, token.Raw) { return nil @@ -44,25 +62,39 @@ func (m *sessionManager) getRaw(c fiber.Ctx, key string, raw []byte) []byte { // set token in session func (m *sessionManager) setRaw(c fiber.Ctx, key string, raw []byte, exp time.Duration) { - sess, err := m.session.Get(c) - if err != nil { - return - } - // the key is crucial in crsf and sometimes a reference to another value which can be reused later(pool/unsafe values concept), so a copy is made here - sess.Set(m.key, &Token{Key: key, Raw: raw, Expiration: time.Now().Add(exp)}) - if err := sess.Save(); err != nil { - log.Warn("csrf: failed to save session: ", err) + sess := session.FromContext(c) + if sess != nil { + // the key is crucial in crsf and sometimes a reference to another value which can be reused later(pool/unsafe values concept), so a copy is made here + sess.Set(sessionKey, Token{Key: key, Raw: raw, Expiration: time.Now().Add(exp)}) + } else { + // Try to get the session from the store + storeSess, err := m.session.Get(c) + if err != nil { + // Handle error + return + } + storeSess.Set(sessionKey, Token{Key: key, Raw: raw, Expiration: time.Now().Add(exp)}) + if err := storeSess.Save(); err != nil { + log.Warn("csrf: failed to save session: ", err) + } } } // delete token from session func (m *sessionManager) delRaw(c fiber.Ctx) { - sess, err := m.session.Get(c) - if err != nil { - return - } - sess.Delete(m.key) - if err := sess.Save(); err != nil { - log.Warn("csrf: failed to save session: ", err) + sess := session.FromContext(c) + if sess != nil { + sess.Delete(sessionKey) + } else { + // Try to get the session from the store + storeSess, err := m.session.Get(c) + if err != nil { + // Handle error + return + } + storeSess.Delete(sessionKey) + if err := storeSess.Save(); err != nil { + log.Warn("csrf: failed to save session: ", err) + } } } diff --git a/middleware/session/config.go b/middleware/session/config.go index 1eabc05bd4..c2a115d732 100644 --- a/middleware/session/config.go +++ b/middleware/session/config.go @@ -5,60 +5,98 @@ import ( "time" "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/log" "github.com/gofiber/utils/v2" ) -// Config defines the config for middleware. +// Config defines the configuration for the session middleware. type Config struct { - // Storage interface to store the session data - // Optional. Default value memory.New() + // Storage interface for storing session data. + // + // Optional. Default: memory.New() Storage fiber.Storage + // Next defines a function to skip this middleware when it returns true. + // Optional. Default: nil + Next func(c fiber.Ctx) bool + + // Store defines the session store. + // + // Required. + Store *Store + + // ErrorHandler defines a function to handle errors. + // + // Optional. Default: nil + ErrorHandler func(fiber.Ctx, error) + // KeyGenerator generates the session key. - // Optional. Default value utils.UUIDv4 + // + // Optional. Default: utils.UUIDv4 KeyGenerator func() string - // KeyLookup is a string in the form of ":" that is used - // to extract session id from the request. - // Possible values: "header:", "query:" or "cookie:" - // Optional. Default value "cookie:session_id". + // KeyLookup is a string in the format ":" used to extract the session ID from the request. + // + // Possible values: "header:", "query:", "cookie:" + // + // Optional. Default: "cookie:session_id" KeyLookup string - // Domain of the cookie. - // Optional. Default value "". + // CookieDomain defines the domain of the session cookie. + // + // Optional. Default: "" CookieDomain string - // Path of the cookie. - // Optional. Default value "". + // CookiePath defines the path of the session cookie. + // + // Optional. Default: "" CookiePath string - // Value of SameSite cookie. - // Optional. Default value "Lax". + // CookieSameSite specifies the SameSite attribute of the cookie. + // + // Optional. Default: "Lax" CookieSameSite string - // Source defines where to obtain the session id + // Source defines where to obtain the session ID. source Source - // The session name + // sessionName is the name of the session. sessionName string - // Allowed session duration - // Optional. Default value 24 * time.Hour - Expiration time.Duration - // Indicates if cookie is secure. - // Optional. Default value false. + // IdleTimeout defines the maximum duration of inactivity before the session expires. + // + // Note: The idle timeout is updated on each `Save()` call. If a middleware handler is used, `Save()` is called automatically. + // + // Optional. Default: 30 * time.Minute + IdleTimeout time.Duration + + // AbsoluteTimeout defines the maximum duration of the session before it expires. + // + // If set to 0, the session will not have an absolute timeout, and will expire after the idle timeout. + // + // Optional. Default: 0 + AbsoluteTimeout time.Duration + + // CookieSecure specifies if the session cookie should be secure. + // + // Optional. Default: false CookieSecure bool - // Indicates if cookie is HTTP only. - // Optional. Default value false. + // CookieHTTPOnly specifies if the session cookie should be HTTP-only. + // + // Optional. Default: false CookieHTTPOnly bool - // Decides whether cookie should last for only the browser sesison. - // Ignores Expiration if set to true - // Optional. Default value false. + // CookieSessionOnly determines if the cookie should expire when the browser session ends. + // + // If true, the cookie will be deleted when the browser is closed. + // Note: This will not delete the session data from the store. + // + // Optional. Default: false CookieSessionOnly bool } +// Source represents the type of session ID source. type Source string const ( @@ -67,28 +105,59 @@ const ( SourceURLQuery Source = "query" ) -// ConfigDefault is the default config +// ConfigDefault provides the default configuration. var ConfigDefault = Config{ - Expiration: 24 * time.Hour, + IdleTimeout: 30 * time.Minute, KeyLookup: "cookie:session_id", KeyGenerator: utils.UUIDv4, - source: "cookie", + source: SourceCookie, sessionName: "session_id", } -// Helper function to set default values +// DefaultErrorHandler logs the error and sends a 500 status code. +// +// Parameters: +// - c: The Fiber context. +// - err: The error to handle. +// +// Usage: +// +// DefaultErrorHandler(c, err) +func DefaultErrorHandler(c fiber.Ctx, err error) { + log.Errorf("session: %v", err) + if sendErr := c.SendStatus(fiber.StatusInternalServerError); sendErr != nil { + log.Errorf("session: %v", sendErr) + } +} + +// configDefault sets default values for the Config struct. +// +// Parameters: +// - config: Variadic parameter to override the default config. +// +// Returns: +// - Config: The configuration with default values set. +// +// Usage: +// +// cfg := configDefault() +// cfg := configDefault(customConfig) func configDefault(config ...Config) Config { - // Return default config if nothing provided + // Return default config if none provided. if len(config) < 1 { return ConfigDefault } - // Override default config + // Override default config with provided config. cfg := config[0] - // Set default values - if int(cfg.Expiration.Seconds()) <= 0 { - cfg.Expiration = ConfigDefault.Expiration + // Set default values where necessary. + if cfg.IdleTimeout <= 0 { + cfg.IdleTimeout = ConfigDefault.IdleTimeout + } + // Ensure AbsoluteTimeout is greater than or equal to IdleTimeout. + if cfg.AbsoluteTimeout > 0 && cfg.AbsoluteTimeout < cfg.IdleTimeout { + panic("[session] AbsoluteTimeout must be greater than or equal to IdleTimeout") } if cfg.KeyLookup == "" { cfg.KeyLookup = ConfigDefault.KeyLookup @@ -97,10 +166,11 @@ func configDefault(config ...Config) Config { cfg.KeyGenerator = ConfigDefault.KeyGenerator } + // Parse KeyLookup into source and session name. selectors := strings.Split(cfg.KeyLookup, ":") const numSelectors = 2 if len(selectors) != numSelectors { - panic("[session] KeyLookup must in the form of :") + panic("[session] KeyLookup must be in the format ':'") } switch Source(selectors[0]) { case SourceCookie: @@ -110,7 +180,7 @@ func configDefault(config ...Config) Config { case SourceURLQuery: cfg.source = SourceURLQuery default: - panic("[session] source is not supported") + panic("[session] unsupported source in KeyLookup") } cfg.sessionName = selectors[1] diff --git a/middleware/session/config_test.go b/middleware/session/config_test.go new file mode 100644 index 0000000000..c87ecef258 --- /dev/null +++ b/middleware/session/config_test.go @@ -0,0 +1,59 @@ +package session + +import ( + "testing" + "time" + + "github.com/gofiber/fiber/v3" + "github.com/stretchr/testify/require" + "github.com/valyala/fasthttp" +) + +func TestConfigDefault(t *testing.T) { + // Test default config + cfg := configDefault() + require.Equal(t, 30*time.Minute, cfg.IdleTimeout) + require.Equal(t, "cookie:session_id", cfg.KeyLookup) + require.NotNil(t, cfg.KeyGenerator) + require.Equal(t, SourceCookie, cfg.source) + require.Equal(t, "session_id", cfg.sessionName) +} + +func TestConfigDefaultWithCustomConfig(t *testing.T) { + // Test custom config + customConfig := Config{ + IdleTimeout: 48 * time.Hour, + KeyLookup: "header:custom_session_id", + KeyGenerator: func() string { return "custom_key" }, + } + cfg := configDefault(customConfig) + require.Equal(t, 48*time.Hour, cfg.IdleTimeout) + require.Equal(t, "header:custom_session_id", cfg.KeyLookup) + require.NotNil(t, cfg.KeyGenerator) + require.Equal(t, SourceHeader, cfg.source) + require.Equal(t, "custom_session_id", cfg.sessionName) +} + +func TestDefaultErrorHandler(t *testing.T) { + // Create a new Fiber app + app := fiber.New() + + // Create a new context + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + + // Test DefaultErrorHandler + DefaultErrorHandler(ctx, fiber.ErrInternalServerError) + require.Equal(t, fiber.StatusInternalServerError, ctx.Response().StatusCode()) +} + +func TestInvalidKeyLookupFormat(t *testing.T) { + require.PanicsWithValue(t, "[session] KeyLookup must be in the format ':'", func() { + configDefault(Config{KeyLookup: "invalid_format"}) + }) +} + +func TestUnsupportedSource(t *testing.T) { + require.PanicsWithValue(t, "[session] unsupported source in KeyLookup", func() { + configDefault(Config{KeyLookup: "unsupported:session_id"}) + }) +} diff --git a/middleware/session/data.go b/middleware/session/data.go index 08cb833f4e..052e43bc1b 100644 --- a/middleware/session/data.go +++ b/middleware/session/data.go @@ -8,57 +8,120 @@ import ( // //go:generate msgp -o=data_msgp.go -tests=true -unexported type data struct { - Data map[string]any + Data map[any]any sync.RWMutex `msg:"-"` } var dataPool = sync.Pool{ New: func() any { d := new(data) - d.Data = make(map[string]any) + d.Data = make(map[any]any) return d }, } +// acquireData returns a new data object from the pool. +// +// Returns: +// - *data: The data object. +// +// Usage: +// +// d := acquireData() func acquireData() *data { - return dataPool.Get().(*data) //nolint:forcetypeassert // We store nothing else in the pool + obj := dataPool.Get() + if d, ok := obj.(*data); ok { + return d + } + // Handle unexpected type in the pool + panic("unexpected type in data pool") } +// Reset clears the data map and resets the data object. +// +// Usage: +// +// d.Reset() func (d *data) Reset() { d.Lock() - d.Data = make(map[string]any) - d.Unlock() + defer d.Unlock() + d.Data = make(map[any]any) } -func (d *data) Get(key string) any { +// Get retrieves a value from the data map by key. +// +// Parameters: +// - key: The key to retrieve. +// +// Returns: +// - any: The value associated with the key. +// +// Usage: +// +// value := d.Get("key") +func (d *data) Get(key any) any { d.RLock() - v := d.Data[key] - d.RUnlock() - return v + defer d.RUnlock() + return d.Data[key] } -func (d *data) Set(key string, value any) { +// Set updates or creates a new key-value pair in the data map. +// +// Parameters: +// - key: The key to set. +// - value: The value to set. +// +// Usage: +// +// d.Set("key", "value") +func (d *data) Set(key, value any) { d.Lock() + defer d.Unlock() d.Data[key] = value - d.Unlock() } -func (d *data) Delete(key string) { +// Delete removes a key-value pair from the data map. +// +// Parameters: +// - key: The key to delete. +// +// Usage: +// +// d.Delete("key") +func (d *data) Delete(key any) { d.Lock() + defer d.Unlock() delete(d.Data, key) - d.Unlock() } -func (d *data) Keys() []string { - d.Lock() - keys := make([]string, 0, len(d.Data)) +// Keys retrieves all keys in the data map. +// +// Returns: +// - []any: A slice of all keys in the data map. +// +// Usage: +// +// keys := d.Keys() +func (d *data) Keys() []any { + d.RLock() + defer d.RUnlock() + keys := make([]any, 0, len(d.Data)) for k := range d.Data { keys = append(keys, k) } - d.Unlock() return keys } +// Len returns the number of key-value pairs in the data map. +// +// Returns: +// - int: The number of key-value pairs. +// +// Usage: +// +// length := d.Len() func (d *data) Len() int { + d.RLock() + defer d.RUnlock() return len(d.Data) } diff --git a/middleware/session/data_msgp.go b/middleware/session/data_msgp.go index a93ffcfb27..a640e141b8 100644 --- a/middleware/session/data_msgp.go +++ b/middleware/session/data_msgp.go @@ -24,36 +24,6 @@ func (z *data) DecodeMsg(dc *msgp.Reader) (err error) { return } switch msgp.UnsafeString(field) { - case "Data": - var zb0002 uint32 - zb0002, err = dc.ReadMapHeader() - if err != nil { - err = msgp.WrapError(err, "Data") - return - } - if z.Data == nil { - z.Data = make(map[string]interface{}, zb0002) - } else if len(z.Data) > 0 { - for key := range z.Data { - delete(z.Data, key) - } - } - for zb0002 > 0 { - zb0002-- - var za0001 string - var za0002 interface{} - za0001, err = dc.ReadString() - if err != nil { - err = msgp.WrapError(err, "Data") - return - } - za0002, err = dc.ReadIntf() - if err != nil { - err = msgp.WrapError(err, "Data", za0001) - return - } - z.Data[za0001] = za0002 - } default: err = dc.Skip() if err != nil { @@ -66,48 +36,22 @@ func (z *data) DecodeMsg(dc *msgp.Reader) (err error) { } // EncodeMsg implements msgp.Encodable -func (z *data) EncodeMsg(en *msgp.Writer) (err error) { - // map header, size 1 - // write "Data" - err = en.Append(0x81, 0xa4, 0x44, 0x61, 0x74, 0x61) +func (z data) EncodeMsg(en *msgp.Writer) (err error) { + // map header, size 0 + _ = z + err = en.Append(0x80) if err != nil { return } - err = en.WriteMapHeader(uint32(len(z.Data))) - if err != nil { - err = msgp.WrapError(err, "Data") - return - } - for za0001, za0002 := range z.Data { - err = en.WriteString(za0001) - if err != nil { - err = msgp.WrapError(err, "Data") - return - } - err = en.WriteIntf(za0002) - if err != nil { - err = msgp.WrapError(err, "Data", za0001) - return - } - } return } // MarshalMsg implements msgp.Marshaler -func (z *data) MarshalMsg(b []byte) (o []byte, err error) { +func (z data) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) - // map header, size 1 - // string "Data" - o = append(o, 0x81, 0xa4, 0x44, 0x61, 0x74, 0x61) - o = msgp.AppendMapHeader(o, uint32(len(z.Data))) - for za0001, za0002 := range z.Data { - o = msgp.AppendString(o, za0001) - o, err = msgp.AppendIntf(o, za0002) - if err != nil { - err = msgp.WrapError(err, "Data", za0001) - return - } - } + // map header, size 0 + _ = z + o = append(o, 0x80) return } @@ -129,36 +73,6 @@ func (z *data) UnmarshalMsg(bts []byte) (o []byte, err error) { return } switch msgp.UnsafeString(field) { - case "Data": - var zb0002 uint32 - zb0002, bts, err = msgp.ReadMapHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err, "Data") - return - } - if z.Data == nil { - z.Data = make(map[string]interface{}, zb0002) - } else if len(z.Data) > 0 { - for key := range z.Data { - delete(z.Data, key) - } - } - for zb0002 > 0 { - var za0001 string - var za0002 interface{} - zb0002-- - za0001, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err, "Data") - return - } - za0002, bts, err = msgp.ReadIntfBytes(bts) - if err != nil { - err = msgp.WrapError(err, "Data", za0001) - return - } - z.Data[za0001] = za0002 - } default: bts, err = msgp.Skip(bts) if err != nil { @@ -172,13 +86,7 @@ func (z *data) UnmarshalMsg(bts []byte) (o []byte, err error) { } // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z *data) Msgsize() (s int) { - s = 1 + 5 + msgp.MapHeaderSize - if z.Data != nil { - for za0001, za0002 := range z.Data { - _ = za0002 - s += msgp.StringPrefixSize + len(za0001) + msgp.GuessSize(za0002) - } - } +func (z data) Msgsize() (s int) { + s = 1 return } diff --git a/middleware/session/data_test.go b/middleware/session/data_test.go new file mode 100644 index 0000000000..1913f761d3 --- /dev/null +++ b/middleware/session/data_test.go @@ -0,0 +1,204 @@ +package session + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestKeys(t *testing.T) { + t.Parallel() + + // Test case: Empty data + t.Run("Empty data", func(t *testing.T) { + t.Parallel() + d := acquireData() + defer dataPool.Put(d) + defer d.Reset() + keys := d.Keys() + require.Empty(t, keys, "Expected no keys in empty data") + }) + + // Test case: Single key + t.Run("Single key", func(t *testing.T) { + t.Parallel() + d := acquireData() + defer dataPool.Put(d) + defer d.Reset() + d.Set("key1", "value1") + keys := d.Keys() + require.Len(t, keys, 1, "Expected one key") + require.Contains(t, keys, "key1", "Expected key1 to be present") + }) + + // Test case: Multiple keys + t.Run("Multiple keys", func(t *testing.T) { + t.Parallel() + d := acquireData() + defer dataPool.Put(d) + defer d.Reset() + d.Set("key1", "value1") + d.Set("key2", "value2") + d.Set("key3", "value3") + keys := d.Keys() + require.Len(t, keys, 3, "Expected three keys") + require.Contains(t, keys, "key1", "Expected key1 to be present") + require.Contains(t, keys, "key2", "Expected key2 to be present") + require.Contains(t, keys, "key3", "Expected key3 to be present") + }) + + // Test case: Concurrent access + t.Run("Concurrent access", func(t *testing.T) { + t.Parallel() + d := acquireData() + defer dataPool.Put(d) + defer d.Reset() + d.Set("key1", "value1") + d.Set("key2", "value2") + d.Set("key3", "value3") + + done := make(chan bool) + go func() { + keys := d.Keys() + assert.Len(t, keys, 3, "Expected three keys") + done <- true + }() + go func() { + keys := d.Keys() + assert.Len(t, keys, 3, "Expected three keys") + done <- true + }() + <-done + <-done + }) +} + +func TestData_Len(t *testing.T) { + t.Parallel() + + // Test case: Empty data + t.Run("Empty data", func(t *testing.T) { + t.Parallel() + d := acquireData() + defer dataPool.Put(d) + defer d.Reset() + length := d.Len() + require.Equal(t, 0, length, "Expected length to be 0 for empty data") + }) + + // Test case: Single key + t.Run("Single key", func(t *testing.T) { + t.Parallel() + d := acquireData() + defer dataPool.Put(d) + defer d.Reset() + d.Set("key1", "value1") + length := d.Len() + require.Equal(t, 1, length, "Expected length to be 1 when one key is set") + }) + + // Test case: Multiple keys + t.Run("Multiple keys", func(t *testing.T) { + t.Parallel() + d := acquireData() + defer dataPool.Put(d) + defer d.Reset() + d.Set("key1", "value1") + d.Set("key2", "value2") + d.Set("key3", "value3") + length := d.Len() + require.Equal(t, 3, length, "Expected length to be 3 when three keys are set") + }) + + // Test case: Concurrent access + t.Run("Concurrent access", func(t *testing.T) { + t.Parallel() + d := acquireData() + defer dataPool.Put(d) + defer d.Reset() + d.Set("key1", "value1") + d.Set("key2", "value2") + d.Set("key3", "value3") + + done := make(chan bool, 2) // Buffered channel with size 2 + go func() { + length := d.Len() + assert.Equal(t, 3, length, "Expected length to be 3 during concurrent access") + done <- true + }() + go func() { + length := d.Len() + assert.Equal(t, 3, length, "Expected length to be 3 during concurrent access") + done <- true + }() + <-done + <-done + }) +} + +func TestData_Get(t *testing.T) { + t.Parallel() + + // Test case: Non-existent key + t.Run("Non-existent key", func(t *testing.T) { + t.Parallel() + d := acquireData() + defer dataPool.Put(d) + defer d.Reset() + value := d.Get("non-existent-key") + require.Nil(t, value, "Expected nil for non-existent key") + }) + + // Test case: Existing key + t.Run("Existing key", func(t *testing.T) { + t.Parallel() + d := acquireData() + defer dataPool.Put(d) + defer d.Reset() + d.Set("key1", "value1") + value := d.Get("key1") + require.Equal(t, "value1", value, "Expected value1 for key1") + }) +} + +func TestData_Reset(t *testing.T) { + t.Parallel() + + // Test case: Reset data + t.Run("Reset data", func(t *testing.T) { + t.Parallel() + d := acquireData() + defer dataPool.Put(d) + d.Set("key1", "value1") + d.Set("key2", "value2") + d.Reset() + require.Empty(t, d.Data, "Expected data map to be empty after reset") + }) +} + +func TestData_Delete(t *testing.T) { + t.Parallel() + + // Test case: Delete existing key + t.Run("Delete existing key", func(t *testing.T) { + t.Parallel() + d := acquireData() + defer dataPool.Put(d) + defer d.Reset() + d.Set("key1", "value1") + d.Delete("key1") + value := d.Get("key1") + require.Nil(t, value, "Expected nil for deleted key") + }) + + // Test case: Delete non-existent key + t.Run("Delete non-existent key", func(t *testing.T) { + t.Parallel() + d := acquireData() + defer dataPool.Put(d) + defer d.Reset() + d.Delete("non-existent-key") + // No assertion needed, just ensure no panic or error + }) +} diff --git a/middleware/session/middleware.go b/middleware/session/middleware.go new file mode 100644 index 0000000000..c14bc19efe --- /dev/null +++ b/middleware/session/middleware.go @@ -0,0 +1,301 @@ +// Package session provides session management middleware for Fiber. +// This middleware handles user sessions, including storing session data in the store. +package session + +import ( + "errors" + "sync" + + "github.com/gofiber/fiber/v3" +) + +// Middleware holds session data and configuration. +type Middleware struct { + Session *Session + ctx fiber.Ctx + config Config + mu sync.RWMutex + destroyed bool +} + +// Context key for session middleware lookup. +type middlewareKey int + +const ( + // middlewareContextKey is the key used to store the *Middleware in the context locals. + middlewareContextKey middlewareKey = iota +) + +var ( + // ErrTypeAssertionFailed occurs when a type assertion fails. + ErrTypeAssertionFailed = errors.New("failed to type-assert to *Middleware") + + // Pool for reusing middleware instances. + middlewarePool = &sync.Pool{ + New: func() any { + return &Middleware{} + }, + } +) + +// New initializes session middleware with optional configuration. +// +// Parameters: +// - config: Variadic parameter to override default config. +// +// Returns: +// - fiber.Handler: The Fiber handler for the session middleware. +// +// Usage: +// +// app.Use(session.New()) +// +// Usage: +// +// app.Use(session.New()) +func New(config ...Config) fiber.Handler { + if len(config) > 0 { + handler, _ := NewWithStore(config[0]) + return handler + } + handler, _ := NewWithStore() + return handler +} + +// NewWithStore creates session middleware with an optional custom store. +// +// Parameters: +// - config: Variadic parameter to override default config. +// +// Returns: +// - fiber.Handler: The Fiber handler for the session middleware. +// - *Store: The session store. +// +// Usage: +// +// handler, store := session.NewWithStore() +func NewWithStore(config ...Config) (fiber.Handler, *Store) { + cfg := configDefault(config...) + + if cfg.Store == nil { + cfg.Store = NewStore(cfg) + } + + handler := func(c fiber.Ctx) error { + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Acquire session middleware + m := acquireMiddleware() + m.initialize(c, cfg) + + stackErr := c.Next() + + m.mu.RLock() + destroyed := m.destroyed + m.mu.RUnlock() + + if !destroyed { + m.saveSession() + } + + releaseMiddleware(m) + return stackErr + } + + return handler, cfg.Store +} + +// initialize sets up middleware for the request. +func (m *Middleware) initialize(c fiber.Ctx, cfg Config) { + m.mu.Lock() + defer m.mu.Unlock() + + session, err := cfg.Store.getSession(c) + if err != nil { + panic(err) // handle or log this error appropriately in production + } + + m.config = cfg + m.Session = session + m.ctx = c + + c.Locals(middlewareContextKey, m) +} + +// saveSession handles session saving and error management after the response. +func (m *Middleware) saveSession() { + if err := m.Session.saveSession(); err != nil { + if m.config.ErrorHandler != nil { + m.config.ErrorHandler(m.ctx, err) + } else { + DefaultErrorHandler(m.ctx, err) + } + } + + releaseSession(m.Session) +} + +// acquireMiddleware retrieves a middleware instance from the pool. +func acquireMiddleware() *Middleware { + m, ok := middlewarePool.Get().(*Middleware) + if !ok { + panic(ErrTypeAssertionFailed.Error()) + } + return m +} + +// releaseMiddleware resets and returns middleware to the pool. +// +// Parameters: +// - m: The middleware object to release. +// +// Usage: +// +// releaseMiddleware(m) +func releaseMiddleware(m *Middleware) { + m.mu.Lock() + m.config = Config{} + m.Session = nil + m.ctx = nil + m.destroyed = false + m.mu.Unlock() + middlewarePool.Put(m) +} + +// FromContext returns the Middleware from the Fiber context. +// +// Parameters: +// - c: The Fiber context. +// +// Returns: +// - *Middleware: The middleware object if found, otherwise nil. +// +// Usage: +// +// m := session.FromContext(c) +func FromContext(c fiber.Ctx) *Middleware { + m, ok := c.Locals(middlewareContextKey).(*Middleware) + if !ok { + return nil + } + return m +} + +// Set sets a key-value pair in the session. +// +// Parameters: +// - key: The key to set. +// - value: The value to set. +// +// Usage: +// +// m.Set("key", "value") +func (m *Middleware) Set(key, value any) { + m.mu.Lock() + defer m.mu.Unlock() + + m.Session.Set(key, value) +} + +// Get retrieves a value from the session by key. +// +// Parameters: +// - key: The key to retrieve. +// +// Returns: +// - any: The value associated with the key. +// +// Usage: +// +// value := m.Get("key") +func (m *Middleware) Get(key any) any { + m.mu.RLock() + defer m.mu.RUnlock() + + return m.Session.Get(key) +} + +// Delete removes a key-value pair from the session. +// +// Parameters: +// - key: The key to delete. +// +// Usage: +// +// m.Delete("key") +func (m *Middleware) Delete(key any) { + m.mu.Lock() + defer m.mu.Unlock() + + m.Session.Delete(key) +} + +// Destroy destroys the session. +// +// Returns: +// - error: An error if the destruction fails. +// +// Usage: +// +// err := m.Destroy() +func (m *Middleware) Destroy() error { + m.mu.Lock() + defer m.mu.Unlock() + + err := m.Session.Destroy() + m.destroyed = true + return err +} + +// Fresh checks if the session is fresh. +// +// Returns: +// - bool: True if the session is fresh, otherwise false. +// +// Usage: +// +// isFresh := m.Fresh() +func (m *Middleware) Fresh() bool { + return m.Session.Fresh() +} + +// ID returns the session ID. +// +// Returns: +// - string: The session ID. +// +// Usage: +// +// id := m.ID() +func (m *Middleware) ID() string { + return m.Session.ID() +} + +// Reset resets the session. +// +// Returns: +// - error: An error if the reset fails. +// +// Usage: +// +// err := m.Reset() +func (m *Middleware) Reset() error { + m.mu.Lock() + defer m.mu.Unlock() + + return m.Session.Reset() +} + +// Store returns the session store. +// +// Returns: +// - *Store: The session store. +// +// Usage: +// +// store := m.Store() +func (m *Middleware) Store() *Store { + return m.config.Store +} diff --git a/middleware/session/middleware_test.go b/middleware/session/middleware_test.go new file mode 100644 index 0000000000..579d61c44c --- /dev/null +++ b/middleware/session/middleware_test.go @@ -0,0 +1,469 @@ +package session + +import ( + "strings" + "sync" + "testing" + "time" + + "github.com/gofiber/fiber/v3" + "github.com/stretchr/testify/require" + "github.com/valyala/fasthttp" +) + +func Test_Session_Middleware(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New()) + + app.Get("/get", func(c fiber.Ctx) error { + sess := FromContext(c) + if sess == nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + value, ok := sess.Get("key").(string) + if !ok { + return c.Status(fiber.StatusNotFound).SendString("key not found") + } + return c.SendString("value=" + value) + }) + + app.Post("/set", func(c fiber.Ctx) error { + sess := FromContext(c) + if sess == nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + // get a value from the body + value := c.FormValue("value") + sess.Set("key", value) + return c.SendStatus(fiber.StatusOK) + }) + + app.Post("/delete", func(c fiber.Ctx) error { + sess := FromContext(c) + if sess == nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + sess.Delete("key") + return c.SendStatus(fiber.StatusOK) + }) + + app.Post("/reset", func(c fiber.Ctx) error { + sess := FromContext(c) + if sess == nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + if err := sess.Reset(); err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + return c.SendStatus(fiber.StatusOK) + }) + + app.Post("/destroy", func(c fiber.Ctx) error { + sess := FromContext(c) + if sess == nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + if err := sess.Destroy(); err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + return c.SendStatus(fiber.StatusOK) + }) + + app.Post("/fresh", func(c fiber.Ctx) error { + sess := FromContext(c) + if sess == nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + // Reset the session to make it fresh + if err := sess.Reset(); err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + if sess.Fresh() { + return c.SendStatus(fiber.StatusOK) + } + return c.SendStatus(fiber.StatusInternalServerError) + }) + + // Test GET, SET, DELETE, RESET, DESTROY by sending requests to the respective routes + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.SetRequestURI("/get") + h := app.Handler() + h(ctx) + require.Equal(t, fiber.StatusNotFound, ctx.Response.StatusCode()) + token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + require.NotEmpty(t, token, "Expected Set-Cookie header to be present") + tokenParts := strings.SplitN(strings.SplitN(token, ";", 2)[0], "=", 2) + require.Len(t, tokenParts, 2, "Expected Set-Cookie header to contain a token") + token = tokenParts[1] + require.Equal(t, "key not found", string(ctx.Response.Body())) + + // Test POST /set + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.SetRequestURI("/set") + ctx.Request.Header.Set("Content-Type", "application/x-www-form-urlencoded") // Set the Content-Type + ctx.Request.SetBodyString("value=hello") + ctx.Request.Header.SetCookie("session_id", token) + h(ctx) + require.Equal(t, fiber.StatusOK, ctx.Response.StatusCode()) + + // Test GET /get to check if the value was set + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.SetRequestURI("/get") + ctx.Request.Header.SetCookie("session_id", token) + h(ctx) + require.Equal(t, fiber.StatusOK, ctx.Response.StatusCode()) + require.Equal(t, "value=hello", string(ctx.Response.Body())) + + // Test POST /delete to delete the value + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.SetRequestURI("/delete") + ctx.Request.Header.SetCookie("session_id", token) + h(ctx) + require.Equal(t, fiber.StatusOK, ctx.Response.StatusCode()) + + // Test GET /get to check if the value was deleted + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.SetRequestURI("/get") + ctx.Request.Header.SetCookie("session_id", token) + h(ctx) + require.Equal(t, fiber.StatusNotFound, ctx.Response.StatusCode()) + require.Equal(t, "key not found", string(ctx.Response.Body())) + + // Test POST /reset to reset the session + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.SetRequestURI("/reset") + ctx.Request.Header.SetCookie("session_id", token) + h(ctx) + require.Equal(t, fiber.StatusOK, ctx.Response.StatusCode()) + // verify we have a new session token + newToken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + require.NotEmpty(t, newToken, "Expected Set-Cookie header to be present") + newTokenParts := strings.SplitN(strings.SplitN(newToken, ";", 2)[0], "=", 2) + require.Len(t, newTokenParts, 2, "Expected Set-Cookie header to contain a token") + newToken = newTokenParts[1] + require.NotEqual(t, token, newToken) + token = newToken + + // Test POST /destroy to destroy the session + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.SetRequestURI("/destroy") + ctx.Request.Header.SetCookie("session_id", token) + h(ctx) + require.Equal(t, fiber.StatusOK, ctx.Response.StatusCode()) + + // Verify the session cookie is set to expire + setCookieHeader := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + require.Contains(t, setCookieHeader, "expires=") + cookieParts := strings.Split(setCookieHeader, ";") + expired := false + for _, part := range cookieParts { + if strings.Contains(part, "expires=") { + part = strings.TrimSpace(part) + expiryDateStr := strings.TrimPrefix(part, "expires=") + // Correctly parse the date with "GMT" timezone + expiryDate, err := time.Parse(time.RFC1123, strings.TrimSpace(expiryDateStr)) + require.NoError(t, err) + if expiryDate.Before(time.Now()) { + expired = true + break + } + } + } + require.True(t, expired, "Session cookie should be expired") + + // Sleep so that the session expires + time.Sleep(1 * time.Second) + + // Test GET /get to check if the session was destroyed + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.SetRequestURI("/get") + ctx.Request.Header.SetCookie("session_id", token) + h(ctx) + require.Equal(t, fiber.StatusNotFound, ctx.Response.StatusCode()) + // check that we have a new session token + newToken = string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + require.NotEmpty(t, newToken, "Expected Set-Cookie header to be present") + parts := strings.Split(newToken, ";") + require.Greater(t, len(parts), 1) + valueParts := strings.Split(parts[0], "=") + require.Greater(t, len(valueParts), 1) + newToken = valueParts[1] + require.NotEqual(t, token, newToken) + token = newToken + + // Test POST /fresh to check if the session is fresh + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.SetRequestURI("/fresh") + ctx.Request.Header.SetCookie("session_id", token) + h(ctx) + require.Equal(t, fiber.StatusOK, ctx.Response.StatusCode()) + // check that we have a new session token + newToken = string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + require.NotEmpty(t, newToken, "Expected Set-Cookie header to be present") + newTokenParts = strings.SplitN(strings.SplitN(newToken, ";", 2)[0], "=", 2) + require.Len(t, newTokenParts, 2, "Expected Set-Cookie header to contain a token") + newToken = newTokenParts[1] + require.NotEqual(t, token, newToken) +} + +func Test_Session_NewWithStore(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New()) + + app.Get("/", func(c fiber.Ctx) error { + sess := FromContext(c) + id := sess.ID() + return c.SendString("value=" + id) + }) + app.Post("/", func(c fiber.Ctx) error { + sess := FromContext(c) + id := sess.ID() + c.Cookie(&fiber.Cookie{ + Name: "session_id", + Value: id, + }) + return nil + }) + + h := app.Handler() + + // Test GET request without cookie + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) + require.Equal(t, fiber.StatusOK, ctx.Response.StatusCode()) + // Get session cookie + token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + require.NotEmpty(t, token, "Expected Set-Cookie header to be present") + tokenParts := strings.SplitN(strings.SplitN(token, ";", 2)[0], "=", 2) + require.Len(t, tokenParts, 2, "Expected Set-Cookie header to contain a token") + token = tokenParts[1] + require.Equal(t, "value="+token, string(ctx.Response.Body())) + + // Test GET request with cookie + ctx = &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.Header.SetCookie("session_id", token) + h(ctx) + require.Equal(t, fiber.StatusOK, ctx.Response.StatusCode()) + require.Equal(t, "value="+token, string(ctx.Response.Body())) +} + +func Test_Session_FromSession(t *testing.T) { + t.Parallel() + app := fiber.New() + + sess := FromContext(app.AcquireCtx(&fasthttp.RequestCtx{})) + require.Nil(t, sess) + + app.Use(New()) +} + +func Test_Session_WithConfig(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + Next: func(c fiber.Ctx) bool { + return c.Get("key") == "value" + }, + IdleTimeout: 1 * time.Second, + KeyLookup: "cookie:session_id_test", + KeyGenerator: func() string { + return "test" + }, + source: "cookie_test", + sessionName: "session_id_test", + })) + + app.Get("/", func(c fiber.Ctx) error { + sess := FromContext(c) + id := sess.ID() + return c.SendString("value=" + id) + }) + + app.Get("/isFresh", func(c fiber.Ctx) error { + sess := FromContext(c) + if sess.Fresh() { + return c.SendStatus(fiber.StatusOK) + } + return c.SendStatus(fiber.StatusInternalServerError) + }) + + app.Post("/", func(c fiber.Ctx) error { + sess := FromContext(c) + id := sess.ID() + c.Cookie(&fiber.Cookie{ + Name: "session_id_test", + Value: id, + }) + return nil + }) + + h := app.Handler() + + // Test GET request without cookie + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) + require.Equal(t, fiber.StatusOK, ctx.Response.StatusCode()) + // Get session cookie + token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + require.NotEmpty(t, token, "Expected Set-Cookie header to be present") + tokenParts := strings.SplitN(strings.SplitN(token, ";", 2)[0], "=", 2) + require.Len(t, tokenParts, 2, "Expected Set-Cookie header to contain a token") + token = tokenParts[1] + require.Equal(t, "value="+token, string(ctx.Response.Body())) + + // Test GET request with cookie + ctx = &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.Header.SetCookie("session_id_test", token) + h(ctx) + require.Equal(t, fiber.StatusOK, ctx.Response.StatusCode()) + require.Equal(t, "value="+token, string(ctx.Response.Body())) + + // Test POST request with cookie + ctx = &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.SetCookie("session_id_test", token) + h(ctx) + require.Equal(t, fiber.StatusOK, ctx.Response.StatusCode()) + + // Test POST request without cookie + ctx = &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodPost) + h(ctx) + require.Equal(t, fiber.StatusOK, ctx.Response.StatusCode()) + + // Test POST request with wrong key + ctx = &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.SetCookie("session_id", token) + h(ctx) + require.Equal(t, fiber.StatusOK, ctx.Response.StatusCode()) + + // Test POST request with wrong value + ctx = &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.SetCookie("session_id_test", "wrong") + h(ctx) + require.Equal(t, fiber.StatusOK, ctx.Response.StatusCode()) + + // Check idle timeout not expired + ctx = &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.Header.SetCookie("session_id_test", token) + ctx.Request.SetRequestURI("/isFresh") + h(ctx) + require.Equal(t, fiber.StatusInternalServerError, ctx.Response.StatusCode()) + + // Test idle timeout + time.Sleep(1200 * time.Millisecond) + ctx = &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.Header.SetCookie("session_id_test", token) + ctx.Request.SetRequestURI("/isFresh") + h(ctx) + require.Equal(t, fiber.StatusOK, ctx.Response.StatusCode()) +} + +func Test_Session_Next(t *testing.T) { + t.Parallel() + + var ( + doNext bool + muNext sync.RWMutex + ) + + app := fiber.New() + + app.Use(New(Config{ + Next: func(_ fiber.Ctx) bool { + muNext.RLock() + defer muNext.RUnlock() + return doNext + }, + })) + + app.Get("/", func(c fiber.Ctx) error { + sess := FromContext(c) + if sess == nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + id := sess.ID() + return c.SendString("value=" + id) + }) + + h := app.Handler() + + // Test with Next returning false + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) + require.Equal(t, fiber.StatusOK, ctx.Response.StatusCode()) + // Get session cookie + token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + require.NotEmpty(t, token, "Expected Set-Cookie header to be present") + tokenParts := strings.SplitN(strings.SplitN(token, ";", 2)[0], "=", 2) + require.Len(t, tokenParts, 2, "Expected Set-Cookie header to contain a token") + token = tokenParts[1] + require.Equal(t, "value="+token, string(ctx.Response.Body())) + + // Test with Next returning true + muNext.Lock() + doNext = true + muNext.Unlock() + + ctx = &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) + require.Equal(t, fiber.StatusInternalServerError, ctx.Response.StatusCode()) +} + +func Test_Session_Middleware_Store(t *testing.T) { + t.Parallel() + app := fiber.New() + + handler, sessionStore := NewWithStore() + + app.Use(handler) + + app.Get("/", func(c fiber.Ctx) error { + sess := FromContext(c) + st := sess.Store() + if st != sessionStore { + return c.SendStatus(fiber.StatusInternalServerError) + } + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + + // Test GET request + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) + require.Equal(t, fiber.StatusOK, ctx.Response.StatusCode()) +} diff --git a/middleware/session/session.go b/middleware/session/session.go index 8a16590064..ffb5c52722 100644 --- a/middleware/session/session.go +++ b/middleware/session/session.go @@ -12,95 +12,177 @@ import ( "github.com/valyala/fasthttp" ) +// Session represents a user session. type Session struct { - ctx fiber.Ctx // fiber context - config *Store // store configuration - data *data // key value data - byteBuffer *bytes.Buffer // byte buffer for the en- and decode - id string // session id - exp time.Duration // expiration of this session - mu sync.RWMutex // Mutex to protect non-data fields - fresh bool // if new session + ctx fiber.Ctx // fiber context + config *Store // store configuration + data *data // key value data + id string // session id + idleTimeout time.Duration // idleTimeout of this session + mu sync.RWMutex // Mutex to protect non-data fields + fresh bool // if new session +} + +type absExpirationKeyType int + +const ( + // sessionIDContextKey is the key used to store the session ID in the context locals. + absExpirationKey absExpirationKeyType = iota +) + +// Session pool for reusing byte buffers. +var byteBufferPool = sync.Pool{ + New: func() any { + return new(bytes.Buffer) + }, } var sessionPool = sync.Pool{ New: func() any { - return new(Session) + return &Session{} }, } +// acquireSession returns a new Session from the pool. +// +// Returns: +// - *Session: The session object. +// +// Usage: +// +// s := acquireSession() func acquireSession() *Session { s := sessionPool.Get().(*Session) //nolint:forcetypeassert,errcheck // We store nothing else in the pool if s.data == nil { s.data = acquireData() } - if s.byteBuffer == nil { - s.byteBuffer = new(bytes.Buffer) - } s.fresh = true return s } +// Release releases the session back to the pool. +// +// This function should be called after the session is no longer needed. +// This function is used to reduce the number of allocations and +// to improve the performance of the session store. +// +// The session should not be used after calling this function. +// +// Important: The Release function should only be used when accessing the session directly, +// for example, when you have called func (s *Session) Get(ctx) to get the session. +// It should not be used when using the session with a *Middleware handler in the request +// call stack, as the middleware will still need to access the session. +// +// Usage: +// +// sess := session.Get(ctx) +// defer sess.Release() +func (s *Session) Release() { + if s == nil { + return + } + releaseSession(s) +} + func releaseSession(s *Session) { s.mu.Lock() s.id = "" - s.exp = 0 + s.idleTimeout = 0 s.ctx = nil s.config = nil if s.data != nil { s.data.Reset() } - if s.byteBuffer != nil { - s.byteBuffer.Reset() - } s.mu.Unlock() sessionPool.Put(s) } -// Fresh is true if the current session is new +// Fresh returns whether the session is new +// +// Returns: +// - bool: True if the session is fresh, otherwise false. +// +// Usage: +// +// isFresh := s.Fresh() func (s *Session) Fresh() bool { s.mu.RLock() defer s.mu.RUnlock() return s.fresh } -// ID returns the session id +// ID returns the session ID +// +// Returns: +// - string: The session ID. +// +// Usage: +// +// id := s.ID() func (s *Session) ID() string { s.mu.RLock() defer s.mu.RUnlock() return s.id } -// Get will return the value -func (s *Session) Get(key string) any { - // Better safe than sorry +// Get returns the value associated with the given key. +// +// Parameters: +// - key: The key to retrieve. +// +// Returns: +// - any: The value associated with the key. +// +// Usage: +// +// value := s.Get("key") +func (s *Session) Get(key any) any { if s.data == nil { return nil } return s.data.Get(key) } -// Set will update or create a new key value -func (s *Session) Set(key string, val any) { - // Better safe than sorry +// Set updates or creates a new key-value pair in the session. +// +// Parameters: +// - key: The key to set. +// - val: The value to set. +// +// Usage: +// +// s.Set("key", "value") +func (s *Session) Set(key, val any) { if s.data == nil { return } s.data.Set(key, val) } -// Delete will delete the value -func (s *Session) Delete(key string) { - // Better safe than sorry +// Delete removes the key-value pair from the session. +// +// Parameters: +// - key: The key to delete. +// +// Usage: +// +// s.Delete("key") +func (s *Session) Delete(key any) { if s.data == nil { return } s.data.Delete(key) } -// Destroy will delete the session from Storage and expire session cookie +// Destroy deletes the session from storage and expires the session cookie. +// +// Returns: +// - error: An error if the destruction fails. +// +// Usage: +// +// err := s.Destroy() func (s *Session) Destroy() error { - // Better safe than sorry if s.data == nil { return nil } @@ -121,7 +203,14 @@ func (s *Session) Destroy() error { return nil } -// Regenerate generates a new session id and delete the old one from Storage +// Regenerate generates a new session id and deletes the old one from storage. +// +// Returns: +// - error: An error if the regeneration fails. +// +// Usage: +// +// err := s.Regenerate() func (s *Session) Regenerate() error { s.mu.Lock() defer s.mu.Unlock() @@ -137,7 +226,14 @@ func (s *Session) Regenerate() error { return nil } -// Reset generates a new session id, deletes the old one from storage, and resets the associated data +// Reset generates a new session id, deletes the old one from storage, and resets the associated data. +// +// Returns: +// - error: An error if the reset fails. +// +// Usage: +// +// err := s.Reset() func (s *Session) Reset() error { // Reset local data if s.data != nil { @@ -147,12 +243,8 @@ func (s *Session) Reset() error { s.mu.Lock() defer s.mu.Unlock() - // Reset byte buffer - if s.byteBuffer != nil { - s.byteBuffer.Reset() - } // Reset expiration - s.exp = 0 + s.idleTimeout = 0 // Delete old id from storage if err := s.config.Storage.Delete(s.id); err != nil { @@ -168,75 +260,102 @@ func (s *Session) Reset() error { return nil } -// refresh generates a new session, and set session.fresh to be true +// refresh generates a new session, and sets session.fresh to be true. func (s *Session) refresh() { s.id = s.config.KeyGenerator() s.fresh = true } -// Save will update the storage and client cookie +// Save saves the session data and updates the cookie // -// sess.Save() will save the session data to the storage and update the -// client cookie, and it will release the session after saving. +// Note: If the session is being used in the handler, calling Save will have +// no effect and the session will automatically be saved when the handler returns. // -// It's not safe to use the session after calling Save(). +// Returns: +// - error: An error if the save operation fails. +// +// Usage: +// +// err := s.Save() func (s *Session) Save() error { - // Better safe than sorry + if s.ctx == nil { + return s.saveSession() + } + + // If the session is being used in the handler, it should not be saved + if m, ok := s.ctx.Locals(middlewareContextKey).(*Middleware); ok { + if m.Session == s { + // Session is in use, so we do nothing and return + return nil + } + } + + return s.saveSession() +} + +// saveSession encodes session data to saves it to storage. +func (s *Session) saveSession() error { if s.data == nil { return nil } s.mu.Lock() + defer s.mu.Unlock() - // Check if session has your own expiration, otherwise use default value - if s.exp <= 0 { - s.exp = s.config.Expiration + // Set idleTimeout if not already set + if s.idleTimeout <= 0 { + s.idleTimeout = s.config.IdleTimeout } // Update client cookie s.setSession() - // Convert data to bytes - encCache := gob.NewEncoder(s.byteBuffer) - err := encCache.Encode(&s.data.Data) + // Encode session data + s.data.RLock() + encodedBytes, err := s.encodeSessionData() + s.data.RUnlock() if err != nil { return fmt.Errorf("failed to encode data: %w", err) } - // Copy the data in buffer - encodedBytes := make([]byte, s.byteBuffer.Len()) - copy(encodedBytes, s.byteBuffer.Bytes()) - // Pass copied bytes with session id to provider - if err := s.config.Storage.Set(s.id, encodedBytes, s.exp); err != nil { - return err - } - - s.mu.Unlock() - - // Release session - // TODO: It's not safe to use the Session after calling Save() - releaseSession(s) - - return nil + return s.config.Storage.Set(s.id, encodedBytes, s.idleTimeout) } -// Keys will retrieve all keys in current session -func (s *Session) Keys() []string { +// Keys retrieves all keys in the current session. +// +// Returns: +// - []string: A slice of all keys in the session. +// +// Usage: +// +// keys := s.Keys() +func (s *Session) Keys() []any { if s.data == nil { - return []string{} + return []any{} } return s.data.Keys() } -// SetExpiry sets a specific expiration for this session -func (s *Session) SetExpiry(exp time.Duration) { +// SetIdleTimeout used when saving the session on the next call to `Save()`. +// +// Parameters: +// - idleTimeout: The duration for the idle timeout. +// +// Usage: +// +// s.SetIdleTimeout(time.Hour) +func (s *Session) SetIdleTimeout(idleTimeout time.Duration) { s.mu.Lock() defer s.mu.Unlock() - s.exp = exp + s.idleTimeout = idleTimeout } func (s *Session) setSession() { + if s.ctx == nil { + return + } + if s.config.source == SourceHeader { s.ctx.Request().Header.SetBytesV(s.config.sessionName, []byte(s.id)) s.ctx.Response().Header.SetBytesV(s.config.sessionName, []byte(s.id)) @@ -249,8 +368,8 @@ func (s *Session) setSession() { // Cookies are also session cookies if they do not specify the Expires or Max-Age attribute. // refer: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie if !s.config.CookieSessionOnly { - fcookie.SetMaxAge(int(s.exp.Seconds())) - fcookie.SetExpire(time.Now().Add(s.exp)) + fcookie.SetMaxAge(int(s.idleTimeout.Seconds())) + fcookie.SetExpire(time.Now().Add(s.idleTimeout)) } fcookie.SetSecure(s.config.CookieSecure) fcookie.SetHTTPOnly(s.config.CookieHTTPOnly) @@ -269,6 +388,10 @@ func (s *Session) setSession() { } func (s *Session) delSession() { + if s.ctx == nil { + return + } + if s.config.source == SourceHeader { s.ctx.Request().Header.Del(s.config.sessionName) s.ctx.Response().Header.Del(s.config.sessionName) @@ -299,12 +422,92 @@ func (s *Session) delSession() { } } -// decodeSessionData decodes the session data from raw bytes. +// decodeSessionData decodes session data from raw bytes +// +// Parameters: +// - rawData: The raw byte data to decode. +// +// Returns: +// - error: An error if the decoding fails. +// +// Usage: +// +// err := s.decodeSessionData(rawData) func (s *Session) decodeSessionData(rawData []byte) error { - _, _ = s.byteBuffer.Write(rawData) - encCache := gob.NewDecoder(s.byteBuffer) - if err := encCache.Decode(&s.data.Data); err != nil { + byteBuffer := byteBufferPool.Get().(*bytes.Buffer) //nolint:forcetypeassert,errcheck // We store nothing else in the pool + defer byteBufferPool.Put(byteBuffer) + defer byteBuffer.Reset() + _, _ = byteBuffer.Write(rawData) + decCache := gob.NewDecoder(byteBuffer) + if err := decCache.Decode(&s.data.Data); err != nil { return fmt.Errorf("failed to decode session data: %w", err) } return nil } + +// encodeSessionData encodes session data to raw bytes +// +// Parameters: +// - rawData: The raw byte data to encode. +// +// Returns: +// - error: An error if the encoding fails. +// +// Usage: +// +// err := s.encodeSessionData(rawData) +func (s *Session) encodeSessionData() ([]byte, error) { + byteBuffer := byteBufferPool.Get().(*bytes.Buffer) //nolint:forcetypeassert,errcheck // We store nothing else in the pool + defer byteBufferPool.Put(byteBuffer) + defer byteBuffer.Reset() + encCache := gob.NewEncoder(byteBuffer) + if err := encCache.Encode(&s.data.Data); err != nil { + return nil, fmt.Errorf("failed to encode session data: %w", err) + } + // Copy the bytes + // Copy the data in buffer + encodedBytes := make([]byte, byteBuffer.Len()) + copy(encodedBytes, byteBuffer.Bytes()) + + return encodedBytes, nil +} + +// absExpiration returns the session absolute expiration time or a zero time if not set. +// +// Returns: +// - time.Time: The session absolute expiration time. Zero time if not set. +// +// Usage: +// +// expiration := s.absExpiration() +func (s *Session) absExpiration() time.Time { + absExpiration, ok := s.Get(absExpirationKey).(time.Time) + if ok { + return absExpiration + } + return time.Time{} +} + +// isAbsExpired returns true if the session is expired. +// +// If the session has an absolute expiration time set, this function will return true if the +// current time is after the absolute expiration time. +// +// Returns: +// - bool: True if the session is expired, otherwise false. +func (s *Session) isAbsExpired() bool { + absExpiration := s.absExpiration() + return !absExpiration.IsZero() && time.Now().After(absExpiration) +} + +// setAbsoluteExpiration sets the absolute session expiration time. +// +// Parameters: +// - expiration: The session expiration time. +// +// Usage: +// +// s.setExpiration(time.Now().Add(time.Hour)) +func (s *Session) setAbsExpiration(absExpiration time.Time) { + s.Set(absExpirationKey, absExpiration) +} diff --git a/middleware/session/session_test.go b/middleware/session/session_test.go index fa12d690a6..038bfc4b8d 100644 --- a/middleware/session/session_test.go +++ b/middleware/session/session_test.go @@ -8,6 +8,7 @@ import ( "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/internal/storage/memory" + "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/valyala/fasthttp" ) @@ -17,14 +18,13 @@ func Test_Session(t *testing.T) { t.Parallel() // session store - store := New() + store := NewStore() // fiber instance app := fiber.New() // fiber context ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) // Get a new session sess, err := store.Get(ctx) @@ -33,6 +33,7 @@ func Test_Session(t *testing.T) { token := sess.ID() require.NoError(t, sess.Save()) + sess.Release() app.ReleaseCtx(ctx) ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) @@ -46,7 +47,7 @@ func Test_Session(t *testing.T) { // get keys keys := sess.Keys() - require.Equal(t, []string{}, keys) + require.Equal(t, []any{}, keys) // get value name := sess.Get("name") @@ -60,7 +61,7 @@ func Test_Session(t *testing.T) { require.Equal(t, "john", name) keys = sess.Keys() - require.Equal(t, []string{"name"}, keys) + require.Equal(t, []any{"name"}, keys) // delete key sess.Delete("name") @@ -71,7 +72,7 @@ func Test_Session(t *testing.T) { // get keys keys = sess.Keys() - require.Equal(t, []string{}, keys) + require.Equal(t, []any{}, keys) // get id id := sess.ID() @@ -81,6 +82,9 @@ func Test_Session(t *testing.T) { err = sess.Save() require.NoError(t, err) + // release the session + sess.Release() + // release the context app.ReleaseCtx(ctx) // requesting entirely new context to prevent falsy tests @@ -93,6 +97,8 @@ func Test_Session(t *testing.T) { // this id should be randomly generated as session key was deleted require.Len(t, sess.ID(), 36) + sess.Release() + // when we use the original session for the second time // the session be should be same if the session is not expired app.ReleaseCtx(ctx) @@ -102,6 +108,7 @@ func Test_Session(t *testing.T) { // request the server with the old session ctx.Request().Header.SetCookie(store.sessionName, id) sess, err = store.Get(ctx) + defer sess.Release() require.NoError(t, err) require.False(t, sess.Fresh()) require.Equal(t, sess.id, id) @@ -112,7 +119,7 @@ func Test_Session_Types(t *testing.T) { t.Parallel() // session store - store := New() + store := NewStore() // fiber instance app := fiber.New() @@ -186,6 +193,7 @@ func Test_Session_Types(t *testing.T) { err = sess.Save() require.NoError(t, err) + sess.Release() app.ReleaseCtx(ctx) ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) @@ -277,6 +285,8 @@ func Test_Session_Types(t *testing.T) { require.True(t, ok) require.Equal(t, vcomplex128, vcomplex128Result) + sess.Release() + app.ReleaseCtx(ctx) } @@ -284,7 +294,7 @@ func Test_Session_Types(t *testing.T) { func Test_Session_Store_Reset(t *testing.T) { t.Parallel() // session store - store := New() + store := NewStore() // fiber instance app := fiber.New() // fiber context @@ -304,6 +314,7 @@ func Test_Session_Store_Reset(t *testing.T) { require.NoError(t, store.Reset()) id := sess.ID() + sess.Release() app.ReleaseCtx(ctx) ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(ctx) @@ -311,11 +322,187 @@ func Test_Session_Store_Reset(t *testing.T) { // make sure the session is recreated sess, err = store.Get(ctx) + defer sess.Release() require.NoError(t, err) require.True(t, sess.Fresh()) require.Nil(t, sess.Get("hello")) } +func Test_Session_KeyTypes(t *testing.T) { + t.Parallel() + + // session store + store := NewStore() + // fiber instance + app := fiber.New() + // fiber context + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + + // get session + sess, err := store.Get(ctx) + require.NoError(t, err) + require.True(t, sess.Fresh()) + + type Person struct { + Name string + } + + type unexportedKey int + + // register non-default types + store.RegisterType(Person{}) + store.RegisterType(unexportedKey(0)) + + type unregisteredKeyType int + type unregisteredValueType int + + // verify unregistered keys types are not allowed + var ( + unregisteredKey unregisteredKeyType + unregisteredValue unregisteredValueType + ) + sess.Set(unregisteredKey, "test") + err = sess.Save() + require.Error(t, err) + sess.Delete(unregisteredKey) + err = sess.Save() + require.NoError(t, err) + sess.Set("abc", unregisteredValue) + err = sess.Save() + require.Error(t, err) + sess.Delete("abc") + err = sess.Save() + require.NoError(t, err) + + require.NoError(t, sess.Reset()) + + var ( + kbool = true + kstring = "str" + kint = 13 + kint8 int8 = 13 + kint16 int16 = 13 + kint32 int32 = 13 + kint64 int64 = 13 + kuint uint = 13 + kuint8 uint8 = 13 + kuint16 uint16 = 13 + kuint32 uint32 = 13 + kuint64 uint64 = 13 + kuintptr uintptr = 13 + kbyte byte = 'k' + krune = 'k' + kfloat32 float32 = 13 + kfloat64 float64 = 13 + kcomplex64 complex64 = 13 + kcomplex128 complex128 = 13 + kuser = Person{Name: "John"} + kunexportedKey = unexportedKey(13) + ) + + var ( + vbool = true + vstring = "str" + vint = 13 + vint8 int8 = 13 + vint16 int16 = 13 + vint32 int32 = 13 + vint64 int64 = 13 + vuint uint = 13 + vuint8 uint8 = 13 + vuint16 uint16 = 13 + vuint32 uint32 = 13 + vuint64 uint64 = 13 + vuintptr uintptr = 13 + vbyte byte = 'k' + vrune = 'k' + vfloat32 float32 = 13 + vfloat64 float64 = 13 + vcomplex64 complex64 = 13 + vcomplex128 complex128 = 13 + vuser = Person{Name: "John"} + vunexportedKey = unexportedKey(13) + ) + + keys := []any{ + kbool, + kstring, + kint, + kint8, + kint16, + kint32, + kint64, + kuint, + kuint8, + kuint16, + kuint32, + kuint64, + kuintptr, + kbyte, + krune, + kfloat32, + kfloat64, + kcomplex64, + kcomplex128, + kuser, + kunexportedKey, + } + + values := []any{ + vbool, + vstring, + vint, + vint8, + vint16, + vint32, + vint64, + vuint, + vuint8, + vuint16, + vuint32, + vuint64, + vuintptr, + vbyte, + vrune, + vfloat32, + vfloat64, + vcomplex64, + vcomplex128, + vuser, + vunexportedKey, + } + + // loop test all key value pairs + for i, key := range keys { + sess.Set(key, values[i]) + } + + id := sess.ID() + ctx.Request().Header.SetCookie(store.sessionName, id) + // save session + err = sess.Save() + require.NoError(t, err) + + sess.Release() + app.ReleaseCtx(ctx) + ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + ctx.Request().Header.SetCookie(store.sessionName, id) + + // get session + sess, err = store.Get(ctx) + require.NoError(t, err) + defer sess.Release() + require.False(t, sess.Fresh()) + + // loop test all key value pairs + for i, key := range keys { + // get value + result := sess.Get(key) + require.Equal(t, values[i], result) + } +} + // go test -run Test_Session_Save func Test_Session_Save(t *testing.T) { t.Parallel() @@ -323,7 +510,7 @@ func Test_Session_Save(t *testing.T) { t.Run("save to cookie", func(t *testing.T) { t.Parallel() // session store - store := New() + store := NewStore() // fiber instance app := fiber.New() // fiber context @@ -338,12 +525,13 @@ func Test_Session_Save(t *testing.T) { // save session err = sess.Save() require.NoError(t, err) + sess.Release() }) t.Run("save to header", func(t *testing.T) { t.Parallel() // session store - store := New(Config{ + store := NewStore(Config{ KeyLookup: "header:session_id", }) // fiber instance @@ -363,10 +551,11 @@ func Test_Session_Save(t *testing.T) { require.NoError(t, err) require.Equal(t, store.getSessionID(ctx), string(ctx.Response().Header.Peek(store.sessionName))) require.Equal(t, store.getSessionID(ctx), string(ctx.Request().Header.Peek(store.sessionName))) + sess.Release() }) } -func Test_Session_Save_Expiration(t *testing.T) { +func Test_Session_Save_IdleTimeout(t *testing.T) { t.Parallel() t.Run("save to cookie", func(t *testing.T) { @@ -374,7 +563,7 @@ func Test_Session_Save_Expiration(t *testing.T) { const sessionDuration = 5 * time.Second // session store - store := New() + store := NewStore() // fiber instance app := fiber.New() // fiber context @@ -391,12 +580,13 @@ func Test_Session_Save_Expiration(t *testing.T) { token := sess.ID() // expire this session in 5 seconds - sess.SetExpiry(sessionDuration) + sess.SetIdleTimeout(sessionDuration) // save session err = sess.Save() require.NoError(t, err) + sess.Release() app.ReleaseCtx(ctx) ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) @@ -409,16 +599,103 @@ func Test_Session_Save_Expiration(t *testing.T) { // just to make sure the session has been expired time.Sleep(sessionDuration + (10 * time.Millisecond)) + sess.Release() + app.ReleaseCtx(ctx) ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(ctx) + // here you should get a new session + ctx.Request().Header.SetCookie(store.sessionName, token) + sess, err = store.Get(ctx) + defer sess.Release() + require.NoError(t, err) + require.Nil(t, sess.Get("name")) + require.NotEqual(t, sess.ID(), token) + }) +} + +func Test_Session_Save_AbsoluteTimeout(t *testing.T) { + t.Parallel() + + t.Run("save to cookie", func(t *testing.T) { + t.Parallel() + + const absoluteTimeout = 1 * time.Second + // session store + store := NewStore(Config{ + IdleTimeout: absoluteTimeout, + AbsoluteTimeout: absoluteTimeout, + }) + + // force change to IdleTimeout + store.Config.IdleTimeout = 10 * time.Second + + // fiber instance + app := fiber.New() + // fiber context + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + + // get session + sess, err := store.Get(ctx) + require.NoError(t, err) + + // set value + sess.Set("name", "john") + + token := sess.ID() + + // save session + err = sess.Save() + require.NoError(t, err) + + sess.Release() + app.ReleaseCtx(ctx) + ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) + + // here you need to get the old session yet + ctx.Request().Header.SetCookie(store.sessionName, token) + sess, err = store.Get(ctx) + require.NoError(t, err) + require.Equal(t, "john", sess.Get("name")) + + // just to make sure the session has been expired + time.Sleep(absoluteTimeout + (100 * time.Millisecond)) + + sess.Release() + + app.ReleaseCtx(ctx) + ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) + // here you should get a new session ctx.Request().Header.SetCookie(store.sessionName, token) sess, err = store.Get(ctx) require.NoError(t, err) require.Nil(t, sess.Get("name")) require.NotEqual(t, sess.ID(), token) + require.True(t, sess.Fresh()) + require.IsType(t, time.Time{}, sess.Get(absExpirationKey)) + + token = sess.ID() + + sess.Set("name", "john") + + // save session + err = sess.Save() + require.NoError(t, err) + + sess.Release() + app.ReleaseCtx(ctx) + + // just to make sure the session has been expired + time.Sleep(absoluteTimeout + (100 * time.Millisecond)) + + // try to get expired session by id + sess, err = store.GetByID(token) + require.Error(t, err) + require.ErrorIs(t, err, ErrSessionIDNotFoundInStore) + require.Nil(t, sess) }) } @@ -429,7 +706,7 @@ func Test_Session_Destroy(t *testing.T) { t.Run("destroy from cookie", func(t *testing.T) { t.Parallel() // session store - store := New() + store := NewStore() // fiber instance app := fiber.New() // fiber context @@ -438,6 +715,7 @@ func Test_Session_Destroy(t *testing.T) { // get session sess, err := store.Get(ctx) + defer sess.Release() require.NoError(t, err) sess.Set("name", "fenny") @@ -449,7 +727,7 @@ func Test_Session_Destroy(t *testing.T) { t.Run("destroy from header", func(t *testing.T) { t.Parallel() // session store - store := New(Config{ + store := NewStore(Config{ KeyLookup: "header:session_id", }) // fiber instance @@ -467,6 +745,7 @@ func Test_Session_Destroy(t *testing.T) { id := sess.ID() require.NoError(t, sess.Save()) + sess.Release() app.ReleaseCtx(ctx) ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(ctx) @@ -475,6 +754,7 @@ func Test_Session_Destroy(t *testing.T) { ctx.Request().Header.Set(store.sessionName, id) sess, err = store.Get(ctx) require.NoError(t, err) + defer sess.Release() err = sess.Destroy() require.NoError(t, err) @@ -487,19 +767,19 @@ func Test_Session_Destroy(t *testing.T) { func Test_Session_Custom_Config(t *testing.T) { t.Parallel() - store := New(Config{Expiration: time.Hour, KeyGenerator: func() string { return "very random" }}) - require.Equal(t, time.Hour, store.Expiration) + store := NewStore(Config{IdleTimeout: time.Hour, KeyGenerator: func() string { return "very random" }}) + require.Equal(t, time.Hour, store.IdleTimeout) require.Equal(t, "very random", store.KeyGenerator()) - store = New(Config{Expiration: 0}) - require.Equal(t, ConfigDefault.Expiration, store.Expiration) + store = NewStore(Config{IdleTimeout: 0}) + require.Equal(t, ConfigDefault.IdleTimeout, store.IdleTimeout) } // go test -run Test_Session_Cookie func Test_Session_Cookie(t *testing.T) { t.Parallel() // session store - store := New() + store := NewStore() // fiber instance app := fiber.New() // fiber context @@ -511,15 +791,19 @@ func Test_Session_Cookie(t *testing.T) { require.NoError(t, err) require.NoError(t, sess.Save()) + sess.Release() + // cookie should be set on Save ( even if empty data ) - require.Len(t, ctx.Response().Header.PeekCookie(store.sessionName), 84) + cookie := ctx.Response().Header.PeekCookie(store.sessionName) + require.NotNil(t, cookie) + require.Regexp(t, `^session_id=[a-f0-9\-]{36}; max-age=\d+; path=/; SameSite=Lax$`, string(cookie)) } // go test -run Test_Session_Cookie_In_Response // Regression: https://github.com/gofiber/fiber/pull/1191 func Test_Session_Cookie_In_Middleware_Chain(t *testing.T) { t.Parallel() - store := New() + store := NewStore() app := fiber.New() // fiber context @@ -534,8 +818,11 @@ func Test_Session_Cookie_In_Middleware_Chain(t *testing.T) { id := sess.ID() require.NoError(t, sess.Save()) + sess.Release() + sess, err = store.Get(ctx) require.NoError(t, err) + defer sess.Release() sess.Set("name", "john") require.True(t, sess.Fresh()) require.Equal(t, id, sess.ID()) // session id should be the same @@ -548,7 +835,7 @@ func Test_Session_Cookie_In_Middleware_Chain(t *testing.T) { // Regression: https://github.com/gofiber/fiber/issues/1365 func Test_Session_Deletes_Single_Key(t *testing.T) { t.Parallel() - store := New() + store := NewStore() app := fiber.New() ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) @@ -559,6 +846,7 @@ func Test_Session_Deletes_Single_Key(t *testing.T) { sess.Set("id", "1") require.NoError(t, sess.Save()) + sess.Release() app.ReleaseCtx(ctx) ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) ctx.Request().Header.SetCookie(store.sessionName, id) @@ -568,11 +856,13 @@ func Test_Session_Deletes_Single_Key(t *testing.T) { sess.Delete("id") require.NoError(t, sess.Save()) + sess.Release() app.ReleaseCtx(ctx) ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) ctx.Request().Header.SetCookie(store.sessionName, id) sess, err = store.Get(ctx) + defer sess.Release() require.NoError(t, err) require.False(t, sess.Fresh()) require.Nil(t, sess.Get("id")) @@ -587,7 +877,7 @@ func Test_Session_Reset(t *testing.T) { app := fiber.New() // session store - store := New() + store := NewStore() t.Run("reset session data and id, and set fresh to be true", func(t *testing.T) { t.Parallel() @@ -609,6 +899,7 @@ func Test_Session_Reset(t *testing.T) { err = freshSession.Save() require.NoError(t, err) + freshSession.Release() app.ReleaseCtx(ctx) ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) @@ -630,7 +921,7 @@ func Test_Session_Reset(t *testing.T) { // Check that the session data has been reset keys := acquiredSession.Keys() - require.Equal(t, []string{}, keys) + require.Equal(t, []any{}, keys) // Set a new value for 'name' and check that it's updated acquiredSession.Set("name", "john") @@ -641,6 +932,8 @@ func Test_Session_Reset(t *testing.T) { err = acquiredSession.Save() require.NoError(t, err) + acquiredSession.Release() + // Check that the session id is not in the header or cookie anymore require.Equal(t, "", string(ctx.Response().Header.Peek(store.sessionName))) require.Equal(t, "", string(ctx.Request().Header.Peek(store.sessionName))) @@ -658,7 +951,7 @@ func Test_Session_Regenerate(t *testing.T) { t.Run("set fresh to be true when regenerating a session", func(t *testing.T) { t.Parallel() // session store - store := New() + store := NewStore() // a random session uuid originalSessionUUIDString := "" // fiber context @@ -674,6 +967,8 @@ func Test_Session_Regenerate(t *testing.T) { err = freshSession.Save() require.NoError(t, err) + freshSession.Release() + // release the context app.ReleaseCtx(ctx) @@ -686,6 +981,7 @@ func Test_Session_Regenerate(t *testing.T) { // as the session is in the storage, session.fresh should be false acquiredSession, err := store.Get(ctx) require.NoError(t, err) + defer acquiredSession.Release() require.False(t, acquiredSession.Fresh()) err = acquiredSession.Regenerate() @@ -704,7 +1000,7 @@ func Test_Session_Regenerate(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Session -benchmem -count=4 func Benchmark_Session(b *testing.B) { b.Run("default", func(b *testing.B) { - app, store := fiber.New(), New() + app, store := fiber.New(), NewStore() c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) c.Request().Header.SetCookie(store.sessionName, "12356789") @@ -715,12 +1011,14 @@ func Benchmark_Session(b *testing.B) { sess, _ := store.Get(c) //nolint:errcheck // We're inside a benchmark sess.Set("john", "doe") _ = sess.Save() //nolint:errcheck // We're inside a benchmark + + sess.Release() } }) b.Run("storage", func(b *testing.B) { app := fiber.New() - store := New(Config{ + store := NewStore(Config{ Storage: memory.New(), }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) @@ -733,6 +1031,8 @@ func Benchmark_Session(b *testing.B) { sess, _ := store.Get(c) //nolint:errcheck // We're inside a benchmark sess.Set("john", "doe") _ = sess.Save() //nolint:errcheck // We're inside a benchmark + + sess.Release() } }) } @@ -740,7 +1040,7 @@ func Benchmark_Session(b *testing.B) { // go test -v -run=^$ -bench=Benchmark_Session_Parallel -benchmem -count=4 func Benchmark_Session_Parallel(b *testing.B) { b.Run("default", func(b *testing.B) { - app, store := fiber.New(), New() + app, store := fiber.New(), NewStore() b.ReportAllocs() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { @@ -751,6 +1051,9 @@ func Benchmark_Session_Parallel(b *testing.B) { sess, _ := store.Get(c) //nolint:errcheck // We're inside a benchmark sess.Set("john", "doe") _ = sess.Save() //nolint:errcheck // We're inside a benchmark + + sess.Release() + app.ReleaseCtx(c) } }) @@ -758,7 +1061,7 @@ func Benchmark_Session_Parallel(b *testing.B) { b.Run("storage", func(b *testing.B) { app := fiber.New() - store := New(Config{ + store := NewStore(Config{ Storage: memory.New(), }) b.ReportAllocs() @@ -771,6 +1074,9 @@ func Benchmark_Session_Parallel(b *testing.B) { sess, _ := store.Get(c) //nolint:errcheck // We're inside a benchmark sess.Set("john", "doe") _ = sess.Save() //nolint:errcheck // We're inside a benchmark + + sess.Release() + app.ReleaseCtx(c) } }) @@ -780,7 +1086,7 @@ func Benchmark_Session_Parallel(b *testing.B) { // go test -v -run=^$ -bench=Benchmark_Session_Asserted -benchmem -count=4 func Benchmark_Session_Asserted(b *testing.B) { b.Run("default", func(b *testing.B) { - app, store := fiber.New(), New() + app, store := fiber.New(), NewStore() c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) c.Request().Header.SetCookie(store.sessionName, "12356789") @@ -793,12 +1099,13 @@ func Benchmark_Session_Asserted(b *testing.B) { sess.Set("john", "doe") err = sess.Save() require.NoError(b, err) + sess.Release() } }) b.Run("storage", func(b *testing.B) { app := fiber.New() - store := New(Config{ + store := NewStore(Config{ Storage: memory.New(), }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) @@ -813,6 +1120,7 @@ func Benchmark_Session_Asserted(b *testing.B) { sess.Set("john", "doe") err = sess.Save() require.NoError(b, err) + sess.Release() } }) } @@ -820,7 +1128,7 @@ func Benchmark_Session_Asserted(b *testing.B) { // go test -v -run=^$ -bench=Benchmark_Session_Asserted_Parallel -benchmem -count=4 func Benchmark_Session_Asserted_Parallel(b *testing.B) { b.Run("default", func(b *testing.B) { - app, store := fiber.New(), New() + app, store := fiber.New(), NewStore() b.ReportAllocs() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { @@ -832,6 +1140,7 @@ func Benchmark_Session_Asserted_Parallel(b *testing.B) { require.NoError(b, err) sess.Set("john", "doe") require.NoError(b, sess.Save()) + sess.Release() app.ReleaseCtx(c) } }) @@ -839,7 +1148,7 @@ func Benchmark_Session_Asserted_Parallel(b *testing.B) { b.Run("storage", func(b *testing.B) { app := fiber.New() - store := New(Config{ + store := NewStore(Config{ Storage: memory.New(), }) b.ReportAllocs() @@ -853,6 +1162,7 @@ func Benchmark_Session_Asserted_Parallel(b *testing.B) { require.NoError(b, err) sess.Set("john", "doe") require.NoError(b, sess.Save()) + sess.Release() app.ReleaseCtx(c) } }) @@ -863,7 +1173,7 @@ func Benchmark_Session_Asserted_Parallel(b *testing.B) { func Test_Session_Concurrency(t *testing.T) { t.Parallel() app := fiber.New() - store := New() + store := NewStore() var wg sync.WaitGroup errChan := make(chan error, 10) // Buffered channel to collect errors @@ -877,7 +1187,7 @@ func Test_Session_Concurrency(t *testing.T) { localCtx := app.AcquireCtx(&fasthttp.RequestCtx{}) - sess, err := store.Get(localCtx) + sess, err := store.getSession(localCtx) if err != nil { errChan <- err return @@ -901,6 +1211,9 @@ func Test_Session_Concurrency(t *testing.T) { return } + // release the session + sess.Release() + // Release the context app.ReleaseCtx(localCtx) @@ -917,6 +1230,7 @@ func Test_Session_Concurrency(t *testing.T) { errChan <- err return } + defer sess.Release() // Get the value name := sess.Get("name") @@ -963,3 +1277,42 @@ func Test_Session_Concurrency(t *testing.T) { require.NoError(t, err) } } + +func Test_Session_StoreGetDecodeSessionDataError(t *testing.T) { + // Initialize a new store with default config + store := NewStore() + + // Create a new Fiber app + app := fiber.New() + + // Generate a fake session ID + sessionID := uuid.New().String() + + // Store invalid session data to simulate decode error + err := store.Storage.Set(sessionID, []byte("invalid data"), 0) + require.NoError(t, err, "Failed to set invalid session data") + + // Create a new request context + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + // Set the session ID in cookies + c.Request().Header.SetCookie(store.sessionName, sessionID) + + // Attempt to get the session + _, err = store.Get(c) + require.Error(t, err, "Expected error due to invalid session data, but got nil") + + // Check that the error message is as expected + require.Contains(t, err.Error(), "failed to decode session data", "Unexpected error message") + + // Check that the error is as expected + require.ErrorContains(t, err, "failed to decode session data", "Unexpected error") + + // Attempt to get the session by ID + _, err = store.GetByID(sessionID) + require.Error(t, err, "Expected error due to invalid session data, but got nil") + + // Check that the error message is as expected + require.ErrorContains(t, err, "failed to decode session data", "Unexpected error") +} diff --git a/middleware/session/store.go b/middleware/session/store.go index 01b4548c0a..013743d068 100644 --- a/middleware/session/store.go +++ b/middleware/session/store.go @@ -4,14 +4,20 @@ import ( "encoding/gob" "errors" "fmt" + "time" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/internal/storage/memory" + "github.com/gofiber/fiber/v3/log" "github.com/gofiber/utils/v2" ) // ErrEmptySessionID is an error that occurs when the session ID is empty. -var ErrEmptySessionID = errors.New("session id cannot be empty") +var ( + ErrEmptySessionID = errors.New("session ID cannot be empty") + ErrSessionAlreadyLoadedByMiddleware = errors.New("session already loaded by middleware") + ErrSessionIDNotFoundInStore = errors.New("session ID not found in session store") +) // sessionIDKey is the local key type used to store and retrieve the session ID in context. type sessionIDKey int @@ -26,7 +32,17 @@ type Store struct { } // New creates a new session store with the provided configuration. -func New(config ...Config) *Store { +// +// Parameters: +// - config: Variadic parameter to override default config. +// +// Returns: +// - *Store: The session store. +// +// Usage: +// +// store := session.New() +func NewStore(config ...Config) *Store { // Set default config cfg := configDefault(config...) @@ -34,18 +50,75 @@ func New(config ...Config) *Store { cfg.Storage = memory.New() } - return &Store{ + store := &Store{ Config: cfg, } + + if cfg.AbsoluteTimeout > 0 { + store.RegisterType(absExpirationKey) + store.RegisterType(time.Time{}) + } + + return store } // RegisterType registers a custom type for encoding/decoding into any storage provider. +// +// Parameters: +// - i: The custom type to register. +// +// Usage: +// +// store.RegisterType(MyCustomType{}) func (*Store) RegisterType(i any) { gob.Register(i) } -// Get retrieves or creates a session for the given context. +// Get will get/create a session. +// +// This function will return an ErrSessionAlreadyLoadedByMiddleware if +// the session is already loaded by the middleware. +// +// Parameters: +// - c: The Fiber context. +// +// Returns: +// - *Session: The session object. +// - error: An error if the session retrieval fails or if the session is already loaded by the middleware. +// +// Usage: +// +// sess, err := store.Get(c) +// if err != nil { +// // handle error +// } func (s *Store) Get(c fiber.Ctx) (*Session, error) { + // If session is already loaded in the context, + // it should not be loaded again + _, ok := c.Locals(middlewareContextKey).(*Middleware) + if ok { + return nil, ErrSessionAlreadyLoadedByMiddleware + } + + return s.getSession(c) +} + +// getSession retrieves a session based on the context. +// +// Parameters: +// - c: The Fiber context. +// +// Returns: +// - *Session: The session object. +// - error: An error if the session retrieval fails. +// +// Usage: +// +// sess, err := store.getSession(c) +// if err != nil { +// // handle error +// } +func (s *Store) getSession(c fiber.Ctx) (*Session, error) { var rawData []byte var err error @@ -79,7 +152,6 @@ func (s *Store) Get(c fiber.Ctx) (*Session, error) { sess := acquireSession() sess.mu.Lock() - defer sess.mu.Unlock() sess.ctx = c sess.config = s @@ -89,16 +161,40 @@ func (s *Store) Get(c fiber.Ctx) (*Session, error) { // Decode session data if found if rawData != nil { sess.data.Lock() - defer sess.data.Unlock() - if err := sess.decodeSessionData(rawData); err != nil { + err := sess.decodeSessionData(rawData) + sess.data.Unlock() + if err != nil { + sess.mu.Unlock() + sess.Release() return nil, fmt.Errorf("failed to decode session data: %w", err) } } + sess.mu.Unlock() + + if fresh && s.AbsoluteTimeout > 0 { + sess.setAbsExpiration(time.Now().Add(s.AbsoluteTimeout)) + } else if sess.isAbsExpired() { + if err := sess.Reset(); err != nil { + return nil, fmt.Errorf("failed to reset session: %w", err) + } + sess.setAbsExpiration(time.Now().Add(s.AbsoluteTimeout)) + } + return sess, nil } // getSessionID returns the session ID from cookies, headers, or query string. +// +// Parameters: +// - c: The Fiber context. +// +// Returns: +// - string: The session ID. +// +// Usage: +// +// id := store.getSessionID(c) func (s *Store) getSessionID(c fiber.Ctx) string { id := c.Cookies(s.sessionName) if len(id) > 0 { @@ -123,14 +219,113 @@ func (s *Store) getSessionID(c fiber.Ctx) string { } // Reset deletes all sessions from the storage. +// +// Returns: +// - error: An error if the reset operation fails. +// +// Usage: +// +// err := store.Reset() +// if err != nil { +// // handle error +// } func (s *Store) Reset() error { return s.Storage.Reset() } // Delete deletes a session by its ID. +// +// Parameters: +// - id: The unique identifier of the session. +// +// Returns: +// - error: An error if the deletion fails or if the session ID is empty. +// +// Usage: +// +// err := store.Delete(id) +// if err != nil { +// // handle error +// } func (s *Store) Delete(id string) error { if id == "" { return ErrEmptySessionID } return s.Storage.Delete(id) } + +// GetByID retrieves a session by its ID from the storage. +// If the session is not found, it returns nil and an error. +// +// Unlike session middleware methods, this function does not automatically: +// +// - Load the session into the request context. +// +// - Save the session data to the storage or update the client cookie. +// +// Important Notes: +// +// - The session object returned by GetByID does not have a context associated with it. +// +// - When using this method alongside session middleware, there is a potential for collisions, +// so be mindful of interactions between manually retrieved sessions and middleware-managed sessions. +// +// - If you modify a session returned by GetByID, you must call session.Save() to persist the changes. +// +// - When you are done with the session, you should call session.Release() to release the session back to the pool. +// +// Parameters: +// - id: The unique identifier of the session. +// +// Returns: +// - *Session: The session object if found, otherwise nil. +// - error: An error if the session retrieval fails or if the session ID is empty. +// +// Usage: +// +// sess, err := store.GetByID(id) +// if err != nil { +// // handle error +// } +func (s *Store) GetByID(id string) (*Session, error) { + if id == "" { + return nil, ErrEmptySessionID + } + + rawData, err := s.Storage.Get(id) + if err != nil { + return nil, err + } + if rawData == nil { + return nil, ErrSessionIDNotFoundInStore + } + + sess := acquireSession() + + sess.mu.Lock() + + sess.config = s + sess.id = id + sess.fresh = false + + sess.data.Lock() + decodeErr := sess.decodeSessionData(rawData) + sess.data.Unlock() + sess.mu.Unlock() + if decodeErr != nil { + sess.Release() + return nil, fmt.Errorf("failed to decode session data: %w", decodeErr) + } + + if s.AbsoluteTimeout > 0 { + if sess.isAbsExpired() { + if err := sess.Destroy(); err != nil { + sess.Release() + log.Errorf("failed to destroy session: %v", err) + } + return nil, ErrSessionIDNotFoundInStore + } + } + + return sess, nil +} diff --git a/middleware/session/store_test.go b/middleware/session/store_test.go index adce2e3488..8a45c7e5fb 100644 --- a/middleware/session/store_test.go +++ b/middleware/session/store_test.go @@ -20,9 +20,10 @@ func Test_Store_getSessionID(t *testing.T) { t.Run("from cookie", func(t *testing.T) { t.Parallel() // session store - store := New() + store := NewStore() // fiber context ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) // set cookie ctx.Request().Header.SetCookie(store.sessionName, expectedID) @@ -33,11 +34,12 @@ func Test_Store_getSessionID(t *testing.T) { t.Run("from header", func(t *testing.T) { t.Parallel() // session store - store := New(Config{ + store := NewStore(Config{ KeyLookup: "header:session_id", }) // fiber context ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) // set header ctx.Request().Header.Set(store.sessionName, expectedID) @@ -48,11 +50,12 @@ func Test_Store_getSessionID(t *testing.T) { t.Run("from url query", func(t *testing.T) { t.Parallel() // session store - store := New(Config{ + store := NewStore(Config{ KeyLookup: "query:session_id", }) // fiber context ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) // set url parameter ctx.Request().SetRequestURI(fmt.Sprintf("/path?%s=%s", store.sessionName, expectedID)) @@ -73,9 +76,10 @@ func Test_Store_Get(t *testing.T) { t.Run("session should be re-generated if it is invalid", func(t *testing.T) { t.Parallel() // session store - store := New() + store := NewStore() // fiber context ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) // set cookie ctx.Request().Header.SetCookie(store.sessionName, unexpectedID) @@ -93,10 +97,11 @@ func Test_Store_DeleteSession(t *testing.T) { // fiber instance app := fiber.New() // session store - store := New() + store := NewStore() // fiber context ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) // Create a new session session, err := store.Get(ctx) @@ -116,3 +121,105 @@ func Test_Store_DeleteSession(t *testing.T) { // The session ID should be different now, because the old session was deleted require.NotEqual(t, sessionID, session.ID()) } + +func TestStore_Get_SessionAlreadyLoaded(t *testing.T) { + // Create a new Fiber app + app := fiber.New() + + // Create a new context + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + + // Mock middleware and set it in the context + middleware := &Middleware{} + ctx.Locals(middlewareContextKey, middleware) + + // Create a new store + store := &Store{} + + // Call the Get method + sess, err := store.Get(ctx) + + // Assert that the error is ErrSessionAlreadyLoadedByMiddleware + require.Nil(t, sess) + require.Equal(t, ErrSessionAlreadyLoadedByMiddleware, err) +} + +func TestStore_Delete(t *testing.T) { + // Create a new store + store := NewStore() + + t.Run("delete with empty session ID", func(t *testing.T) { + err := store.Delete("") + require.Error(t, err) + require.Equal(t, ErrEmptySessionID, err) + }) + + t.Run("delete non-existing session", func(t *testing.T) { + err := store.Delete("non-existing-session-id") + require.NoError(t, err) + }) +} + +func Test_Store_GetByID(t *testing.T) { + t.Parallel() + // Create a new store + store := NewStore() + + t.Run("empty session ID", func(t *testing.T) { + t.Parallel() + sess, err := store.GetByID("") + require.Error(t, err) + require.Nil(t, sess) + require.Equal(t, ErrEmptySessionID, err) + }) + + t.Run("non-existent session ID", func(t *testing.T) { + t.Parallel() + sess, err := store.GetByID("non-existent-session-id") + require.Error(t, err) + require.Nil(t, sess) + require.Equal(t, ErrSessionIDNotFoundInStore, err) + }) + + t.Run("valid session ID", func(t *testing.T) { + t.Parallel() + app := fiber.New() + // Create a new session + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + session, err := store.Get(ctx) + defer session.Release() + defer app.ReleaseCtx(ctx) + require.NoError(t, err) + + // Save the session ID + sessionID := session.ID() + + // Save the session + err = session.Save() + require.NoError(t, err) + + // Retrieve the session by ID + retrievedSession, err := store.GetByID(sessionID) + require.NoError(t, err) + require.NotNil(t, retrievedSession) + require.Equal(t, sessionID, retrievedSession.ID()) + + // Call Save on the retrieved session + retrievedSession.Set("key", "value") + err = retrievedSession.Save() + require.NoError(t, err) + + // Call Other Session methods + require.Equal(t, "value", retrievedSession.Get("key")) + require.False(t, retrievedSession.Fresh()) + + require.NoError(t, retrievedSession.Reset()) + require.NoError(t, retrievedSession.Destroy()) + require.IsType(t, []any{}, retrievedSession.Keys()) + require.NoError(t, retrievedSession.Regenerate()) + require.NotPanics(t, func() { + retrievedSession.Release() + }) + }) +} From 3367ecfa5bdda81bebe7479549a4c8babac495c6 Mon Sep 17 00:00:00 2001 From: alequilesl <118303832+alequilesl@users.noreply.github.com> Date: Fri, 25 Oct 2024 22:28:07 -0400 Subject: [PATCH 14/31] balance brakets --- docs/whats_new.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/whats_new.md b/docs/whats_new.md index 53c2f9d8e6..540f82ad26 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -144,7 +144,6 @@ app.Route("/api").Route("/user/:id?") // Delete user return c.JSON(fiber.Map{"message": "User deleted", "id": c.Params("id")}) }) -}) ``` From 579d9a3f3dd4611780110e8f30e84024f2b58717 Mon Sep 17 00:00:00 2001 From: xEricL <37921711+xEricL@users.noreply.github.com> Date: Mon, 28 Oct 2024 03:11:24 -0400 Subject: [PATCH 15/31] =?UTF-8?q?=F0=9F=93=9A=20Doc:=20Clarify=20SendFile?= =?UTF-8?q?=20Docs=20(#3172)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📚 Doc: Clarify SendFile ContentType header set by file format * 📚 Doc: Make SendFile default value formatting consistent * 📚 Doc: Add missing `fiber.` in SendFile usage docs * 📚 Doc: Hyphenate 'case-sensitive' * 📚 Doc: Clarify `SendFile` behavior for missing/invalid file extensions * 🚨 Test: Validate `SendFile` Content-Type header --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> --- .github/testdata/fs/img/fiberpng | Bin 0 -> 1542 bytes .github/testdata/fs/img/fiberpng.jpeg | Bin 0 -> 1542 bytes .github/testdata/fs/img/fiberpng.notvalidext | Bin 0 -> 1542 bytes app.go | 2 +- app_test.go | 4 +- ctx.go | 17 ++++---- ctx_interface_gen.go | 7 +-- ctx_test.go | 43 +++++++++++++++++++ docs/api/ctx.md | 22 +++++----- router.go | 4 +- 10 files changed, 72 insertions(+), 27 deletions(-) create mode 100644 .github/testdata/fs/img/fiberpng create mode 100644 .github/testdata/fs/img/fiberpng.jpeg create mode 100644 .github/testdata/fs/img/fiberpng.notvalidext diff --git a/.github/testdata/fs/img/fiberpng b/.github/testdata/fs/img/fiberpng new file mode 100644 index 0000000000000000000000000000000000000000..fa981bcf261035838ef96c52ef4ae9339ca1e788 GIT binary patch literal 1542 zcmb7E`#aMM82`GJW=h3enp<|(j?0?d3#qx}X>DtSA}qHsp`1jAaM(C9BKKV9I*v`! zlv_5}VidXM9%&}z5-J@2fu5(&^S;mLeLv6Vecs#4);8|453)(Sv{H8Pa&<2yML-Oh`C@g@>s zjXR5esQ7LZXQdnlejIO-js*Y_O-s{r*7pSFo5FW5fw0`IQt48`z42FYAIGL;iiD$p z#<2E?+*N+a`p!xaG9kh-6S7+>gU)ta*|i(|B^<*vDgV@{2rOvy2Ks*jn{QfgMhd zUdV&-#afzSwu5wPQP~iS(g(XpO3vqLPaF-2sVjEk5T7gIxjRJK3)*@u*U6*Omrp%HsHi?OQ44Fk@t$x9`ealid73L|jHnlTUkLAT;wz znU)<8C%PQ*gOF6dgl5gns-3HryP99^HNdA!(V~nR%)CFxK0^3?W!MEr25dGLa!~Zp z{VL>WZjXYByjrJsv-_BdgJbebEQL7KT-pb_4`fhCv#*#%3z7AA|8%5f%D&RGZ9zU# z$jhI9CY0o`e@ZUZQ_91%$WM5_6KJSc4b(dU9)0FQz*&~tSJ0qm9b}kygf_P6BbS0? zmVM)64rmJ1PyJAOi_cb6CG~Je~O$Ar7}EcY^uR^NGI@v7~=A4hbQXPYtta%IQc_>B;TXtjUL{RqIzw*-kO!RsE$I z=dCOv9!=)p-yw`=xjot%YEN!oYbzjXgJZJ|RkI)aJgi(0T+^vT6=V5n*5Z^rW$v@X z5@X=&Mt$?Wa%Z2o>DUolkMkFnO;~Pi1*6U~5otYBsWUzpapDrNFSPZLG1uH&T%J}A zyRE=DxE7gz*sF>-C@p+h zaw_T{4jm$f*6UXnRn5mve-WS-Wz=brP{ZZVYpzl)Cci`U;a`{*%-LI|(HMy1xF*%I zt3B~bRXq`HHf%jfyWint0G&;fba@ca;fSz?!$FFp-rGQEo(n+_Qghi#HWl$VN!r&| zmUU4+iCVFUB%}vSZoX!j8jdc<7zTOhKX=ucFG}fL;jvewd}9_;LFj(jIe^x+sk2qpd|_N`G79t}s`D;$S7BA7Tq6kSR+SkcwS5~?hCejxXHcDVJHje8MO^cCFEQ4D3?G$)~hZp08FAN^mzl5WA~RXMtmr&QT9P z4iSO(@L=ZJzs~C8_CU{qDTzqaZ0UbbozK*$YxqbZa*E*VxHCRV)SVC96m|mcf=@}C zSAl^iBJb<1f~D{IbFuC8D><#j*tL5DtSA}qHsp`1jAaM(C9BKKV9I*v`! zlv_5}VidXM9%&}z5-J@2fu5(&^S;mLeLv6Vecs#4);8|453)(Sv{H8Pa&<2yML-Oh`C@g@>s zjXR5esQ7LZXQdnlejIO-js*Y_O-s{r*7pSFo5FW5fw0`IQt48`z42FYAIGL;iiD$p z#<2E?+*N+a`p!xaG9kh-6S7+>gU)ta*|i(|B^<*vDgV@{2rOvy2Ks*jn{QfgMhd zUdV&-#afzSwu5wPQP~iS(g(XpO3vqLPaF-2sVjEk5T7gIxjRJK3)*@u*U6*Omrp%HsHi?OQ44Fk@t$x9`ealid73L|jHnlTUkLAT;wz znU)<8C%PQ*gOF6dgl5gns-3HryP99^HNdA!(V~nR%)CFxK0^3?W!MEr25dGLa!~Zp z{VL>WZjXYByjrJsv-_BdgJbebEQL7KT-pb_4`fhCv#*#%3z7AA|8%5f%D&RGZ9zU# z$jhI9CY0o`e@ZUZQ_91%$WM5_6KJSc4b(dU9)0FQz*&~tSJ0qm9b}kygf_P6BbS0? zmVM)64rmJ1PyJAOi_cb6CG~Je~O$Ar7}EcY^uR^NGI@v7~=A4hbQXPYtta%IQc_>B;TXtjUL{RqIzw*-kO!RsE$I z=dCOv9!=)p-yw`=xjot%YEN!oYbzjXgJZJ|RkI)aJgi(0T+^vT6=V5n*5Z^rW$v@X z5@X=&Mt$?Wa%Z2o>DUolkMkFnO;~Pi1*6U~5otYBsWUzpapDrNFSPZLG1uH&T%J}A zyRE=DxE7gz*sF>-C@p+h zaw_T{4jm$f*6UXnRn5mve-WS-Wz=brP{ZZVYpzl)Cci`U;a`{*%-LI|(HMy1xF*%I zt3B~bRXq`HHf%jfyWint0G&;fba@ca;fSz?!$FFp-rGQEo(n+_Qghi#HWl$VN!r&| zmUU4+iCVFUB%}vSZoX!j8jdc<7zTOhKX=ucFG}fL;jvewd}9_;LFj(jIe^x+sk2qpd|_N`G79t}s`D;$S7BA7Tq6kSR+SkcwS5~?hCejxXHcDVJHje8MO^cCFEQ4D3?G$)~hZp08FAN^mzl5WA~RXMtmr&QT9P z4iSO(@L=ZJzs~C8_CU{qDTzqaZ0UbbozK*$YxqbZa*E*VxHCRV)SVC96m|mcf=@}C zSAl^iBJb<1f~D{IbFuC8D><#j*tL5DtSA}qHsp`1jAaM(C9BKKV9I*v`! zlv_5}VidXM9%&}z5-J@2fu5(&^S;mLeLv6Vecs#4);8|453)(Sv{H8Pa&<2yML-Oh`C@g@>s zjXR5esQ7LZXQdnlejIO-js*Y_O-s{r*7pSFo5FW5fw0`IQt48`z42FYAIGL;iiD$p z#<2E?+*N+a`p!xaG9kh-6S7+>gU)ta*|i(|B^<*vDgV@{2rOvy2Ks*jn{QfgMhd zUdV&-#afzSwu5wPQP~iS(g(XpO3vqLPaF-2sVjEk5T7gIxjRJK3)*@u*U6*Omrp%HsHi?OQ44Fk@t$x9`ealid73L|jHnlTUkLAT;wz znU)<8C%PQ*gOF6dgl5gns-3HryP99^HNdA!(V~nR%)CFxK0^3?W!MEr25dGLa!~Zp z{VL>WZjXYByjrJsv-_BdgJbebEQL7KT-pb_4`fhCv#*#%3z7AA|8%5f%D&RGZ9zU# z$jhI9CY0o`e@ZUZQ_91%$WM5_6KJSc4b(dU9)0FQz*&~tSJ0qm9b}kygf_P6BbS0? zmVM)64rmJ1PyJAOi_cb6CG~Je~O$Ar7}EcY^uR^NGI@v7~=A4hbQXPYtta%IQc_>B;TXtjUL{RqIzw*-kO!RsE$I z=dCOv9!=)p-yw`=xjot%YEN!oYbzjXgJZJ|RkI)aJgi(0T+^vT6=V5n*5Z^rW$v@X z5@X=&Mt$?Wa%Z2o>DUolkMkFnO;~Pi1*6U~5otYBsWUzpapDrNFSPZLG1uH&T%J}A zyRE=DxE7gz*sF>-C@p+h zaw_T{4jm$f*6UXnRn5mve-WS-Wz=brP{ZZVYpzl)Cci`U;a`{*%-LI|(HMy1xF*%I zt3B~bRXq`HHf%jfyWint0G&;fba@ca;fSz?!$FFp-rGQEo(n+_Qghi#HWl$VN!r&| zmUU4+iCVFUB%}vSZoX!j8jdc<7zTOhKX=ucFG}fL;jvewd}9_;LFj(jIe^x+sk2qpd|_N`G79t}s`D;$S7BA7Tq6kSR+SkcwS5~?hCejxXHcDVJHje8MO^cCFEQ4D3?G$)~hZp08FAN^mzl5WA~RXMtmr&QT9P z4iSO(@L=ZJzs~C8_CU{qDTzqaZ0UbbozK*$YxqbZa*E*VxHCRV)SVC96m|mcf=@}C zSAl^iBJb<1f~D{IbFuC8D><#j*tL5 Date: Mon, 28 Oct 2024 07:48:25 -0400 Subject: [PATCH 16/31] build(deps): bump github.com/tinylib/msgp from 1.2.1 to 1.2.3 (#3182) Bumps [github.com/tinylib/msgp](https://github.com/tinylib/msgp) from 1.2.1 to 1.2.3. - [Release notes](https://github.com/tinylib/msgp/releases) - [Commits](https://github.com/tinylib/msgp/compare/v1.2.1...v1.2.3) --- updated-dependencies: - dependency-name: github.com/tinylib/msgp dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 2b5e60a1bf..00441bd2a1 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/mattn/go-colorable v0.1.13 github.com/mattn/go-isatty v0.0.20 github.com/stretchr/testify v1.9.0 - github.com/tinylib/msgp v1.2.1 + github.com/tinylib/msgp v1.2.3 github.com/valyala/bytebufferpool v1.0.0 github.com/valyala/fasthttp v1.56.0 ) @@ -18,7 +18,7 @@ require ( github.com/andybalholm/brotli v1.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/klauspost/compress v1.17.9 // indirect - github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 // indirect + github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect golang.org/x/net v0.29.0 // indirect diff --git a/go.sum b/go.sum index 42768451af..c1caadc4a3 100644 --- a/go.sum +++ b/go.sum @@ -17,14 +17,14 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 h1:jYi87L8j62qkXzaYHAQAhEapgukhenIMZRBKTNRLHJ4= -github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tinylib/msgp v1.2.1 h1:6ypy2qcCznxpP4hpORzhtXyTqrBs7cfM9MCCWY8zsmU= -github.com/tinylib/msgp v1.2.1/go.mod h1:2vIGs3lcUo8izAATNobrCHevYZC/LMsJtw4JPiYPHro= +github.com/tinylib/msgp v1.2.3 h1:6ryR/GnmkqptS/HSe6JylgoKASyBKefBQnvIesnyiV4= +github.com/tinylib/msgp v1.2.3/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.56.0 h1:bEZdJev/6LCBlpdORfrLu/WOZXXxvrUQSiyniuaoW8U= From 9eee2923a55cbf4ed5cd2730d3632b3d03c32260 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Nov 2024 13:57:47 +0100 Subject: [PATCH 17/31] build(deps): bump github.com/tinylib/msgp from 1.2.3 to 1.2.4 (#3185) Bumps [github.com/tinylib/msgp](https://github.com/tinylib/msgp) from 1.2.3 to 1.2.4. - [Release notes](https://github.com/tinylib/msgp/releases) - [Commits](https://github.com/tinylib/msgp/compare/v1.2.3...v1.2.4) --- updated-dependencies: - dependency-name: github.com/tinylib/msgp dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 00441bd2a1..389b782d69 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/mattn/go-colorable v0.1.13 github.com/mattn/go-isatty v0.0.20 github.com/stretchr/testify v1.9.0 - github.com/tinylib/msgp v1.2.3 + github.com/tinylib/msgp v1.2.4 github.com/valyala/bytebufferpool v1.0.0 github.com/valyala/fasthttp v1.56.0 ) diff --git a/go.sum b/go.sum index c1caadc4a3..ee20d5e515 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tinylib/msgp v1.2.3 h1:6ryR/GnmkqptS/HSe6JylgoKASyBKefBQnvIesnyiV4= -github.com/tinylib/msgp v1.2.3/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/tinylib/msgp v1.2.4 h1:yLFeUGostXXSGW5vxfT5dXG/qzkn4schv2I7at5+hVU= +github.com/tinylib/msgp v1.2.4/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.56.0 h1:bEZdJev/6LCBlpdORfrLu/WOZXXxvrUQSiyniuaoW8U= From 87faed717fb081426d2d2365acc9fe95389e0533 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Nov 2024 12:58:50 +0000 Subject: [PATCH 18/31] build(deps): bump github.com/valyala/fasthttp from 1.56.0 to 1.57.0 Bumps [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp) from 1.56.0 to 1.57.0. - [Release notes](https://github.com/valyala/fasthttp/releases) - [Commits](https://github.com/valyala/fasthttp/compare/v1.56.0...v1.57.0) --- updated-dependencies: - dependency-name: github.com/valyala/fasthttp dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 12 ++++++------ go.sum | 26 ++++++++++++++------------ 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 389b782d69..781a260a03 100644 --- a/go.mod +++ b/go.mod @@ -11,18 +11,18 @@ require ( github.com/stretchr/testify v1.9.0 github.com/tinylib/msgp v1.2.4 github.com/valyala/bytebufferpool v1.0.0 - github.com/valyala/fasthttp v1.56.0 + github.com/valyala/fasthttp v1.57.0 ) require ( - github.com/andybalholm/brotli v1.1.0 // indirect + github.com/andybalholm/brotli v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/klauspost/compress v1.17.9 // indirect + github.com/klauspost/compress v1.17.11 // indirect github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - golang.org/x/net v0.29.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.18.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ee20d5e515..f3eb0e2468 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= -github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= @@ -10,8 +10,8 @@ github.com/gofiber/utils/v2 v2.0.0-beta.7 h1:NnHFrRHvhrufPABdWajcKZejz9HnCWmT/as github.com/gofiber/utils/v2 v2.0.0-beta.7/go.mod h1:J/M03s+HMdZdvhAeyh76xT72IfVqBzuz/OJkrMa7cwU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -27,20 +27,22 @@ github.com/tinylib/msgp v1.2.4 h1:yLFeUGostXXSGW5vxfT5dXG/qzkn4schv2I7at5+hVU= github.com/tinylib/msgp v1.2.4/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.56.0 h1:bEZdJev/6LCBlpdORfrLu/WOZXXxvrUQSiyniuaoW8U= -github.com/valyala/fasthttp v1.56.0/go.mod h1:sReBt3XZVnudxuLOx4J/fMrJVorWRiWY2koQKgABiVI= +github.com/valyala/fasthttp v1.57.0 h1:Xw8SjWGEP/+wAAgyy5XTvgrWlOD1+TxbbvNADYCm1Tg= +github.com/valyala/fasthttp v1.57.0/go.mod h1:h6ZBaPRlzpZ6O3H5t2gEk1Qi33+TmLvfwgLLp0t9CpE= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +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/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 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 dcdd2eb2c6bc3cfaae4fb78da584e2ab883d432a Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Mon, 11 Nov 2024 04:37:27 -0500 Subject: [PATCH 19/31] Bump golangci-lint to v1.62.0 (#3196) --- .github/workflows/linter.yml | 2 +- Makefile | 2 +- client/hooks_test.go | 18 +++++++++--------- client/response_test.go | 4 ++-- ctx_test.go | 10 +++++----- log/default.go | 2 +- middleware/adaptor/adaptor.go | 2 +- middleware/cache/heap.go | 2 +- middleware/cache/manager.go | 2 +- middleware/limiter/manager.go | 2 +- middleware/logger/default_logger.go | 4 ++-- middleware/logger/logger_test.go | 6 ++---- middleware/logger/tags.go | 2 +- router.go | 2 +- 14 files changed, 29 insertions(+), 31 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index dc04fec67b..2dc54bc585 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -37,4 +37,4 @@ jobs: uses: golangci/golangci-lint-action@v6 with: # NOTE: Keep this in sync with the version from .golangci.yml - version: v1.61.0 + version: v1.62.0 diff --git a/Makefile b/Makefile index 33526991a3..87d4b50db5 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.61.0 run ./... + go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.0 run ./... ## test: 🚦 Execute all tests .PHONY: test diff --git a/client/hooks_test.go b/client/hooks_test.go index 359e3184d2..8e91392930 100644 --- a/client/hooks_test.go +++ b/client/hooks_test.go @@ -254,7 +254,7 @@ func Test_Parser_Request_Header(t *testing.T) { err := parserRequestHeader(client, req) require.NoError(t, err) - require.Equal(t, []byte(applicationJSON), req.RawRequest.Header.ContentType()) + require.Equal(t, []byte(applicationJSON), req.RawRequest.Header.ContentType()) //nolint:testifylint // test }) t.Run("auto set xml header", func(t *testing.T) { @@ -297,8 +297,8 @@ func Test_Parser_Request_Header(t *testing.T) { err := parserRequestHeader(client, req) require.NoError(t, err) - require.True(t, strings.Contains(string(req.RawRequest.Header.MultipartFormBoundary()), "--FiberFormBoundary")) - require.True(t, strings.Contains(string(req.RawRequest.Header.ContentType()), multipartFormData)) + require.Contains(t, string(req.RawRequest.Header.MultipartFormBoundary()), "--FiberFormBoundary") + require.Contains(t, string(req.RawRequest.Header.ContentType()), multipartFormData) }) t.Run("ua should have default value", func(t *testing.T) { @@ -436,7 +436,7 @@ func Test_Parser_Request_Body(t *testing.T) { err := parserRequestBody(client, req) require.NoError(t, err) - require.Equal(t, []byte("{\"name\":\"foo\"}"), req.RawRequest.Body()) + require.Equal(t, []byte("{\"name\":\"foo\"}"), req.RawRequest.Body()) //nolint:testifylint // test }) t.Run("xml body", func(t *testing.T) { @@ -489,8 +489,8 @@ func Test_Parser_Request_Body(t *testing.T) { err := parserRequestBody(client, req) require.NoError(t, err) - require.True(t, strings.Contains(string(req.RawRequest.Body()), "----FiberFormBoundary")) - require.True(t, strings.Contains(string(req.RawRequest.Body()), "world")) + require.Contains(t, string(req.RawRequest.Body()), "----FiberFormBoundary") + require.Contains(t, string(req.RawRequest.Body()), "world") }) t.Run("file and form data", func(t *testing.T) { @@ -502,9 +502,9 @@ func Test_Parser_Request_Body(t *testing.T) { err := parserRequestBody(client, req) require.NoError(t, err) - require.True(t, strings.Contains(string(req.RawRequest.Body()), "----FiberFormBoundary")) - require.True(t, strings.Contains(string(req.RawRequest.Body()), "world")) - require.True(t, strings.Contains(string(req.RawRequest.Body()), "bar")) + require.Contains(t, string(req.RawRequest.Body()), "----FiberFormBoundary") + require.Contains(t, string(req.RawRequest.Body()), "world") + require.Contains(t, string(req.RawRequest.Body()), "bar") }) t.Run("raw body", func(t *testing.T) { diff --git a/client/response_test.go b/client/response_test.go index ab22ab388a..0d27ee7ed4 100644 --- a/client/response_test.go +++ b/client/response_test.go @@ -375,7 +375,7 @@ func Test_Response_Save(t *testing.T) { data, err := io.ReadAll(file) require.NoError(t, err) - require.Equal(t, "{\"status\":\"success\"}", string(data)) + require.JSONEq(t, "{\"status\":\"success\"}", string(data)) }) t.Run("io.Writer", func(t *testing.T) { @@ -396,7 +396,7 @@ func Test_Response_Save(t *testing.T) { err = resp.Save(buf) require.NoError(t, err) - require.Equal(t, "{\"status\":\"success\"}", buf.String()) + require.JSONEq(t, "{\"status\":\"success\"}", buf.String()) }) t.Run("error type", func(t *testing.T) { diff --git a/ctx_test.go b/ctx_test.go index 3b6b44ebac..a017fae411 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -1189,7 +1189,7 @@ func Test_Ctx_AutoFormat_Struct(t *testing.T) { c.Request().Header.Set(HeaderAccept, MIMEApplicationJSON) err := c.AutoFormat(data) require.NoError(t, err) - require.Equal(t, + require.JSONEq(t, `{"Sender":"Carol","Recipients":["Alice","Bob"],"Urgency":3}`, string(c.Response().Body()), ) @@ -3549,7 +3549,7 @@ func Test_Ctx_JSON(t *testing.T) { "Age": 20, }) require.NoError(t, err) - require.Equal(t, `{"Age":20,"Name":"Grame"}`, string(c.Response().Body())) + require.JSONEq(t, `{"Age":20,"Name":"Grame"}`, string(c.Response().Body())) require.Equal(t, "application/json", string(c.Response().Header.Peek("content-type"))) // Test with ctype @@ -3558,7 +3558,7 @@ func Test_Ctx_JSON(t *testing.T) { "Age": 20, }, "application/problem+json") require.NoError(t, err) - require.Equal(t, `{"Age":20,"Name":"Grame"}`, string(c.Response().Body())) + require.JSONEq(t, `{"Age":20,"Name":"Grame"}`, string(c.Response().Body())) require.Equal(t, "application/problem+json", string(c.Response().Header.Peek("content-type"))) testEmpty := func(v any, r string) { @@ -3612,7 +3612,7 @@ func Benchmark_Ctx_JSON(b *testing.B) { err = c.JSON(data) } require.NoError(b, err) - require.Equal(b, `{"Name":"Grame","Age":20}`, string(c.Response().Body())) + require.JSONEq(b, `{"Name":"Grame","Age":20}`, string(c.Response().Body())) } // go test -run=^$ -bench=Benchmark_Ctx_JSON_Ctype -benchmem -count=4 @@ -3635,7 +3635,7 @@ func Benchmark_Ctx_JSON_Ctype(b *testing.B) { err = c.JSON(data, "application/problem+json") } require.NoError(b, err) - require.Equal(b, `{"Name":"Grame","Age":20}`, string(c.Response().Body())) + require.JSONEq(b, `{"Name":"Grame","Age":20}`, string(c.Response().Body())) require.Equal(b, "application/problem+json", string(c.Response().Header.Peek("content-type"))) } diff --git a/log/default.go b/log/default.go index a835b3b403..9a3c93b1c2 100644 --- a/log/default.go +++ b/log/default.go @@ -93,7 +93,7 @@ func (l *defaultLogger) privateLogw(lv Level, format string, keysAndValues []any if i > 0 || format != "" { buf.WriteByte(' ') } - buf.WriteString(keysAndValues[i].(string)) //nolint:forcetypeassert // Keys must be strings + buf.WriteString(keysAndValues[i].(string)) //nolint:forcetypeassert,errcheck // Keys must be strings buf.WriteByte('=') buf.WriteString(utils.ToString(keysAndValues[i+1])) } diff --git a/middleware/adaptor/adaptor.go b/middleware/adaptor/adaptor.go index 03c6287e6f..867f440adf 100644 --- a/middleware/adaptor/adaptor.go +++ b/middleware/adaptor/adaptor.go @@ -163,7 +163,7 @@ func handlerFunc(app *fiber.App, h ...fiber.Handler) http.HandlerFunc { } } - if _, _, err := net.SplitHostPort(r.RemoteAddr); err != nil && err.(*net.AddrError).Err == "missing port in address" { //nolint:errorlint, forcetypeassert // overlinting + if _, _, err := net.SplitHostPort(r.RemoteAddr); err != nil && err.(*net.AddrError).Err == "missing port in address" { //nolint:errorlint,forcetypeassert,errcheck // overlinting r.RemoteAddr = net.JoinHostPort(r.RemoteAddr, "80") } diff --git a/middleware/cache/heap.go b/middleware/cache/heap.go index c5715392ef..bba69fe4f5 100644 --- a/middleware/cache/heap.go +++ b/middleware/cache/heap.go @@ -41,7 +41,7 @@ func (h indexedHeap) Swap(i, j int) { } func (h *indexedHeap) Push(x any) { - h.pushInternal(x.(heapEntry)) //nolint:forcetypeassert // Forced type assertion required to implement the heap.Interface interface + h.pushInternal(x.(heapEntry)) //nolint:forcetypeassert,errcheck // Forced type assertion required to implement the heap.Interface interface } func (h *indexedHeap) Pop() any { diff --git a/middleware/cache/manager.go b/middleware/cache/manager.go index 3a796c7758..7d20c0498e 100644 --- a/middleware/cache/manager.go +++ b/middleware/cache/manager.go @@ -50,7 +50,7 @@ func newManager(storage fiber.Storage) *manager { // acquire returns an *entry from the sync.Pool func (m *manager) acquire() *item { - return m.pool.Get().(*item) //nolint:forcetypeassert // We store nothing else in the pool + return m.pool.Get().(*item) //nolint:forcetypeassert,errcheck // We store nothing else in the pool } // release and reset *entry to sync.Pool diff --git a/middleware/limiter/manager.go b/middleware/limiter/manager.go index ecaee7f127..d004acad0e 100644 --- a/middleware/limiter/manager.go +++ b/middleware/limiter/manager.go @@ -45,7 +45,7 @@ func newManager(storage fiber.Storage) *manager { // acquire returns an *entry from the sync.Pool func (m *manager) acquire() *item { - return m.pool.Get().(*item) //nolint:forcetypeassert // We store nothing else in the pool + return m.pool.Get().(*item) //nolint:forcetypeassert,errcheck // We store nothing else in the pool } // release and reset *entry to sync.Pool diff --git a/middleware/logger/default_logger.go b/middleware/logger/default_logger.go index e359bd4a83..369b2c8580 100644 --- a/middleware/logger/default_logger.go +++ b/middleware/logger/default_logger.go @@ -31,7 +31,7 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error { } buf.WriteString( fmt.Sprintf("%s |%s %3d %s| %13v | %15s |%s %-7s %s| %-"+data.ErrPaddingStr+"s %s\n", - data.Timestamp.Load().(string), //nolint:forcetypeassert // Timestamp is always a string + data.Timestamp.Load().(string), //nolint:forcetypeassert,errcheck // Timestamp is always a string statusColor(c.Response().StatusCode(), colors), c.Response().StatusCode(), colors.Reset, data.Stop.Sub(data.Start), c.IP(), @@ -61,7 +61,7 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error { } // Timestamp - buf.WriteString(data.Timestamp.Load().(string)) //nolint:forcetypeassert // Timestamp is always a string + buf.WriteString(data.Timestamp.Load().(string)) //nolint:forcetypeassert,errcheck // Timestamp is always a string buf.WriteString(" | ") // Status Code with 3 fixed width, right aligned diff --git a/middleware/logger/logger_test.go b/middleware/logger/logger_test.go index 0bc06531c9..46a435ecc9 100644 --- a/middleware/logger/logger_test.go +++ b/middleware/logger/logger_test.go @@ -305,8 +305,7 @@ func Test_Logger_WithLatency(t *testing.T) { require.Equal(t, fiber.StatusOK, resp.StatusCode) // Assert that the log output contains the expected latency value in the current time unit - require.True(t, bytes.HasSuffix(buff.Bytes(), []byte(tu.unit)), - fmt.Sprintf("Expected latency to be in %s, got %s", tu.unit, buff.String())) + require.True(t, bytes.HasSuffix(buff.Bytes(), []byte(tu.unit)), "Expected latency to be in %s, got %s", tu.unit, buff.String()) // Reset the buffer buff.Reset() @@ -350,8 +349,7 @@ func Test_Logger_WithLatency_DefaultFormat(t *testing.T) { // parse out the latency value from the log output latency := bytes.Split(buff.Bytes(), []byte(" | "))[2] // Assert that the latency value is in the current time unit - require.True(t, bytes.HasSuffix(latency, []byte(tu.unit)), - fmt.Sprintf("Expected latency to be in %s, got %s", tu.unit, latency)) + require.True(t, bytes.HasSuffix(latency, []byte(tu.unit)), "Expected latency to be in %s, got %s", tu.unit, latency) // Reset the buffer buff.Reset() diff --git a/middleware/logger/tags.go b/middleware/logger/tags.go index 8baacfdc09..25f1a48ff5 100644 --- a/middleware/logger/tags.go +++ b/middleware/logger/tags.go @@ -200,7 +200,7 @@ func createTagMap(cfg *Config) map[string]LogFunc { return output.WriteString(fmt.Sprintf("%13v", latency)) }, TagTime: func(output Buffer, _ fiber.Ctx, data *Data, _ string) (int, error) { - return output.WriteString(data.Timestamp.Load().(string)) //nolint:forcetypeassert // We always store a string in here + return output.WriteString(data.Timestamp.Load().(string)) //nolint:forcetypeassert,errcheck // We always store a string in here }, } // merge with custom tags from user diff --git a/router.go b/router.go index 27e3bc7e74..2091cfc6cb 100644 --- a/router.go +++ b/router.go @@ -234,7 +234,7 @@ func (app *App) requestHandler(rctx *fasthttp.RequestCtx) { if app.newCtxFunc != nil { _, err = app.nextCustom(c) } else { - _, err = app.next(c.(*DefaultCtx)) + _, err = app.next(c.(*DefaultCtx)) //nolint:errcheck // It is fine to ignore the error here } if err != nil { if catch := c.App().ErrorHandler(c, err); catch != nil { From 2c7bdb9fd14f8c6e106da25a16fcdfbe3220134b Mon Sep 17 00:00:00 2001 From: Karen <8099322@qq.com> Date: Tue, 12 Nov 2024 09:47:22 +0800 Subject: [PATCH 20/31] =?UTF-8?q?=F0=9F=A9=B9=20fix:=20Close=20File=20Afte?= =?UTF-8?q?r=20SaveFileToStorage=20(#3197)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: close file after opening in SaveFileToStorage to prevent resource leaks * ♻️ refactor: simplify file close logic * Update ctx.go --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> --- ctx.go | 1 + 1 file changed, 1 insertion(+) diff --git a/ctx.go b/ctx.go index 1bbf2ba130..fcf5c138da 100644 --- a/ctx.go +++ b/ctx.go @@ -1470,6 +1470,7 @@ func (*DefaultCtx) SaveFileToStorage(fileheader *multipart.FileHeader, path stri if err != nil { return fmt.Errorf("failed to open: %w", err) } + defer file.Close() //nolint:errcheck // not needed content, err := io.ReadAll(file) if err != nil { From 7cddb84b214a18860dcbac6caa9e260e9f48efa1 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Wed, 13 Nov 2024 07:11:09 -0500 Subject: [PATCH 21/31] =?UTF-8?q?=F0=9F=93=9A=20Doc:=20Update=20intro=20do?= =?UTF-8?q?cumentation=20(#3204)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auto update intro documentation --- docs/intro.md | 63 ++++++++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/docs/intro.md b/docs/intro.md index 7e0e1798c9..4dc763df36 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -4,15 +4,15 @@ id: welcome title: 👋 Welcome sidebar_position: 1 --- -An online API documentation with examples so you can start building web apps with Fiber right away! +Welcome to the online API documentation for Fiber, complete with examples to help you start building web applications with Fiber right away! -**Fiber** is an [Express](https://github.com/expressjs/express) inspired **web framework** built on top of [Fasthttp](https://github.com/valyala/fasthttp), the **fastest** HTTP engine for [Go](https://go.dev/doc/). Designed to **ease** things up for **fast** development with **zero memory allocation** and **performance** in mind. +**Fiber** is an [Express](https://github.com/expressjs/express)-inspired **web framework** built on top of [Fasthttp](https://github.com/valyala/fasthttp), the **fastest** HTTP engine for [Go](https://go.dev/doc/). It is designed to facilitate rapid development with **zero memory allocations** and a strong focus on **performance**. -These docs are for **Fiber v3**, which was released on **March XX, 2024**. +These docs are for **Fiber v3**, which was released on **Month xx, 202x**. ### Installation -First of all, [download](https://go.dev/dl/) and install Go. `1.22` or higher is required. +First, [download](https://go.dev/dl/) and install Go. Version `1.22` or higher is required. Installation is done using the [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) command: @@ -22,7 +22,7 @@ go get github.com/gofiber/fiber/v3 ### Zero Allocation -Fiber is optimized for **high-performance**, meaning values returned from **fiber.Ctx** are **not** immutable by default and **will** be re-used across requests. As a rule of thumb, you **must** only use context values within the handler and **must not** keep any references. Once you return from the handler, any values obtained from the context will be re-used in future requests. Here is an example: +Fiber is optimized for **high performance**, meaning values returned from **fiber.Ctx** are **not** immutable by default and **will** be reused across requests. As a rule of thumb, you **must** only use context values within the handler and **must not** keep any references. Once you return from the handler, any values obtained from the context will be reused in future requests. Here is an example: ```go func handler(c fiber.Ctx) error { @@ -44,13 +44,13 @@ func handler(c fiber.Ctx) error { buffer := make([]byte, len(result)) copy(buffer, result) resultCopy := string(buffer) - // Variable is now valid forever + // Variable is now valid indefinitely // ... } ``` -We created a custom `CopyString` function that does the above and is available under [gofiber/utils](https://github.com/gofiber/utils). +We created a custom `CopyString` function that performs the above and is available under [gofiber/utils](https://github.com/gofiber/utils). ```go app.Get("/:foo", func(c fiber.Ctx) error { @@ -61,7 +61,7 @@ app.Get("/:foo", func(c fiber.Ctx) error { }) ``` -Alternatively, you can also use the `Immutable` setting. It will make all values returned from the context immutable, allowing you to persist them anywhere. Of course, this comes at the cost of performance. +Alternatively, you can enable the `Immutable` setting. This makes all values returned from the context immutable, allowing you to persist them anywhere. Note that this comes at the cost of performance. ```go app := fiber.New(fiber.Config{ @@ -69,11 +69,11 @@ app := fiber.New(fiber.Config{ }) ``` -For more information, please check [**\#426**](https://github.com/gofiber/fiber/issues/426), [**\#185**](https://github.com/gofiber/fiber/issues/185) and [**\#3012**](https://github.com/gofiber/fiber/issues/3012). +For more information, please refer to [#426](https://github.com/gofiber/fiber/issues/426), [#185](https://github.com/gofiber/fiber/issues/185), and [#3012](https://github.com/gofiber/fiber/issues/3012). ### Hello, World -Embedded below is essentially the most straightforward **Fiber** app you can create: +Below is the most straightforward **Fiber** application you can create: ```go package main @@ -95,15 +95,15 @@ func main() { go run server.go ``` -Browse to `http://localhost:3000` and you should see `Hello, World!` on the page. +Browse to `http://localhost:3000` and you should see `Hello, World!` displayed on the page. -### Basic routing +### Basic Routing -Routing refers to determining how an application responds to a client request to a particular endpoint, which is a URI (or path) and a specific HTTP request method (`GET`, `PUT`, `POST`, etc.). +Routing determines how an application responds to a client request to a particular endpoint, which is a URI (or path) and a specific HTTP request method (`GET`, `PUT`, `POST`, etc.). Each route can have **multiple handler functions** that are executed when the route is matched. -Route definition takes the following structures: +Route definitions follow the structure below: ```go // Function signature @@ -115,10 +115,10 @@ app.Method(path string, ...func(fiber.Ctx) error) - `path` is a virtual path on the server - `func(fiber.Ctx) error` is a callback function containing the [Context](https://docs.gofiber.io/api/ctx) executed when the route is matched -#### Simple route +#### Simple Route ```go -// Respond with "Hello, World!" on root path, "/" +// Respond with "Hello, World!" on root path "/" app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello, World!") }) @@ -131,11 +131,11 @@ app.Get("/", func(c fiber.Ctx) error { app.Get("/:value", func(c fiber.Ctx) error { return c.SendString("value: " + c.Params("value")) - // => Get request with value: hello world + // => Response: "value: hello world" }) ``` -#### Optional parameter +#### Optional Parameter ```go // GET http://localhost:3000/john @@ -143,9 +143,10 @@ app.Get("/:value", func(c fiber.Ctx) error { app.Get("/:name?", func(c fiber.Ctx) error { if c.Params("name") != "" { return c.SendString("Hello " + c.Params("name")) - // => Hello john + // => Response: "Hello john" } return c.SendString("Where is john?") + // => Response: "Where is john?" }) ``` @@ -156,27 +157,33 @@ app.Get("/:name?", func(c fiber.Ctx) error { app.Get("/api/*", func(c fiber.Ctx) error { return c.SendString("API path: " + c.Params("*")) - // => API path: user/john + // => Response: "API path: user/john" }) ``` -### Static files +### Static Files -To serve static files such as **images**, **CSS**, and **JavaScript** files, replace your function handler with a file or directory string. -You can check out [static middleware](./middleware/static.md) for more information. -Function signature: +To serve static files such as **images**, **CSS**, and **JavaScript** files, use the `Static` method with a directory path. For more information, refer to the [static middleware](./middleware/static.md). Use the following code to serve files in a directory named `./public`: ```go -app := fiber.New() +package main + +import ( + "github.com/gofiber/fiber/v3" +) -app.Get("/*", static.New("./public")) +func main() { + app := fiber.New() -app.Listen(":3000") + app.Static("/", "./public") + + app.Listen(":3000") +} ``` -Now, you can load the files that are in the `./public` directory: +Now, you can access the files in the `./public` directory via your browser: ```bash http://localhost:3000/hello.html From 16f9056f5fa5f2c295d19b40b002d3300d4fb482 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Wed, 13 Nov 2024 10:12:19 -0500 Subject: [PATCH 22/31] =?UTF-8?q?=F0=9F=90=9B=20fix:=20Improve=20naming=20?= =?UTF-8?q?convention=20for=20Context=20returning=20functions=20(#3193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Rename UserContext() to Context(). Rename Context() to RequestCtx() * Update Ctxt docs and What's new * Remove extra blank lines --------- Co-authored-by: M. Efe Çetin --- bind.go | 10 ++--- client/client_test.go | 2 +- ctx.go | 22 +++++------ ctx_interface_gen.go | 12 +++--- ctx_test.go | 32 ++++++++-------- docs/api/ctx.md | 53 +++++++++++++-------------- docs/middleware/timeout.md | 8 ++-- docs/whats_new.md | 3 ++ middleware/adaptor/adaptor.go | 6 +-- middleware/adaptor/adaptor_test.go | 14 +++---- middleware/cache/cache_test.go | 2 +- middleware/compress/compress.go | 2 +- middleware/etag/etag.go | 4 +- middleware/expvar/expvar.go | 2 +- middleware/idempotency/idempotency.go | 2 +- middleware/logger/logger_test.go | 6 +-- middleware/pprof/pprof.go | 22 +++++------ middleware/redirect/redirect.go | 2 +- middleware/static/static.go | 12 +++--- middleware/timeout/timeout.go | 4 +- middleware/timeout/timeout_test.go | 4 +- redirect.go | 2 +- redirect_test.go | 16 ++++---- 23 files changed, 122 insertions(+), 120 deletions(-) diff --git a/bind.go b/bind.go index e202cd85e0..eff595cd72 100644 --- a/bind.go +++ b/bind.go @@ -95,7 +95,7 @@ func (b *Bind) RespHeader(out any) error { // Cookie binds the requesr cookie strings into the struct, map[string]string and map[string][]string. // NOTE: If your cookie is like key=val1,val2; they'll be binded as an slice if your map is map[string][]string. Else, it'll use last element of cookie. func (b *Bind) Cookie(out any) error { - if err := b.returnErr(binder.CookieBinder.Bind(b.ctx.Context(), out)); err != nil { + if err := b.returnErr(binder.CookieBinder.Bind(b.ctx.RequestCtx(), out)); err != nil { return err } @@ -104,7 +104,7 @@ func (b *Bind) Cookie(out any) error { // Query binds the query string into the struct, map[string]string and map[string][]string. func (b *Bind) Query(out any) error { - if err := b.returnErr(binder.QueryBinder.Bind(b.ctx.Context(), out)); err != nil { + if err := b.returnErr(binder.QueryBinder.Bind(b.ctx.RequestCtx(), out)); err != nil { return err } @@ -131,7 +131,7 @@ func (b *Bind) XML(out any) error { // Form binds the form into the struct, map[string]string and map[string][]string. func (b *Bind) Form(out any) error { - if err := b.returnErr(binder.FormBinder.Bind(b.ctx.Context(), out)); err != nil { + if err := b.returnErr(binder.FormBinder.Bind(b.ctx.RequestCtx(), out)); err != nil { return err } @@ -149,7 +149,7 @@ func (b *Bind) URI(out any) error { // MultipartForm binds the multipart form into the struct, map[string]string and map[string][]string. func (b *Bind) MultipartForm(out any) error { - if err := b.returnErr(binder.FormBinder.BindMultipart(b.ctx.Context(), out)); err != nil { + if err := b.returnErr(binder.FormBinder.BindMultipart(b.ctx.RequestCtx(), out)); err != nil { return err } @@ -163,7 +163,7 @@ func (b *Bind) MultipartForm(out any) error { // If there're no custom binder for mime type of body, it will return a ErrUnprocessableEntity error. func (b *Bind) Body(out any) error { // Get content-type - ctype := utils.ToLower(utils.UnsafeString(b.ctx.Context().Request.Header.ContentType())) + ctype := utils.ToLower(utils.UnsafeString(b.ctx.RequestCtx().Request.Header.ContentType())) ctype = binder.FilterFlags(utils.ParseVendorSpecificContentType(ctype)) // Check custom binders diff --git a/client/client_test.go b/client/client_test.go index b8dd39bbf0..0323f70ccd 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -1572,7 +1572,7 @@ func Test_Client_SetProxyURL(t *testing.T) { } c.Status(resp.StatusCode()) - c.Context().SetBody(resp.Body()) + c.RequestCtx().SetBody(resp.Body()) return nil }) diff --git a/ctx.go b/ctx.go index fcf5c138da..a2eee2754b 100644 --- a/ctx.go +++ b/ctx.go @@ -382,26 +382,26 @@ func (c *DefaultCtx) ClearCookie(key ...string) { }) } -// Context returns *fasthttp.RequestCtx that carries a deadline +// RequestCtx returns *fasthttp.RequestCtx that carries a deadline // a cancellation signal, and other values across API boundaries. -func (c *DefaultCtx) Context() *fasthttp.RequestCtx { +func (c *DefaultCtx) RequestCtx() *fasthttp.RequestCtx { return c.fasthttp } -// UserContext returns a context implementation that was set by +// Context returns a context implementation that was set by // user earlier or returns a non-nil, empty context,if it was not set earlier. -func (c *DefaultCtx) UserContext() context.Context { +func (c *DefaultCtx) Context() context.Context { ctx, ok := c.fasthttp.UserValue(userContextKey).(context.Context) if !ok { ctx = context.Background() - c.SetUserContext(ctx) + c.SetContext(ctx) } return ctx } -// SetUserContext sets a context implementation by user. -func (c *DefaultCtx) SetUserContext(ctx context.Context) { +// SetContext sets a context implementation by user. +func (c *DefaultCtx) SetContext(ctx context.Context) { c.fasthttp.SetUserValue(userContextKey, ctx) } @@ -1189,8 +1189,8 @@ func (c *DefaultCtx) Query(key string, defaultValue ...string) string { // Queries()["filters[customer][name]"] == "Alice" // Queries()["filters[status]"] == "pending" func (c *DefaultCtx) Queries() map[string]string { - m := make(map[string]string, c.Context().QueryArgs().Len()) - c.Context().QueryArgs().VisitAll(func(key, value []byte) { + m := make(map[string]string, c.RequestCtx().QueryArgs().Len()) + c.RequestCtx().QueryArgs().VisitAll(func(key, value []byte) { m[c.app.getString(key)] = c.app.getString(value) }) return m @@ -1219,7 +1219,7 @@ func (c *DefaultCtx) Queries() map[string]string { // unknown := Query[string](c, "unknown", "default") // Returns "default" since the query parameter "unknown" is not found func Query[V GenericType](c Ctx, key string, defaultValue ...V) V { var v V - q := c.App().getString(c.Context().QueryArgs().Peek(key)) + q := c.App().getString(c.RequestCtx().QueryArgs().Peek(key)) return genericParseType[V](q, v, defaultValue...) } @@ -1630,7 +1630,7 @@ func (c *DefaultCtx) SendFile(file string, config ...SendFile) error { // Apply cache control header if status != StatusNotFound && status != StatusForbidden { if len(cacheControlValue) > 0 { - c.Context().Response.Header.Set(HeaderCacheControl, cacheControlValue) + c.RequestCtx().Response.Header.Set(HeaderCacheControl, cacheControlValue) } return nil diff --git a/ctx_interface_gen.go b/ctx_interface_gen.go index 0714b24329..9fd434bc3e 100644 --- a/ctx_interface_gen.go +++ b/ctx_interface_gen.go @@ -45,14 +45,14 @@ type Ctx interface { // ClearCookie expires a specific cookie by key on the client side. // If no key is provided it expires all cookies that came with the request. ClearCookie(key ...string) - // Context returns *fasthttp.RequestCtx that carries a deadline + // RequestCtx returns *fasthttp.RequestCtx that carries a deadline // a cancellation signal, and other values across API boundaries. - Context() *fasthttp.RequestCtx - // UserContext returns a context implementation that was set by + RequestCtx() *fasthttp.RequestCtx + // Context returns a context implementation that was set by // user earlier or returns a non-nil, empty context,if it was not set earlier. - UserContext() context.Context - // SetUserContext sets a context implementation by user. - SetUserContext(ctx context.Context) + Context() context.Context + // SetContext sets a context implementation by user. + SetContext(ctx context.Context) // Cookie sets a cookie by passing a cookie struct. Cookie(cookie *Cookie) // Cookies are used for getting a cookie value by key. diff --git a/ctx_test.go b/ctx_test.go index a017fae411..af56197c58 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -843,24 +843,24 @@ func Benchmark_Ctx_Body_With_Compression_Immutable(b *testing.B) { } } -// go test -run Test_Ctx_Context -func Test_Ctx_Context(t *testing.T) { +// go test -run Test_Ctx_RequestCtx +func Test_Ctx_RequestCtx(t *testing.T) { t.Parallel() app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) - require.Equal(t, "*fasthttp.RequestCtx", fmt.Sprintf("%T", c.Context())) + require.Equal(t, "*fasthttp.RequestCtx", fmt.Sprintf("%T", c.RequestCtx())) } -// go test -run Test_Ctx_UserContext -func Test_Ctx_UserContext(t *testing.T) { +// go test -run Test_Ctx_Context +func Test_Ctx_Context(t *testing.T) { t.Parallel() app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) t.Run("Nil_Context", func(t *testing.T) { t.Parallel() - ctx := c.UserContext() + ctx := c.Context() require.Equal(t, ctx, context.Background()) }) t.Run("ValueContext", func(t *testing.T) { @@ -872,8 +872,8 @@ func Test_Ctx_UserContext(t *testing.T) { }) } -// go test -run Test_Ctx_SetUserContext -func Test_Ctx_SetUserContext(t *testing.T) { +// go test -run Test_Ctx_SetContext +func Test_Ctx_SetContext(t *testing.T) { t.Parallel() app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) @@ -881,19 +881,19 @@ func Test_Ctx_SetUserContext(t *testing.T) { testKey := struct{}{} testValue := "Test Value" ctx := context.WithValue(context.Background(), testKey, testValue) //nolint: staticcheck // not needed for tests - c.SetUserContext(ctx) - require.Equal(t, testValue, c.UserContext().Value(testKey)) + c.SetContext(ctx) + require.Equal(t, testValue, c.Context().Value(testKey)) } -// go test -run Test_Ctx_UserContext_Multiple_Requests -func Test_Ctx_UserContext_Multiple_Requests(t *testing.T) { +// go test -run Test_Ctx_Context_Multiple_Requests +func Test_Ctx_Context_Multiple_Requests(t *testing.T) { t.Parallel() testKey := struct{}{} testValue := "foobar-value" app := New() app.Get("/", func(c Ctx) error { - ctx := c.UserContext() + ctx := c.Context() if ctx.Value(testKey) != nil { return c.SendStatus(StatusInternalServerError) @@ -901,7 +901,7 @@ func Test_Ctx_UserContext_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 - c.SetUserContext(ctx) + c.SetContext(ctx) return c.Status(StatusOK).SendString(fmt.Sprintf("resp_%s_returned", input)) }) @@ -913,7 +913,7 @@ func Test_Ctx_UserContext_Multiple_Requests(t *testing.T) { resp, err := app.Test(httptest.NewRequest(MethodGet, fmt.Sprintf("/?input=%d", i), nil)) require.NoError(t, err, "Unexpected error from response") - require.Equal(t, StatusOK, resp.StatusCode, "context.Context returned from c.UserContext() is reused") + require.Equal(t, StatusOK, resp.StatusCode, "context.Context returned from c.Context() is reused") b, err := io.ReadAll(resp.Body) require.NoError(t, err, "Unexpected error from reading response body") @@ -3220,7 +3220,7 @@ func Test_Ctx_SendFile_MaxAge(t *testing.T) { // check expectation require.NoError(t, err) require.Equal(t, expectFileContent, c.Response().Body()) - require.Equal(t, "public, max-age=100", string(c.Context().Response.Header.Peek(HeaderCacheControl)), "CacheControl Control") + require.Equal(t, "public, max-age=100", string(c.RequestCtx().Response.Header.Peek(HeaderCacheControl)), "CacheControl Control") require.Equal(t, StatusOK, c.Response().StatusCode()) app.ReleaseCtx(c) } diff --git a/docs/api/ctx.md b/docs/api/ctx.md index 52d8144187..56a2ea61b0 100644 --- a/docs/api/ctx.md +++ b/docs/api/ctx.md @@ -354,15 +354,20 @@ app.Get("/hello", func(c fiber.Ctx) error { ## Context -Returns [\*fasthttp.RequestCtx](https://godoc.org/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. +Context returns a context implementation that was set by user earlier or returns a non-nil, empty context, if it was not set earlier. ```go title="Signature" -func (c Ctx) Context() *fasthttp.RequestCtx +func (c Ctx) Context() context.Context ``` -:::info -Please read the [Fasthttp Documentation](https://pkg.go.dev/github.com/valyala/fasthttp?tab=doc) for more information. -::: +```go title="Example" +app.Get("/", func(c fiber.Ctx) error { + ctx := c.Context() + // ctx is context implementation set by user + + // ... +}) +``` ## Cookie @@ -1489,6 +1494,18 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` +## RequestCtx + +Returns [\*fasthttp.RequestCtx](https://godoc.org/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 Ctx) RequestCtx() *fasthttp.RequestCtx +``` + +:::info +Please read the [Fasthttp Documentation](https://pkg.go.dev/github.com/valyala/fasthttp?tab=doc) for more information. +::: + ## Response Response return the [\*fasthttp.Response](https://godoc.org/github.com/valyala/fasthttp#Response) pointer @@ -1891,18 +1908,18 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` -## SetUserContext +## SetContext -Sets the user specified implementation for context interface. +Sets the user specified implementation for context.Context interface. ```go title="Signature" -func (c Ctx) SetUserContext(ctx context.Context) +func (c Ctx) SetContext(ctx context.Context) ``` ```go title="Example" app.Get("/", func(c fiber.Ctx) error { ctx := context.Background() - c.SetUserContext(ctx) + c.SetContext(ctx) // Here ctx could be any context implementation // ... @@ -2005,24 +2022,6 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` -## UserContext - -UserContext returns a context implementation that was set by user earlier -or returns a non-nil, empty context, if it was not set earlier. - -```go title="Signature" -func (c Ctx) UserContext() context.Context -``` - -```go title="Example" -app.Get("/", func(c fiber.Ctx) error { - ctx := c.UserContext() - // ctx is context implementation set by user - - // ... -}) -``` - ## 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 leaves it listed in the current location. diff --git a/docs/middleware/timeout.md b/docs/middleware/timeout.md index 8f94f0567f..87421d20b9 100644 --- a/docs/middleware/timeout.md +++ b/docs/middleware/timeout.md @@ -8,7 +8,7 @@ There exist two distinct implementations of timeout middleware [Fiber](https://g ## New -As a `fiber.Handler` wrapper, it creates a context with `context.WithTimeout` and pass it in `UserContext`. +As a `fiber.Handler` wrapper, it creates a context with `context.WithTimeout` which is then used with `c.Context()`. If the context passed executions (eg. DB ops, Http calls) takes longer than the given duration to return, the timeout error is set and forwarded to the centralized `ErrorHandler`. @@ -38,7 +38,7 @@ func main() { app := fiber.New() h := func(c fiber.Ctx) error { sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") - if err := sleepWithContext(c.UserContext(), sleepTime); err != nil { + if err := sleepWithContext(c.Context(), sleepTime); err != nil { return fmt.Errorf("%w: execution error", err) } return nil @@ -84,7 +84,7 @@ func main() { app := fiber.New() h := func(c fiber.Ctx) error { sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") - if err := sleepWithContextWithCustomError(c.UserContext(), sleepTime); err != nil { + if err := sleepWithContextWithCustomError(c.Context(), sleepTime); err != nil { return fmt.Errorf("%w: execution error", err) } return nil @@ -116,7 +116,7 @@ func main() { db, _ := gorm.Open(postgres.Open("postgres://localhost/foodb"), &gorm.Config{}) handler := func(ctx fiber.Ctx) error { - tran := db.WithContext(ctx.UserContext()).Begin() + tran := db.WithContext(ctx.Context()).Begin() if tran = tran.Exec("SELECT pg_sleep(50)"); tran.Error != nil { return tran.Error diff --git a/docs/whats_new.md b/docs/whats_new.md index 540f82ad26..0a47dca491 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -229,6 +229,9 @@ DRAFT section - Format -> Param: body interface{} -> handlers ...ResFmt - Redirect -> c.Redirect().To() - SendFile now supports different configurations using the config parameter. +- Context has been renamed to RequestCtx which corresponds to the FastHTTP Request Context. +- UserContext has been renamed to Context which returns a context.Context object. +- SetUserContext has been renamed to SetContext. --- diff --git a/middleware/adaptor/adaptor.go b/middleware/adaptor/adaptor.go index 867f440adf..42b72101f9 100644 --- a/middleware/adaptor/adaptor.go +++ b/middleware/adaptor/adaptor.go @@ -34,7 +34,7 @@ func HTTPHandlerFunc(h http.HandlerFunc) fiber.Handler { func HTTPHandler(h http.Handler) fiber.Handler { return func(c fiber.Ctx) error { handler := fasthttpadaptor.NewFastHTTPHandler(h) - handler(c.Context()) + handler(c.RequestCtx()) return nil } } @@ -43,7 +43,7 @@ func HTTPHandler(h http.Handler) fiber.Handler { // forServer should be set to true when the http.Request is going to be passed to a http.Handler. func ConvertRequest(c fiber.Ctx, forServer bool) (*http.Request, error) { var req http.Request - if err := fasthttpadaptor.ConvertRequest(c.Context(), &req, forServer); err != nil { + if err := fasthttpadaptor.ConvertRequest(c.RequestCtx(), &req, forServer); err != nil { return nil, err //nolint:wrapcheck // This must not be wrapped } return &req, nil @@ -108,7 +108,7 @@ func HTTPMiddleware(mw func(http.Handler) http.Handler) fiber.Handler { c.Request().Header.Set(key, v) } } - CopyContextToFiberContext(r.Context(), c.Context()) + CopyContextToFiberContext(r.Context(), c.RequestCtx()) }) if err := HTTPHandler(mw(nextHandler))(c); err != nil { diff --git a/middleware/adaptor/adaptor_test.go b/middleware/adaptor/adaptor_test.go index 990d421dec..67c306febf 100644 --- a/middleware/adaptor/adaptor_test.go +++ b/middleware/adaptor/adaptor_test.go @@ -162,7 +162,7 @@ func Test_HTTPMiddleware(t *testing.T) { app := fiber.New() app.Use(HTTPMiddleware(nethttpMW)) app.Post("/", func(c fiber.Ctx) error { - value := c.Context().Value(TestContextKey) + value := c.RequestCtx().Value(TestContextKey) val, ok := value.(string) if !ok { t.Error("unexpected error on type-assertion") @@ -170,7 +170,7 @@ func Test_HTTPMiddleware(t *testing.T) { if value != nil { c.Set("context_okay", val) } - value = c.Context().Value(TestContextSecondKey) + value = c.RequestCtx().Value(TestContextSecondKey) if value != nil { val, ok := value.(string) if !ok { @@ -316,12 +316,12 @@ func testFiberToHandlerFunc(t *testing.T, checkDefaultPort bool, app ...*fiber.A fiberH := func(c fiber.Ctx) error { callsCount++ require.Equal(t, expectedMethod, c.Method(), "Method") - require.Equal(t, expectedRequestURI, string(c.Context().RequestURI()), "RequestURI") - require.Equal(t, expectedContentLength, c.Context().Request.Header.ContentLength(), "ContentLength") + require.Equal(t, expectedRequestURI, string(c.RequestCtx().RequestURI()), "RequestURI") + require.Equal(t, expectedContentLength, c.RequestCtx().Request.Header.ContentLength(), "ContentLength") require.Equal(t, expectedHost, c.Hostname(), "Host") require.Equal(t, expectedHost, string(c.Request().Header.Host()), "Host") require.Equal(t, "http://"+expectedHost, c.BaseURL(), "BaseURL") - require.Equal(t, expectedRemoteAddr, c.Context().RemoteAddr().String(), "RemoteAddr") + require.Equal(t, expectedRemoteAddr, c.RequestCtx().RemoteAddr().String(), "RemoteAddr") body := string(c.Body()) require.Equal(t, expectedBody, body, "Body") @@ -392,8 +392,8 @@ func Test_FiberHandler_RequestNilBody(t *testing.T) { fiberH := func(c fiber.Ctx) error { callsCount++ require.Equal(t, expectedMethod, c.Method(), "Method") - require.Equal(t, expectedRequestURI, string(c.Context().RequestURI()), "RequestURI") - require.Equal(t, expectedContentLength, c.Context().Request.Header.ContentLength(), "ContentLength") + require.Equal(t, expectedRequestURI, string(c.RequestCtx().RequestURI()), "RequestURI") + require.Equal(t, expectedContentLength, c.RequestCtx().Request.Header.ContentLength(), "ContentLength") _, err := c.Write([]byte("request body is nil")) return err diff --git a/middleware/cache/cache_test.go b/middleware/cache/cache_test.go index 8f00f1f156..22ab0e2895 100644 --- a/middleware/cache/cache_test.go +++ b/middleware/cache/cache_test.go @@ -894,7 +894,7 @@ func Test_Cache_MaxBytesSizes(t *testing.T) { })) app.Get("/*", func(c fiber.Ctx) error { - path := c.Context().URI().LastPathSegment() + path := c.RequestCtx().URI().LastPathSegment() size, err := strconv.Atoi(string(path)) require.NoError(t, err) return c.Send(make([]byte, size)) diff --git a/middleware/compress/compress.go b/middleware/compress/compress.go index 00f109148e..6bd1ae09c4 100644 --- a/middleware/compress/compress.go +++ b/middleware/compress/compress.go @@ -56,7 +56,7 @@ func New(config ...Config) fiber.Handler { } // Compress response - compressor(c.Context()) + compressor(c.RequestCtx()) // Return from handler return nil diff --git a/middleware/etag/etag.go b/middleware/etag/etag.go index f28a33cec5..a00dd62bf4 100644 --- a/middleware/etag/etag.go +++ b/middleware/etag/etag.go @@ -80,7 +80,7 @@ func New(config ...Config) fiber.Handler { // Check if server's ETag is weak if bytes.Equal(clientEtag[2:], etag) || bytes.Equal(clientEtag[2:], etag[2:]) { // W/1 == 1 || W/1 == W/1 - c.Context().ResetBody() + c.RequestCtx().ResetBody() return c.SendStatus(fiber.StatusNotModified) } @@ -92,7 +92,7 @@ func New(config ...Config) fiber.Handler { if bytes.Contains(clientEtag, etag) { // 1 == 1 - c.Context().ResetBody() + c.RequestCtx().ResetBody() return c.SendStatus(fiber.StatusNotModified) } diff --git a/middleware/expvar/expvar.go b/middleware/expvar/expvar.go index 8ea09e1a77..bcc92c58cd 100644 --- a/middleware/expvar/expvar.go +++ b/middleware/expvar/expvar.go @@ -25,7 +25,7 @@ func New(config ...Config) fiber.Handler { return c.Next() } if path == "/debug/vars" { - expvarhandler.ExpvarHandler(c.Context()) + expvarhandler.ExpvarHandler(c.RequestCtx()) return nil } diff --git a/middleware/idempotency/idempotency.go b/middleware/idempotency/idempotency.go index 923ce5ce9a..096384638a 100644 --- a/middleware/idempotency/idempotency.go +++ b/middleware/idempotency/idempotency.go @@ -51,7 +51,7 @@ func New(config ...Config) fiber.Handler { for header, vals := range res.Headers { for _, val := range vals { - c.Context().Response.Header.Add(header, val) + c.RequestCtx().Response.Header.Add(header, val) } } diff --git a/middleware/logger/logger_test.go b/middleware/logger/logger_test.go index 46a435ecc9..b69668f336 100644 --- a/middleware/logger/logger_test.go +++ b/middleware/logger/logger_test.go @@ -632,7 +632,7 @@ func Test_Logger_ByteSent_Streaming(t *testing.T) { app.Get("/", func(c fiber.Ctx) error { c.Set("Connection", "keep-alive") c.Set("Transfer-Encoding", "chunked") - c.Context().SetBodyStreamWriter(func(w *bufio.Writer) { + c.RequestCtx().SetBodyStreamWriter(func(w *bufio.Writer) { var i int for { i++ @@ -803,7 +803,7 @@ func Benchmark_Logger(b *testing.B) { app.Get("/", func(c fiber.Ctx) error { c.Set("Connection", "keep-alive") c.Set("Transfer-Encoding", "chunked") - c.Context().SetBodyStreamWriter(func(w *bufio.Writer) { + c.RequestCtx().SetBodyStreamWriter(func(w *bufio.Writer) { var i int for { i++ @@ -958,7 +958,7 @@ func Benchmark_Logger_Parallel(b *testing.B) { app.Get("/", func(c fiber.Ctx) error { c.Set("Connection", "keep-alive") c.Set("Transfer-Encoding", "chunked") - c.Context().SetBodyStreamWriter(func(w *bufio.Writer) { + c.RequestCtx().SetBodyStreamWriter(func(w *bufio.Writer) { var i int for { i++ diff --git a/middleware/pprof/pprof.go b/middleware/pprof/pprof.go index 13b01b533c..a8be0c059f 100644 --- a/middleware/pprof/pprof.go +++ b/middleware/pprof/pprof.go @@ -48,27 +48,27 @@ func New(config ...Config) fiber.Handler { // Switch on trimmed path against constant strings switch path { case "/": - pprofIndex(c.Context()) + pprofIndex(c.RequestCtx()) case "/cmdline": - pprofCmdline(c.Context()) + pprofCmdline(c.RequestCtx()) case "/profile": - pprofProfile(c.Context()) + pprofProfile(c.RequestCtx()) case "/symbol": - pprofSymbol(c.Context()) + pprofSymbol(c.RequestCtx()) case "/trace": - pprofTrace(c.Context()) + pprofTrace(c.RequestCtx()) case "/allocs": - pprofAllocs(c.Context()) + pprofAllocs(c.RequestCtx()) case "/block": - pprofBlock(c.Context()) + pprofBlock(c.RequestCtx()) case "/goroutine": - pprofGoroutine(c.Context()) + pprofGoroutine(c.RequestCtx()) case "/heap": - pprofHeap(c.Context()) + pprofHeap(c.RequestCtx()) case "/mutex": - pprofMutex(c.Context()) + pprofMutex(c.RequestCtx()) case "/threadcreate": - pprofThreadcreate(c.Context()) + pprofThreadcreate(c.RequestCtx()) default: // pprof index only works with trailing slash if strings.HasSuffix(path, "/") { diff --git a/middleware/redirect/redirect.go b/middleware/redirect/redirect.go index 4fb7bb8830..0e95095dd3 100644 --- a/middleware/redirect/redirect.go +++ b/middleware/redirect/redirect.go @@ -30,7 +30,7 @@ func New(config ...Config) fiber.Handler { for k, v := range cfg.rulesRegex { replacer := captureTokens(k, c.Path()) if replacer != nil { - queryString := string(c.Context().QueryArgs().QueryString()) + queryString := string(c.RequestCtx().QueryArgs().QueryString()) if queryString != "" { queryString = "?" + queryString } diff --git a/middleware/static/static.go b/middleware/static/static.go index 6cbdbd3d22..7afc77980f 100644 --- a/middleware/static/static.go +++ b/middleware/static/static.go @@ -114,7 +114,7 @@ func New(root string, cfg ...Config) fiber.Handler { }) // Serve file - fileHandler(c.Context()) + fileHandler(c.RequestCtx()) // Sets the response Content-Disposition header to attachment if the Download option is true if config.Download { @@ -122,11 +122,11 @@ func New(root string, cfg ...Config) fiber.Handler { } // Return request if found and not forbidden - status := c.Context().Response.StatusCode() + status := c.RequestCtx().Response.StatusCode() if status != fiber.StatusNotFound && status != fiber.StatusForbidden { if len(cacheControlValue) > 0 { - c.Context().Response.Header.Set(fiber.HeaderCacheControl, cacheControlValue) + c.RequestCtx().Response.Header.Set(fiber.HeaderCacheControl, cacheControlValue) } if config.ModifyResponse != nil { @@ -142,9 +142,9 @@ func New(root string, cfg ...Config) fiber.Handler { } // Reset response to default - c.Context().SetContentType("") // Issue #420 - c.Context().Response.SetStatusCode(fiber.StatusOK) - c.Context().Response.SetBodyString("") + c.RequestCtx().SetContentType("") // Issue #420 + c.RequestCtx().Response.SetStatusCode(fiber.StatusOK) + c.RequestCtx().Response.SetBodyString("") // Next middleware return c.Next() diff --git a/middleware/timeout/timeout.go b/middleware/timeout/timeout.go index 5a9711ce22..a88f2e90b1 100644 --- a/middleware/timeout/timeout.go +++ b/middleware/timeout/timeout.go @@ -11,9 +11,9 @@ import ( // New implementation of timeout middleware. Set custom errors(context.DeadlineExceeded vs) for get fiber.ErrRequestTimeout response. func New(h fiber.Handler, t time.Duration, tErrs ...error) fiber.Handler { return func(ctx fiber.Ctx) error { - timeoutContext, cancel := context.WithTimeout(ctx.UserContext(), t) + timeoutContext, cancel := context.WithTimeout(ctx.Context(), t) defer cancel() - ctx.SetUserContext(timeoutContext) + ctx.SetContext(timeoutContext) if err := h(ctx); err != nil { if errors.Is(err, context.DeadlineExceeded) { return fiber.ErrRequestTimeout diff --git a/middleware/timeout/timeout_test.go b/middleware/timeout/timeout_test.go index b08445eb2a..2e1756184c 100644 --- a/middleware/timeout/timeout_test.go +++ b/middleware/timeout/timeout_test.go @@ -20,7 +20,7 @@ func Test_WithContextTimeout(t *testing.T) { h := New(func(c fiber.Ctx) error { sleepTime, err := time.ParseDuration(c.Params("sleepTime") + "ms") require.NoError(t, err) - if err := sleepWithContext(c.UserContext(), sleepTime, context.DeadlineExceeded); err != nil { + if err := sleepWithContext(c.Context(), sleepTime, context.DeadlineExceeded); err != nil { return fmt.Errorf("%w: l2 wrap", fmt.Errorf("%w: l1 wrap ", err)) } return nil @@ -52,7 +52,7 @@ func Test_WithContextTimeoutWithCustomError(t *testing.T) { h := New(func(c fiber.Ctx) error { sleepTime, err := time.ParseDuration(c.Params("sleepTime") + "ms") require.NoError(t, err) - if err := sleepWithContext(c.UserContext(), sleepTime, ErrFooTimeOut); err != nil { + if err := sleepWithContext(c.Context(), sleepTime, ErrFooTimeOut); err != nil { return fmt.Errorf("%w: execution error", err) } return nil diff --git a/redirect.go b/redirect.go index ebbcb499b8..bc79314922 100644 --- a/redirect.go +++ b/redirect.go @@ -141,7 +141,7 @@ func (r *Redirect) With(key, value string, level ...uint8) *Redirect { // You can get them by using: Redirect().OldInputs(), Redirect().OldInput() func (r *Redirect) WithInput() *Redirect { // Get content-type - ctype := utils.ToLower(utils.UnsafeString(r.c.Context().Request.Header.ContentType())) + ctype := utils.ToLower(utils.UnsafeString(r.c.RequestCtx().Request.Header.ContentType())) ctype = binder.FilterFlags(utils.ParseVendorSpecificContentType(ctype)) oldInput := make(map[string]string) diff --git a/redirect_test.go b/redirect_test.go index 7544aec01d..1570d05fbf 100644 --- a/redirect_test.go +++ b/redirect_test.go @@ -42,7 +42,7 @@ func Test_Redirect_To_WithFlashMessages(t *testing.T) { require.Equal(t, 302, c.Response().StatusCode()) require.Equal(t, "http://example.com", string(c.Response().Header.Peek(HeaderLocation))) - c.Context().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing + c.RequestCtx().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing var msgs redirectionMsgs _, err = msgs.UnmarshalMsg([]byte(c.Cookies(FlashCookieName))) @@ -185,7 +185,7 @@ func Test_Redirect_Back_WithFlashMessages(t *testing.T) { require.Equal(t, 302, c.Response().StatusCode()) require.Equal(t, "/", string(c.Response().Header.Peek(HeaderLocation))) - c.Context().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing + c.RequestCtx().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing var msgs redirectionMsgs _, err = msgs.UnmarshalMsg([]byte(c.Cookies(FlashCookieName))) @@ -236,7 +236,7 @@ func Test_Redirect_Route_WithFlashMessages(t *testing.T) { require.Equal(t, 302, c.Response().StatusCode()) require.Equal(t, "/user", string(c.Response().Header.Peek(HeaderLocation))) - c.Context().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing + c.RequestCtx().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing var msgs redirectionMsgs _, err = msgs.UnmarshalMsg([]byte(c.Cookies(FlashCookieName))) @@ -273,7 +273,7 @@ func Test_Redirect_Route_WithOldInput(t *testing.T) { require.Equal(t, 302, c.Response().StatusCode()) require.Equal(t, "/user", string(c.Response().Header.Peek(HeaderLocation))) - c.Context().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing + c.RequestCtx().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing var msgs redirectionMsgs _, err = msgs.UnmarshalMsg([]byte(c.Cookies(FlashCookieName))) @@ -309,7 +309,7 @@ func Test_Redirect_Route_WithOldInput(t *testing.T) { require.Equal(t, 302, c.Response().StatusCode()) require.Equal(t, "/user", string(c.Response().Header.Peek(HeaderLocation))) - c.Context().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing + c.RequestCtx().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing var msgs redirectionMsgs _, err = msgs.UnmarshalMsg([]byte(c.Cookies(FlashCookieName))) @@ -353,7 +353,7 @@ func Test_Redirect_Route_WithOldInput(t *testing.T) { require.Equal(t, 302, c.Response().StatusCode()) require.Equal(t, "/user", string(c.Response().Header.Peek(HeaderLocation))) - c.Context().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing + c.RequestCtx().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing var msgs redirectionMsgs _, err = msgs.UnmarshalMsg([]byte(c.Cookies(FlashCookieName))) @@ -538,7 +538,7 @@ func Benchmark_Redirect_Route_WithFlashMessages(b *testing.B) { require.Equal(b, 302, c.Response().StatusCode()) require.Equal(b, "/user", string(c.Response().Header.Peek(HeaderLocation))) - c.Context().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing + c.RequestCtx().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing var msgs redirectionMsgs _, err = msgs.UnmarshalMsg([]byte(c.Cookies(FlashCookieName))) @@ -629,7 +629,7 @@ func Benchmark_Redirect_processFlashMessages(b *testing.B) { c.Redirect().processFlashMessages() } - c.Context().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing + c.RequestCtx().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing var msgs redirectionMsgs _, err := msgs.UnmarshalMsg([]byte(c.Cookies(FlashCookieName))) From 60932d3d565c66f9e7beca6d1bc27bc3166e55de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Nov 2024 15:14:49 +0100 Subject: [PATCH 23/31] build(deps): bump codecov/codecov-action from 4.6.0 to 5.0.0 (#3207) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.6.0 to 5.0.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/v4.6.0...v5.0.0) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .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 ebf39c161c..e86be2956b 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@v4.6.0 + uses: codecov/codecov-action@v5.0.0 with: token: ${{ secrets.CODECOV_TOKEN }} file: ./coverage.txt From 2c242e70c79e44ffa016e6726cb9a2b08c98898f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Nov 2024 15:14:58 +0100 Subject: [PATCH 24/31] build(deps): bump DavidAnson/markdownlint-cli2-action from 17 to 18 (#3208) Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 17 to 18. - [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases) - [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/v17...v18) --- updated-dependencies: - dependency-name: DavidAnson/markdownlint-cli2-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/markdown.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/markdown.yml b/.github/workflows/markdown.yml index d8d74905e4..a015149c22 100644 --- a/.github/workflows/markdown.yml +++ b/.github/workflows/markdown.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v4 - name: Run markdownlint-cli2 - uses: DavidAnson/markdownlint-cli2-action@v17 + uses: DavidAnson/markdownlint-cli2-action@v18 with: globs: | **/*.md From f725ded92bac13e773f92ff478e1a461c160abd3 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Sat, 16 Nov 2024 00:34:20 +0800 Subject: [PATCH 25/31] =?UTF-8?q?=F0=9F=94=A5=20feat:=20Add=20Context=20Su?= =?UTF-8?q?pport=20to=20RequestID=20Middleware=20(#3200)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Rename UserContext() to Context(). Rename Context() to RequestCtx() * feat: add requestID in UserContext * Update Ctxt docs and What's new * Remove extra blank lines * ♻️ Refactor: merge issue #3186 * 🔥 Feature: improve FromContext func and test * 📚 Doc: improve requestid middleware * ♻️ Refactor: Rename interface to any * fix: Modify structure sorting to reduce memory usage --------- Co-authored-by: Juan Calderon-Perez Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> --- docs/middleware/requestid.md | 10 ++++ middleware/requestid/requestid.go | 25 ++++++++-- middleware/requestid/requestid_test.go | 67 +++++++++++++++++++------- 3 files changed, 82 insertions(+), 20 deletions(-) diff --git a/docs/middleware/requestid.md b/docs/middleware/requestid.md index 739a4a6190..01ec569e3c 100644 --- a/docs/middleware/requestid.md +++ b/docs/middleware/requestid.md @@ -49,6 +49,16 @@ func handler(c fiber.Ctx) error { } ``` +In version v3, Fiber will inject `requestID` into the built-in `Context` of Go. + +```go +func handler(c fiber.Ctx) error { + id := requestid.FromContext(c.Context()) + log.Printf("Request ID: %s", id) + return c.SendString("Hello, World!") +} +``` + ## Config | Property | Type | Description | Default | diff --git a/middleware/requestid/requestid.go b/middleware/requestid/requestid.go index 8e521dc650..ef67e6f21c 100644 --- a/middleware/requestid/requestid.go +++ b/middleware/requestid/requestid.go @@ -1,7 +1,10 @@ package requestid import ( + "context" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/log" ) // The contextKey type is unexported to prevent collisions with context keys defined in @@ -36,6 +39,10 @@ func New(config ...Config) fiber.Handler { // Add the request ID to locals c.Locals(requestIDKey, rid) + // Add the request ID to UserContext + ctx := context.WithValue(c.Context(), requestIDKey, rid) + c.SetContext(ctx) + // Continue stack return c.Next() } @@ -43,9 +50,21 @@ func New(config ...Config) fiber.Handler { // FromContext returns the request ID from context. // If there is no request ID, an empty string is returned. -func FromContext(c fiber.Ctx) string { - if rid, ok := c.Locals(requestIDKey).(string); ok { - return rid +// Supported context types: +// - fiber.Ctx: Retrieves request ID from Locals +// - context.Context: Retrieves request ID from context values +func FromContext(c any) string { + switch ctx := c.(type) { + case fiber.Ctx: + if rid, ok := ctx.Locals(requestIDKey).(string); ok { + return rid + } + case context.Context: + if rid, ok := ctx.Value(requestIDKey).(string); ok { + return rid + } + default: + log.Errorf("Unsupported context type: %T. Expected fiber.Ctx or context.Context", c) } return "" } diff --git a/middleware/requestid/requestid_test.go b/middleware/requestid/requestid_test.go index c739407be0..ad36884aca 100644 --- a/middleware/requestid/requestid_test.go +++ b/middleware/requestid/requestid_test.go @@ -51,26 +51,59 @@ func Test_RequestID_Next(t *testing.T) { require.Equal(t, fiber.StatusNotFound, resp.StatusCode) } -// go test -run Test_RequestID_Locals +// go test -run Test_RequestID_FromContext func Test_RequestID_FromContext(t *testing.T) { t.Parallel() + reqID := "ThisIsARequestId" - app := fiber.New() - app.Use(New(Config{ - Generator: func() string { - return reqID + type args struct { + inputFunc func(c fiber.Ctx) any + } + + tests := []struct { + args args + name string + }{ + { + name: "From fiber.Ctx", + args: args{ + inputFunc: func(c fiber.Ctx) any { + return c + }, + }, }, - })) - - var ctxVal string - - app.Use(func(c fiber.Ctx) error { - ctxVal = FromContext(c) - return c.Next() - }) - - _, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) - require.NoError(t, err) - require.Equal(t, reqID, ctxVal) + { + name: "From context.Context", + args: args{ + inputFunc: func(c fiber.Ctx) any { + return c.Context() + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + app := fiber.New() + app.Use(New(Config{ + Generator: func() string { + return reqID + }, + })) + + var ctxVal string + + app.Use(func(c fiber.Ctx) error { + ctxVal = FromContext(tt.args.inputFunc(c)) + return c.Next() + }) + + _, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + require.NoError(t, err) + require.Equal(t, reqID, ctxVal) + }) + } } From a12ca10cac05340fb42143943a9059da144b3d94 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Sat, 16 Nov 2024 09:24:14 -0500 Subject: [PATCH 26/31] =?UTF-8?q?=F0=9F=93=9A=20Doc:=20Updates=20to=20API?= =?UTF-8?q?=20documentation=20and=20README=20(#3205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Updates to documentation * Update .github/README.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Remove trailing spaces * Updates based on PR comments * Update docs/api/bind.md * Update docs/api/redirect.md --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: RW --- .github/README.md | 329 ++++++++++++++++++----------- docs/api/app.md | 487 +++++++++++++++++++++++++++---------------- docs/api/bind.md | 327 ++++++++++++++++------------- docs/api/hooks.md | 89 ++++---- docs/api/log.md | 64 +++--- docs/api/redirect.md | 34 ++- 6 files changed, 801 insertions(+), 529 deletions(-) diff --git a/.github/README.md b/.github/README.md index 61159412e8..aa58a8d83c 100644 --- a/.github/README.md +++ b/.github/README.md @@ -39,7 +39,7 @@ Fiber v3 is currently in beta and under active development. While it offers exci ## ⚙️ Installation -Fiber requires **Go version `1.22` or higher** to run. If you need to install or upgrade Go, visit the [official Go download page](https://go.dev/dl/). To start setting up your project. Create a new directory for your project and navigate into it. Then, initialize your project with Go modules by executing the following command in your terminal: +Fiber requires **Go version `1.22` or higher** to run. If you need to install or upgrade Go, visit the [official Go download page](https://go.dev/dl/). To start setting up your project, create a new directory for your project and navigate into it. Then, initialize your project with Go modules by executing the following command in your terminal: ```bash go mod init github.com/your/repo @@ -59,7 +59,7 @@ This command fetches the Fiber package and adds it to your project's dependencie Getting started with Fiber is easy. Here's a basic example to create a simple web server that responds with "Hello, World 👋!" on the root path. This example demonstrates initializing a new Fiber app, setting up a route, and starting the server. -```go +```go title="Example" package main import ( @@ -133,7 +133,16 @@ Listed below are some of the common examples. If you want to see more code examp ### 📖 [**Basic Routing**](https://docs.gofiber.io/#basic-routing) -```go +```go title="Example" +package main + +import ( + "fmt" + "log" + + "github.com/gofiber/fiber/v3" +) + func main() { app := fiber.New() @@ -169,23 +178,33 @@ func main() { log.Fatal(app.Listen(":3000")) } - ``` #### 📖 [**Route Naming**](https://docs.gofiber.io/api/app#name) -```go +```go title="Example" +package main + +import ( + "encoding/json" + "fmt" + "log" + + "github.com/gofiber/fiber/v3" +) + func main() { app := fiber.New() - // GET /api/register app.Get("/api/*", func(c fiber.Ctx) error { msg := fmt.Sprintf("✋ %s", c.Params("*")) return c.SendString(msg) // => ✋ register }).Name("api") - data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") - fmt.Print(string(data)) + route := app.GetRoute("api") + + data, _ := json.MarshalIndent(route, "", " ") + fmt.Println(string(data)) // Prints: // { // "method": "GET", @@ -198,15 +217,24 @@ func main() { log.Fatal(app.Listen(":3000")) } - ``` #### 📖 [**Serving Static Files**](https://docs.gofiber.io/api/app#static) -```go +```go title="Example" +package main + +import ( + "log" + + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/static" +) + func main() { app := fiber.New() + // Serve static files from the "./public" directory app.Get("/*", static.New("./public")) // => http://localhost:3000/js/script.js // => http://localhost:3000/css/style.css @@ -215,27 +243,36 @@ func main() { // => http://localhost:3000/prefix/js/script.js // => http://localhost:3000/prefix/css/style.css + // Serve a single file for any unmatched routes app.Get("*", static.New("./public/index.html")) - // => http://localhost:3000/any/path/shows/index/html + // => http://localhost:3000/any/path/shows/index.html log.Fatal(app.Listen(":3000")) } - ``` #### 📖 [**Middleware & Next**](https://docs.gofiber.io/api/ctx#next) -```go +```go title="Example" +package main + +import ( + "fmt" + "log" + + "github.com/gofiber/fiber/v3" +) + func main() { app := fiber.New() - // Match any route + // Middleware that matches any route app.Use(func(c fiber.Ctx) error { fmt.Println("🥇 First handler") return c.Next() }) - // Match all routes starting with /api + // Middleware that matches all routes starting with /api app.Use("/api", func(c fiber.Ctx) error { fmt.Println("🥈 Second handler") return c.Next() @@ -249,13 +286,12 @@ func main() { log.Fatal(app.Listen(":3000")) } - ```
📚 Show more code examples -### Views engines +### Views Engines 📖 [Config](https://docs.gofiber.io/api/fiber#config) 📖 [Engines](https://github.com/gofiber/template) @@ -263,11 +299,9 @@ func main() { Fiber defaults to the [html/template](https://pkg.go.dev/html/template/) when no view engine is set. -If you want to execute partials or use a different engine like [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) or [pug](https://github.com/Joker/jade) etc.. - -Checkout our [Template](https://github.com/gofiber/template) package that support multiple view engines. +If you want to execute partials or use a different engine like [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache), or [pug](https://github.com/Joker/jade), etc., check out our [Template](https://github.com/gofiber/template) package that supports multiple view engines. -```go +```go title="Example" package main import ( @@ -278,12 +312,12 @@ import ( ) func main() { - // You can setup Views engine before initiation app: + // Initialize a new Fiber app with Pug template engine app := fiber.New(fiber.Config{ Views: pug.New("./views", ".pug"), }) - // And now, you can call template `./views/home.pug` like this: + // Define a route that renders the "home.pug" template app.Get("/", func(c fiber.Ctx) error { return c.Render("home", fiber.Map{ "title": "Homepage", @@ -295,24 +329,32 @@ func main() { } ``` -### Grouping routes into chains +### Grouping Routes into Chains 📖 [Group](https://docs.gofiber.io/api/app#group) -```go +```go title="Example" +package main + +import ( + "log" + + "github.com/gofiber/fiber/v3" +) + func middleware(c fiber.Ctx) error { - fmt.Println("Don't mind me!") + log.Println("Middleware executed") return c.Next() } func handler(c fiber.Ctx) error { - return c.SendString(c.Path()) + return c.SendString("Handler response") } func main() { app := fiber.New() - // Root API route + // Root API group with middleware api := app.Group("/api", middleware) // /api // API v1 routes @@ -325,16 +367,15 @@ func main() { v2.Get("/list", handler) // /api/v2/list v2.Get("/user", handler) // /api/v2/user - // ... + log.Fatal(app.Listen(":3000")) } - ``` -### Middleware logger +### Middleware Logger 📖 [Logger](https://docs.gofiber.io/api/middleware/logger) -```go +```go title="Example" package main import ( @@ -347,9 +388,13 @@ import ( func main() { app := fiber.New() + // Use Logger middleware app.Use(logger.New()) - // ... + // Define routes + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("Hello, Logger!") + }) log.Fatal(app.Listen(":3000")) } @@ -359,7 +404,9 @@ func main() { 📖 [CORS](https://docs.gofiber.io/api/middleware/cors) -```go +```go title="Example" +package main + import ( "log" @@ -370,9 +417,13 @@ import ( func main() { app := fiber.New() + // Use CORS middleware with default settings app.Use(cors.New()) - // ... + // Define routes + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("CORS enabled!") + }) log.Fatal(app.Listen(":3000")) } @@ -384,28 +435,36 @@ Check CORS by passing any domain in `Origin` header: curl -H "Origin: http://example.com" --verbose http://localhost:3000 ``` -### Custom 404 response +### Custom 404 Response 📖 [HTTP Methods](https://docs.gofiber.io/api/ctx#status) -```go +```go title="Example" +package main + +import ( + "log" + + "github.com/gofiber/fiber/v3" +) + func main() { app := fiber.New() + // Define routes app.Get("/", static.New("./public")) app.Get("/demo", func(c fiber.Ctx) error { - return c.SendString("This is a demo!") + return c.SendString("This is a demo page!") }) app.Post("/register", func(c fiber.Ctx) error { - return c.SendString("Welcome!") + return c.SendString("Registration successful!") }) - // Last middleware to match anything + // Middleware to handle 404 Not Found app.Use(func(c fiber.Ctx) error { - return c.SendStatus(404) - // => 404 "Not Found" + return c.SendStatus(fiber.StatusNotFound) // => 404 "Not Found" }) log.Fatal(app.Listen(":3000")) @@ -416,7 +475,15 @@ func main() { 📖 [JSON](https://docs.gofiber.io/api/ctx#json) -```go +```go title="Example" +package main + +import ( + "log" + + "github.com/gofiber/fiber/v3" +) + type User struct { Name string `json:"name"` Age int `json:"age"` @@ -425,11 +492,13 @@ type User struct { func main() { app := fiber.New() + // Route that returns a JSON object app.Get("/user", func(c fiber.Ctx) error { return c.JSON(&User{"John", 20}) // => {"name":"John", "age":20} }) + // Route that returns a JSON map app.Get("/json", func(c fiber.Ctx) error { return c.JSON(fiber.Map{ "success": true, @@ -446,7 +515,9 @@ func main() { 📖 [Websocket](https://github.com/gofiber/websocket) -```go +```go title="Example" +package main + import ( "log" @@ -455,26 +526,31 @@ import ( ) func main() { - app := fiber.New() - - app.Get("/ws", websocket.New(func(c *websocket.Conn) { - for { - mt, msg, err := c.ReadMessage() - if err != nil { - log.Println("read:", err) - break - } - log.Printf("recv: %s", msg) - err = c.WriteMessage(mt, msg) - if err != nil { - log.Println("write:", err) - break - } - } - })) - - log.Fatal(app.Listen(":3000")) - // ws://localhost:3000/ws + app := fiber.New() + + // WebSocket route + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + defer c.Close() + for { + // Read message from client + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", msg) + + // Write message back to client + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // Connect via WebSocket at ws://localhost:3000/ws } ``` @@ -482,42 +558,46 @@ func main() { 📖 [More Info](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) -```go +```go title="Example" +package main + import ( + "bufio" + "fmt" "log" + "time" "github.com/gofiber/fiber/v3" "github.com/valyala/fasthttp" ) func main() { - app := fiber.New() - - app.Get("/sse", func(c fiber.Ctx) error { - c.Set("Content-Type", "text/event-stream") - c.Set("Cache-Control", "no-cache") - c.Set("Connection", "keep-alive") - c.Set("Transfer-Encoding", "chunked") - - c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { - fmt.Println("WRITER") - var i int - - for { - i++ - msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) - fmt.Fprintf(w, "data: Message: %s\n\n", msg) - fmt.Println(msg) - - w.Flush() - time.Sleep(5 * time.Second) - } - })) + app := fiber.New() - return nil - }) + // Server-Sent Events route + app.Get("/sse", func(c fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(func(w *bufio.Writer) { + var i int + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + }) - log.Fatal(app.Listen(":3000")) + return nil + }) + + log.Fatal(app.Listen(":3000")) } ``` @@ -525,7 +605,9 @@ func main() { 📖 [Recover](https://docs.gofiber.io/api/middleware/recover) -```go +```go title="Example" +package main + import ( "log" @@ -536,8 +618,10 @@ import ( func main() { app := fiber.New() + // Use Recover middleware to handle panics gracefully app.Use(recover.New()) + // Route that intentionally panics app.Get("/", func(c fiber.Ctx) error { panic("normally this would crash your app") }) @@ -546,13 +630,13 @@ func main() { } ``` -
- ### Using Trusted Proxy 📖 [Config](https://docs.gofiber.io/api/fiber#config) -```go +```go title="Example" +package main + import ( "log" @@ -561,13 +645,20 @@ import ( func main() { app := fiber.New(fiber.Config{ + // Configure trusted proxies - WARNING: Only trust proxies you control + // Using TrustProxy: true with unrestricted IPs can lead to IP spoofing TrustProxy: true, TrustProxyConfig: fiber.TrustProxyConfig{ - Proxies: []string{"0.0.0.0", "1.1.1.1/30"}, // IP address or IP address range + Proxies: []string{"10.0.0.0/8", "172.16.0.0/12"}, // Example: Internal network ranges only }, ProxyHeader: fiber.HeaderXForwardedFor, }) + // Define routes + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("Trusted Proxy Configured!") + }) + log.Fatal(app.Listen(":3000")) } ``` @@ -583,14 +674,14 @@ Here is a list of middleware that are included within the Fiber framework. | [adaptor](https://github.com/gofiber/fiber/tree/main/middleware/adaptor) | Converter for net/http handlers to/from Fiber request handlers. | | [basicauth](https://github.com/gofiber/fiber/tree/main/middleware/basicauth) | Provides HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | | [cache](https://github.com/gofiber/fiber/tree/main/middleware/cache) | Intercept and cache HTTP responses. | -| [compress](https://github.com/gofiber/fiber/tree/main/middleware/compress) | Compression middleware for Fiber, with support for `deflate`, `gzip`, `brotli` and `zstd`. | +| [compress](https://github.com/gofiber/fiber/tree/main/middleware/compress) | Compression middleware for Fiber, with support for `deflate`, `gzip`, `brotli` and `zstd`. | | [cors](https://github.com/gofiber/fiber/tree/main/middleware/cors) | Enable cross-origin resource sharing (CORS) with various options. | | [csrf](https://github.com/gofiber/fiber/tree/main/middleware/csrf) | Protect from CSRF exploits. | | [earlydata](https://github.com/gofiber/fiber/tree/main/middleware/earlydata) | Adds support for TLS 1.3's early data ("0-RTT") feature. | | [encryptcookie](https://github.com/gofiber/fiber/tree/main/middleware/encryptcookie) | Encrypt middleware which encrypts cookie values. | | [envvar](https://github.com/gofiber/fiber/tree/main/middleware/envvar) | Expose environment variables with providing an optional config. | | [etag](https://github.com/gofiber/fiber/tree/main/middleware/etag) | Allows for caches to be more efficient and save bandwidth, as a web server does not need to resend a full response if the content has not changed. | -| [expvar](https://github.com/gofiber/fiber/tree/main/middleware/expvar) | Serves via its HTTP server runtime exposed variants in the JSON format. | +| [expvar](https://github.com/gofiber/fiber/tree/main/middleware/expvar) | Serves via its HTTP server runtime exposed variables in the JSON format. | | [favicon](https://github.com/gofiber/fiber/tree/main/middleware/favicon) | Ignore favicon from logs or serve from memory if a file path is provided. | | [healthcheck](https://github.com/gofiber/fiber/tree/main/middleware/healthcheck) | Liveness and Readiness probes for Fiber. | | [helmet](https://github.com/gofiber/fiber/tree/main/middleware/helmet) | Helps secure your apps by setting various HTTP headers. | @@ -606,7 +697,7 @@ Here is a list of middleware that are included within the Fiber framework. | [rewrite](https://github.com/gofiber/fiber/tree/main/middleware/rewrite) | Rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | | [session](https://github.com/gofiber/fiber/tree/main/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | | [skip](https://github.com/gofiber/fiber/tree/main/middleware/skip) | Skip middleware that skips a wrapped handler if a predicate is true. | -| [static](https://github.com/gofiber/fiber/tree/main/middleware/static) | Static middleware for Fiber that serves static files such as **images**, **CSS,** and **JavaScript**. | +| [static](https://github.com/gofiber/fiber/tree/main/middleware/static) | Static middleware for Fiber that serves static files such as **images**, **CSS**, and **JavaScript**. | | [timeout](https://github.com/gofiber/fiber/tree/main/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | ## 🧬 External Middleware @@ -615,13 +706,13 @@ List of externally hosted middleware modules and maintained by the [Fiber team]( | Middleware | Description | | :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------- | -| [contrib](https://github.com/gofiber/contrib) | Third party middlewares | -| [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | -| [template](https://github.com/gofiber/template) | This package contains 9 template engines that can be used with Fiber `v3` Go version 1.22 or higher is required. | +| [contrib](https://github.com/gofiber/contrib) | Third-party middlewares | +| [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | +| [template](https://github.com/gofiber/template) | This package contains 9 template engines that can be used with Fiber `v3`. Go version 1.22 or higher is required. | ## 🕶️ Awesome List -For more articles, middlewares, examples or tools check our [awesome list](https://github.com/gofiber/awesome-fiber). +For more articles, middlewares, examples, or tools, check our [awesome list](https://github.com/gofiber/awesome-fiber). ## 👍 Contribute @@ -629,12 +720,12 @@ If you want to say **Thank You** and/or support the active development of `Fiber 1. Add a [GitHub Star](https://github.com/gofiber/fiber/stargazers) to the project. 2. Tweet about the project [on your 𝕏 (Twitter)](https://x.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). -3. Write a review or tutorial on [Medium](https://medium.com/), [Dev.to](https://dev.to/) or personal blog. +3. Write a review or tutorial on [Medium](https://medium.com/), [Dev.to](https://dev.to/) or your personal blog. 4. Support the project by donating a [cup of coffee](https://buymeacoff.ee/fenny). -## 🖥️ Development +## 💻 Development -To ensure your contributions are ready for a Pull Request, please use the following `Makefile` commands. These tools help maintain code quality, consistency. +To ensure your contributions are ready for a Pull Request, please use the following `Makefile` commands. These tools help maintain code quality and consistency. - **make help**: Display available commands. - **make audit**: Conduct quality checks. @@ -649,22 +740,22 @@ Run these commands to ensure your code adheres to project standards and best pra ## ☕ Supporters -Fiber is an open source project that runs on donations to pay the bills e.g. our domain name, gitbook, netlify and serverless hosting. If you want to support Fiber, you can ☕ [**buy a coffee here**](https://buymeacoff.ee/fenny). +Fiber is an open-source project that runs on donations to pay the bills, e.g., our domain name, GitBook, Netlify, and serverless hosting. If you want to support Fiber, you can ☕ [**buy a coffee here**](https://buymeacoff.ee/fenny). | | User | Donation | -| :--------------------------------------------------------- | :----------------------------------------------- | :------- | -| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | -| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | -| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | -| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | -| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | -| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | -| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | -| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | -| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | -| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | -| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | -| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ---------------------------------------------------------- | ------------------------------------------------ | -------- | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | | ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | | ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | | ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | diff --git a/docs/api/app.md b/docs/api/app.md index ef9c2ea08d..8c7f8979bc 100644 --- a/docs/api/app.md +++ b/docs/api/app.md @@ -17,18 +17,28 @@ import RoutingHandler from './../partials/routing/handler.md'; ### Mounting -You can Mount Fiber instance using the [`app.Use`](./app.md#use) method similar to [`express`](https://expressjs.com/en/api.html#router.use). +You can mount a Fiber instance using the [`app.Use`](./app.md#use) method, similar to [`Express`](https://expressjs.com/en/api.html#router.use). + +```go title="Example" +package main + +import ( + "log" + + "github.com/gofiber/fiber/v3" +) -```go title="Examples" func main() { app := fiber.New() micro := fiber.New() + + // Mount the micro app on the "/john" route app.Use("/john", micro) // GET /john/doe -> 200 OK - + micro.Get("/doe", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) }) - + log.Fatal(app.Listen(":3000")) } ``` @@ -41,7 +51,15 @@ The `MountPath` property contains one or more path patterns on which a sub-app w func (app *App) MountPath() string ``` -```go title="Examples" +```go title="Example" +package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v3" +) + func main() { app := fiber.New() one := fiber.New() @@ -51,16 +69,17 @@ func main() { two.Use("/three", three) one.Use("/two", two) app.Use("/one", one) - - one.MountPath() // "/one" - two.MountPath() // "/one/two" - three.MountPath() // "/one/two/three" - app.MountPath() // "" + + fmt.Println("Mount paths:") + fmt.Println("one.MountPath():", one.MountPath()) // "/one" + fmt.Println("two.MountPath():", two.MountPath()) // "/one/two" + fmt.Println("three.MountPath():", three.MountPath()) // "/one/two/three" + fmt.Println("app.MountPath():", app.MountPath()) // "" } ``` :::caution -Mounting order is important for MountPath. If you want to get mount paths properly, you should start mounting from the deepest app. +Mounting order is important for `MountPath`. To get mount paths properly, you should start mounting from the deepest app. ::: ### Group @@ -71,21 +90,33 @@ You can group routes by creating a `*Group` struct. func (app *App) Group(prefix string, handlers ...Handler) Router ``` -```go title="Examples" +```go title="Example" +package main + +import ( + "log" + + "github.com/gofiber/fiber/v3" +) + func main() { - app := fiber.New() + app := fiber.New() + + api := app.Group("/api", handler) // /api - api := app.Group("/api", handler) // /api + v1 := api.Group("/v1", handler) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user - v1 := api.Group("/v1", handler) // /api/v1 - v1.Get("/list", handler) // /api/v1/list - v1.Get("/user", handler) // /api/v1/user + v2 := api.Group("/v2", handler) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user - v2 := api.Group("/v2", handler) // /api/v2 - v2.Get("/list", handler) // /api/v2/list - v2.Get("/user", handler) // /api/v2/user + log.Fatal(app.Listen(":3000")) +} - log.Fatal(app.Listen(":3000")) +func handler(c fiber.Ctx) error { + return c.SendString("Handler response") } ``` @@ -104,64 +135,73 @@ func (app *App) Route(path string) Register ```go type Register interface { - All(handler Handler, middleware ...Handler) Register - Get(handler Handler, middleware ...Handler) Register - Head(handler Handler, middleware ...Handler) Register - Post(handler Handler, middleware ...Handler) Register - Put(handler Handler, middleware ...Handler) Register - Delete(handler Handler, middleware ...Handler) Register - Connect(handler Handler, middleware ...Handler) Register - Options(handler Handler, middleware ...Handler) Register - Trace(handler Handler, middleware ...Handler) Register - Patch(handler Handler, middleware ...Handler) Register - - Add(methods []string, handler Handler, middleware ...Handler) Register - - Route(path string) Register + All(handler Handler, middleware ...Handler) Register + Get(handler Handler, middleware ...Handler) Register + Head(handler Handler, middleware ...Handler) Register + Post(handler Handler, middleware ...Handler) Register + Put(handler Handler, middleware ...Handler) Register + Delete(handler Handler, middleware ...Handler) Register + Connect(handler Handler, middleware ...Handler) Register + Options(handler Handler, middleware ...Handler) Register + Trace(handler Handler, middleware ...Handler) Register + Patch(handler Handler, middleware ...Handler) Register + + Add(methods []string, handler Handler, middleware ...Handler) Register + + Route(path string) Register } ``` -```go title="Examples" +```go title="Example" +package main + +import ( + "log" + + "github.com/gofiber/fiber/v3" +) + func main() { - app := fiber.New() - - // use `Route` as chainable route declaration method - app.Route("/test").Get(func(c fiber.Ctx) error { - return c.SendString("GET /test") - }) - - app.Route("/events").all(func(c fiber.Ctx) error { - // runs for all HTTP verbs first - // think of it as route specific middleware! - }) - .get(func(c fiber.Ctx) error { - return c.SendString("GET /events") - }) - .post(func(c fiber.Ctx) error { - // maybe add a new event... - }) - - // combine multiple routes - app.Route("/v2").Route("/user").Get(func(c fiber.Ctx) error { - return c.SendString("GET /v2/user") - }) - - // use multiple methods - app.Route("/api").Get(func(c fiber.Ctx) error { - return c.SendString("GET /api") - }).Post(func(c fiber.Ctx) error { - return c.SendString("POST /api") - }) - - log.Fatal(app.Listen(":3000")) + app := fiber.New() + + // Use `Route` as a chainable route declaration method + app.Route("/test").Get(func(c fiber.Ctx) error { + return c.SendString("GET /test") + }) + + app.Route("/events").All(func(c fiber.Ctx) error { + // Runs for all HTTP verbs first + // Think of it as route-specific middleware! + }). + Get(func(c fiber.Ctx) error { + return c.SendString("GET /events") + }). + Post(func(c fiber.Ctx) error { + // Maybe add a new event... + return c.SendString("POST /events") + }) + + // Combine multiple routes + app.Route("/v2").Route("/user").Get(func(c fiber.Ctx) error { + return c.SendString("GET /v2/user") + }) + + // Use multiple methods + app.Route("/api").Get(func(c fiber.Ctx) error { + return c.SendString("GET /api") + }).Post(func(c fiber.Ctx) error { + return c.SendString("POST /api") + }) + + log.Fatal(app.Listen(":3000")) } ``` ### HandlersCount -This method returns the amount of registered handlers. +This method returns the number of registered handlers. ```go title="Signature" func (app *App) HandlersCount() uint32 @@ -169,13 +209,22 @@ func (app *App) HandlersCount() uint32 ### Stack -This method returns the original router stack +This method returns the original router stack. ```go title="Signature" func (app *App) Stack() [][]*Route ``` -```go title="Examples" +```go title="Example" +package main + +import ( + "encoding/json" + "log" + + "github.com/gofiber/fiber/v3" +) + var handler = func(c fiber.Ctx) error { return nil } func main() { @@ -187,7 +236,7 @@ func main() { data, _ := json.MarshalIndent(app.Stack(), "", " ") fmt.Println(string(data)) - app.Listen(":3000") + log.Fatal(app.Listen(":3000")) } ``` @@ -228,25 +277,32 @@ func main() { ### Name -This method assigns the name of latest created route. +This method assigns the name to the latest created route. ```go title="Signature" func (app *App) Name(name string) Router ``` -```go title="Examples" -var handler = func(c fiber.Ctx) error { return nil } +```go title="Example" +package main + +import ( + "encoding/json" + "fmt" + "log" + + "github.com/gofiber/fiber/v3" +) func main() { + var handler = func(c fiber.Ctx) error { return nil } + app := fiber.New() app.Get("/", handler) app.Name("index") - app.Get("/doe", handler).Name("home") - app.Trace("/tracer", handler).Name("tracert") - app.Delete("/delete", handler).Name("delete") a := app.Group("/a") @@ -255,10 +311,9 @@ func main() { a.Get("/test", handler).Name("test") data, _ := json.MarshalIndent(app.Stack(), "", " ") - fmt.Print(string(data)) - - app.Listen(":3000") + fmt.Println(string(data)) + log.Fatal(app.Listen(":3000")) } ``` @@ -335,25 +390,34 @@ func main() { ### GetRoute -This method gets the route by name. +This method retrieves a route by its name. ```go title="Signature" func (app *App) GetRoute(name string) Route ``` -```go title="Examples" -var handler = func(c fiber.Ctx) error { return nil } +```go title="Example" +package main + +import ( + "encoding/json" + "fmt" + "log" + + "github.com/gofiber/fiber/v3" +) func main() { app := fiber.New() app.Get("/", handler).Name("index") - data, _ := json.MarshalIndent(app.GetRoute("index"), "", " ") - fmt.Print(string(data)) + route := app.GetRoute("index") + data, _ := json.MarshalIndent(route, "", " ") + fmt.Println(string(data)) - app.Listen(":3000") + log.Fatal(app.Listen(":3000")) } ``` @@ -373,22 +437,38 @@ func main() { ### GetRoutes -This method gets all routes. +This method retrieves all routes. ```go title="Signature" func (app *App) GetRoutes(filterUseOption ...bool) []Route ``` -When filterUseOption equal to true, it will filter the routes registered by the middleware. +When `filterUseOption` is set to `true`, it filters out routes registered by middleware. + +```go title="Example" +package main + +import ( + "encoding/json" + "fmt" + "log" + + "github.com/gofiber/fiber/v3" +) -```go title="Examples" func main() { app := fiber.New() - app.Post("/", func (c fiber.Ctx) error { + + app.Post("/", func(c fiber.Ctx) error { return c.SendString("Hello, World!") }).Name("index") - data, _ := json.MarshalIndent(app.GetRoutes(true), "", " ") - fmt.Print(string(data)) + + routes := app.GetRoutes(true) + + data, _ := json.MarshalIndent(routes, "", " ") + fmt.Println(string(data)) + + log.Fatal(app.Listen(":3000")) } ``` @@ -410,7 +490,7 @@ func main() { ## Config -Config returns the [app config](./fiber.md#config) as value \( read-only \). +`Config` returns the [app config](./fiber.md#config) as a value (read-only). ```go title="Signature" func (app *App) Config() Config @@ -418,7 +498,7 @@ func (app *App) Config() Config ## Handler -Handler returns the server handler that can be used to serve custom [`\*fasthttp.RequestCtx`](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx) requests. +`Handler` returns the server handler that can be used to serve custom [`\*fasthttp.RequestCtx`](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx) requests. ```go title="Signature" func (app *App) Handler() fasthttp.RequestHandler @@ -426,7 +506,7 @@ func (app *App) Handler() fasthttp.RequestHandler ## ErrorHandler -Errorhandler executes the process which was defined for the application in case of errors, this is used in some cases in middlewares. +`ErrorHandler` executes the process defined for the application in case of errors. This is used in some cases in middlewares. ```go title="Signature" func (app *App) ErrorHandler(ctx Ctx, err error) error @@ -434,15 +514,23 @@ func (app *App) ErrorHandler(ctx Ctx, err error) error ## NewCtxFunc -NewCtxFunc allows to customize the ctx struct as we want. +`NewCtxFunc` allows you to customize the `ctx` struct as needed. ```go title="Signature" func (app *App) NewCtxFunc(function func(app *App) CustomCtx) ``` -```go title="Examples" +```go title="Example" +package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v3" +) + type CustomCtx struct { - DefaultCtx + fiber.DefaultCtx } // Custom method @@ -450,86 +538,102 @@ func (c *CustomCtx) Params(key string, defaultValue ...string) string { return "prefix_" + c.DefaultCtx.Params(key) } -app := New() -app.NewCtxFunc(func(app *fiber.App) fiber.CustomCtx { - return &CustomCtx{ - DefaultCtx: *NewDefaultCtx(app), - } -}) -// curl http://localhost:3000/123 -app.Get("/:id", func(c Ctx) error { - // use custom method - output: prefix_123 - return c.SendString(c.Params("id")) -}) +func main() { + app := fiber.New() + + app.NewCtxFunc(func(app *fiber.App) fiber.CustomCtx { + return &CustomCtx{ + DefaultCtx: *fiber.NewDefaultCtx(app), + } + }) + + app.Get("/:id", func(c fiber.Ctx) error { + // Use custom method - output: prefix_123 + return c.SendString(c.Params("id")) + }) + + log.Fatal(app.Listen(":3000")) +} ``` ## RegisterCustomBinder -You can register custom binders to use as [`Bind().Custom("name")`](bind.md#custom). -They should be compatible with CustomBinder interface. +You can register custom binders to use with [`Bind().Custom("name")`](bind.md#custom). They should be compatible with the `CustomBinder` interface. ```go title="Signature" func (app *App) RegisterCustomBinder(binder CustomBinder) ``` -```go title="Examples" -app := fiber.New() +```go title="Example" +package main + +import ( + "log" + + "github.com/gofiber/fiber/v3" + "gopkg.in/yaml.v2" +) + +type User struct { + Name string `yaml:"name"` +} + +type customBinder struct{} -// My custom binder -customBinder := &customBinder{} -// Name of custom binder, which will be used as Bind().Custom("name") func (*customBinder) Name() string { return "custom" } -// Is used in the Body Bind method to check if the binder should be used for custom mime types + func (*customBinder) MIMETypes() []string { return []string{"application/yaml"} } -// Parse the body and bind it to the out interface -func (*customBinder) Parse(c Ctx, out any) error { - // parse yaml body + +func (*customBinder) Parse(c fiber.Ctx, out any) error { + // Parse YAML body return yaml.Unmarshal(c.Body(), out) } -// Register custom binder -app.RegisterCustomBinder(customBinder) - -// curl -X POST http://localhost:3000/custom -H "Content-Type: application/yaml" -d "name: John" -app.Post("/custom", func(c Ctx) error { - var user User - // output: {Name:John} - // Custom binder is used by the name - if err := c.Bind().Custom("custom", &user); err != nil { - return err - } - // ... - return c.JSON(user) -}) -// curl -X POST http://localhost:3000/normal -H "Content-Type: application/yaml" -d "name: Doe" -app.Post("/normal", func(c Ctx) error { - var user User - // output: {Name:Doe} - // Custom binder is used by the mime type - if err := c.Bind().Body(&user); err != nil { - return err - } - // ... - return c.JSON(user) -}) + +func main() { + app := fiber.New() + + // Register custom binder + app.RegisterCustomBinder(&customBinder{}) + + app.Post("/custom", func(c fiber.Ctx) error { + var user User + // Use Custom binder by name + if err := c.Bind().Custom("custom", &user); err != nil { + return err + } + return c.JSON(user) + }) + + app.Post("/normal", func(c fiber.Ctx) error { + var user User + // Custom binder is used by the MIME type + if err := c.Bind().Body(&user); err != nil { + return err + } + return c.JSON(user) + }) + + log.Fatal(app.Listen(":3000")) +} ``` ## RegisterCustomConstraint -RegisterCustomConstraint allows to register custom constraint. +`RegisterCustomConstraint` allows you to register custom constraints. ```go title="Signature" func (app *App) RegisterCustomConstraint(constraint CustomConstraint) ``` -See [Custom Constraint](../guide/routing.md#custom-constraint) section for more information. +See the [Custom Constraint](../guide/routing.md#custom-constraint) section for more information. ## SetTLSHandler -Use SetTLSHandler to set [ClientHelloInfo](https://datatracker.ietf.org/doc/html/rfc8446#section-4.1.2) when using TLS with Listener. +Use `SetTLSHandler` to set [`ClientHelloInfo`](https://datatracker.ietf.org/doc/html/rfc8446#section-4.1.2) when using TLS with a `Listener`. ```go title="Signature" func (app *App) SetTLSHandler(tlsHandler *TLSHandler) @@ -537,38 +641,53 @@ func (app *App) SetTLSHandler(tlsHandler *TLSHandler) ## Test -Testing your application is done with the **Test** method. Use this method for creating `_test.go` files or when you need to debug your routing logic. The default timeout is `1s` if you want to disable a timeout altogether, pass `-1` as a second argument. +Testing your application is done with the `Test` method. Use this method for creating `_test.go` files or when you need to debug your routing logic. The default timeout is `1s`; to disable a timeout altogether, pass `-1` as the second argument. ```go title="Signature" func (app *App) Test(req *http.Request, msTimeout ...int) (*http.Response, error) ``` -```go title="Examples" -// Create route with GET method for test: -app.Get("/", func(c fiber.Ctx) error { - fmt.Println(c.BaseURL()) // => http://google.com - fmt.Println(c.Get("X-Custom-Header")) // => hi +```go title="Example" +package main + +import ( + "fmt" + "io" + "log" + "net/http" + "net/http/httptest" - return c.SendString("hello, World!") -}) + "github.com/gofiber/fiber/v3" +) + +func main() { + app := fiber.New() + + // Create route with GET method for test: + app.Get("/", func(c fiber.Ctx) error { + fmt.Println(c.BaseURL()) // => http://google.com + fmt.Println(c.Get("X-Custom-Header")) // => hi + return c.SendString("hello, World!") + }) -// http.Request -req := httptest.NewRequest("GET", "http://google.com", nil) -req.Header.Set("X-Custom-Header", "hi") + // Create http.Request + req := httptest.NewRequest("GET", "http://google.com", nil) + req.Header.Set("X-Custom-Header", "hi") -// http.Response -resp, _ := app.Test(req) + // Perform the test + resp, _ := app.Test(req) -// Do something with results: -if resp.StatusCode == fiber.StatusOK { - body, _ := io.ReadAll(resp.Body) - fmt.Println(string(body)) // => Hello, World! + // Do something with the results: + if resp.StatusCode == fiber.StatusOK { + body, _ := io.ReadAll(resp.Body) + fmt.Println(string(body)) // => hello, World! + } } ``` ## Hooks -Hooks is a method to return [hooks](./hooks.md) property. +`Hooks` is a method to return the [hooks](./hooks.md) property. ```go title="Signature" func (app *App) Hooks() *Hooks @@ -576,7 +695,7 @@ func (app *App) Hooks() *Hooks ## RebuildTree -The RebuildTree method is designed to rebuild the route tree and enable dynamic route registration. It returns a pointer to the App instance. +The `RebuildTree` method is designed to rebuild the route tree and enable dynamic route registration. It returns a pointer to the `App` instance. ```go title="Signature" func (app *App) RebuildTree() *App @@ -588,16 +707,32 @@ func (app *App) RebuildTree() *App Here’s an example of how to define and register routes dynamically: -```go -app.Get("/define", func(c Ctx) error { // Define a new route dynamically - app.Get("/dynamically-defined", func(c Ctx) error { // Adding a dynamically defined route - return c.SendStatus(http.StatusOK) - }) +```go title="Example" +package main + +import ( + "log" + + "github.com/gofiber/fiber/v3" +) + +func main() { + app := fiber.New() + + app.Get("/define", func(c fiber.Ctx) error { + // Define a new route dynamically + app.Get("/dynamically-defined", func(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) - app.RebuildTree() // Rebuild the route tree to register the new route + // Rebuild the route tree to register the new route + app.RebuildTree() - return c.SendStatus(http.StatusOK) -}) + return c.SendStatus(fiber.StatusOK) + }) + + log.Fatal(app.Listen(":3000")) +} ``` -In this example, a new route is defined and then `RebuildTree()` is called to make sure the new route is registered and available. +In this example, a new route is defined and then `RebuildTree()` is called to ensure the new route is registered and available. diff --git a/docs/api/bind.md b/docs/api/bind.md index 73256cbbb8..2ad0854ca9 100644 --- a/docs/api/bind.md +++ b/docs/api/bind.md @@ -6,13 +6,11 @@ sidebar_position: 4 toc_max_heading_level: 4 --- -Bindings are used to parse the request/response body, query parameters, cookies and much more into a struct. +Bindings are used to parse the request/response body, query parameters, cookies, and much more into a struct. :::info - -All binder returned value are only valid within the handler. Do not store any references. +All binder returned values are only valid within the handler. Do not store any references. Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more...](../#zero-allocation) - ::: ## Binders @@ -32,22 +30,21 @@ Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more.. Binds the request body to a struct. -It is important to specify the correct struct tag based on the content type to be parsed. For example, if you want to parse a JSON body with a field called Pass, you would use a struct field of `json:"pass"`. +It is important to specify the correct struct tag based on the content type to be parsed. For example, if you want to parse a JSON body with a field called `Pass`, you would use a struct field with `json:"pass"`. -| content-type | struct tag | +| Content-Type | Struct Tag | | ----------------------------------- | ---------- | -| `application/x-www-form-urlencoded` | form | -| `multipart/form-data` | form | -| `application/json` | json | -| `application/xml` | xml | -| `text/xml` | xml | +| `application/x-www-form-urlencoded` | `form` | +| `multipart/form-data` | `form` | +| `application/json` | `json` | +| `application/xml` | `xml` | +| `text/xml` | `xml` | ```go title="Signature" func (b *Bind) Body(out any) error ``` ```go title="Example" -// Field names should start with an uppercase letter type Person struct { Name string `json:"name" xml:"name" form:"name"` Pass string `json:"pass" xml:"pass" form:"pass"` @@ -65,34 +62,35 @@ app.Post("/", func(c fiber.Ctx) error { // ... }) +``` -// Run tests with the following curl commands - -// curl -X POST -H "Content-Type: application/json" --data "{\"name\":\"john\",\"pass\":\"doe\"}" localhost:3000 +Run tests with the following `curl` commands: -// curl -X POST -H "Content-Type: application/xml" --data "johndoe" localhost:3000 +```bash +# JSON +curl -X POST -H "Content-Type: application/json" --data "{\"name\":\"john\",\"pass\":\"doe\"}" localhost:3000 -// curl -X POST -H "Content-Type: application/x-www-form-urlencoded" --data "name=john&pass=doe" localhost:3000 +# XML +curl -X POST -H "Content-Type: application/xml" --data "johndoe" localhost:3000 -// curl -X POST -F name=john -F pass=doe http://localhost:3000 +# Form URL-Encoded +curl -X POST -H "Content-Type: application/x-www-form-urlencoded" --data "name=john&pass=doe" localhost:3000 -// curl -X POST "http://localhost:3000/?name=john&pass=doe" +# Multipart Form +curl -X POST -F name=john -F pass=doe http://localhost:3000 ``` -**The methods for the various bodies can also be used directly:** - -#### Form +### Form Binds the request form body to a struct. -It is important to specify the correct struct tag based on the content type to be parsed. For example, if you want to parse a Form body with a field called Pass, you would use a struct field of `form:"pass"`. +It is important to specify the correct struct tag based on the content type to be parsed. For example, if you want to parse a form body with a field called `Pass`, you would use a struct field with `form:"pass"`. ```go title="Signature" func (b *Bind) Form(out any) error ``` ```go title="Example" -// Field names should start with an uppercase letter type Person struct { Name string `form:"name"` Pass string `form:"pass"` @@ -110,24 +108,25 @@ app.Post("/", func(c fiber.Ctx) error { // ... }) +``` -// Run tests with the following curl commands +Run tests with the following `curl` command: -// curl -X POST -H "Content-Type: application/x-www-form-urlencoded" --data "name=john&pass=doe" localhost:3000 +```bash +curl -X POST -H "Content-Type: application/x-www-form-urlencoded" --data "name=john&pass=doe" localhost:3000 ``` -#### JSON +### JSON -Binds the request json body to a struct. +Binds the request JSON body to a struct. -It is important to specify the correct struct tag based on the content type to be parsed. For example, if you want to parse a JSON body with a field called Pass, you would use a struct field of `json:"pass"`. +It is important to specify the correct struct tag based on the content type to be parsed. For example, if you want to parse a JSON body with a field called `Pass`, you would use a struct field with `json:"pass"`. ```go title="Signature" func (b *Bind) JSON(out any) error ``` ```go title="Example" -// Field names should start with an uppercase letter type Person struct { Name string `json:"name"` Pass string `json:"pass"` @@ -145,18 +144,19 @@ app.Post("/", func(c fiber.Ctx) error { // ... }) +``` -// Run tests with the following curl commands - -// curl -X POST -H "Content-Type: application/json" --data "{\"name\":\"john\",\"pass\":\"doe\"}" localhost:3000 +Run tests with the following `curl` command: +```bash +curl -X POST -H "Content-Type: application/json" --data "{\"name\":\"john\",\"pass\":\"doe\"}" localhost:3000 ``` -#### MultipartForm +### MultipartForm Binds the request multipart form body to a struct. -It is important to specify the correct struct tag based on the content type to be parsed. For example, if you want to parse a MultipartForm body with a field called Pass, you would use a struct field of `form:"pass"`. +It is important to specify the correct struct tag based on the content type to be parsed. For example, if you want to parse a multipart form body with a field called `Pass`, you would use a struct field with `form:"pass"`. ```go title="Signature" func (b *Bind) MultipartForm(out any) error @@ -181,18 +181,19 @@ app.Post("/", func(c fiber.Ctx) error { // ... }) +``` -// Run tests with the following curl commands - -// curl -X POST -H "Content-Type: multipart/form-data" -F "name=john" -F "pass=doe" localhost:3000 +Run tests with the following `curl` command: +```bash +curl -X POST -H "Content-Type: multipart/form-data" -F "name=john" -F "pass=doe" localhost:3000 ``` -#### XML +### XML -Binds the request xml form body to a struct. +Binds the request XML body to a struct. -It is important to specify the correct struct tag based on the content type to be parsed. For example, if you want to parse an XML body with a field called Pass, you would use a struct field of `xml:"pass"`. +It is important to specify the correct struct tag based on the content type to be parsed. For example, if you want to parse an XML body with a field called `Pass`, you would use a struct field with `xml:"pass"`. ```go title="Signature" func (b *Bind) XML(out any) error @@ -217,27 +218,28 @@ app.Post("/", func(c fiber.Ctx) error { // ... }) +``` -// Run tests with the following curl commands +Run tests with the following `curl` command: -// curl -X POST -H "Content-Type: application/xml" --data "johndoe" localhost:3000 +```bash +curl -X POST -H "Content-Type: application/xml" --data "johndoe" localhost:3000 ``` ### Cookie -This method is similar to [Body-Binding](#body), but for cookie parameters. -It is important to use the struct tag "cookie". For example, if you want to parse a cookie with a field called Age, you would use a struct field of `cookie:"age"`. +This method is similar to [Body Binding](#body), but for cookie parameters. +It is important to use the struct tag `cookie`. For example, if you want to parse a cookie with a field called `Age`, you would use a struct field with `cookie:"age"`. ```go title="Signature" func (b *Bind) Cookie(out any) error ``` ```go title="Example" -// Field names should start with an uppercase letter type Person struct { - Name string `cookie:"name"` - Age int `cookie:"age"` - Job bool `cookie:"job"` + Name string `cookie:"name"` + Age int `cookie:"age"` + Job bool `cookie:"job"` } app.Get("/", func(c fiber.Ctx) error { @@ -247,29 +249,32 @@ app.Get("/", func(c fiber.Ctx) error { return err } - log.Println(p.Name) // Joseph - log.Println(p.Age) // 23 - log.Println(p.Job) // true + log.Println(p.Name) // Joseph + log.Println(p.Age) // 23 + log.Println(p.Job) // true }) -// Run tests with the following curl command -// curl.exe --cookie "name=Joseph; age=23; job=true" http://localhost:8000/ +``` + +Run tests with the following `curl` command: + +```bash +curl --cookie "name=Joseph; age=23; job=true" http://localhost:8000/ ``` ### Header -This method is similar to [Body-Binding](#body), but for request headers. -It is important to use the struct tag "header". For example, if you want to parse a request header with a field called Pass, you would use a struct field of `header:"pass"`. +This method is similar to [Body Binding](#body), but for request headers. +It is important to use the struct tag `header`. For example, if you want to parse a request header with a field called `Pass`, you would use a struct field with `header:"pass"`. ```go title="Signature" func (b *Bind) Header(out any) error ``` ```go title="Example" -// Field names should start with an uppercase letter type Person struct { - Name string `header:"name"` - Pass string `header:"pass"` - Products []string `header:"products"` + Name string `header:"name"` + Pass string `header:"pass"` + Products []string `header:"products"` } app.Get("/", func(c fiber.Ctx) error { @@ -281,30 +286,32 @@ app.Get("/", func(c fiber.Ctx) error { log.Println(p.Name) // john log.Println(p.Pass) // doe - log.Println(p.Products) // [shoe, hat] + log.Println(p.Products) // [shoe hat] // ... }) -// Run tests with the following curl command +``` + +Run tests with the following `curl` command: -// curl "http://localhost:3000/" -H "name: john" -H "pass: doe" -H "products: shoe,hat" +```bash +curl "http://localhost:3000/" -H "name: john" -H "pass: doe" -H "products: shoe,hat" ``` ### Query -This method is similar to [Body-Binding](#body), but for query parameters. -It is important to use the struct tag "query". For example, if you want to parse a query parameter with a field called Pass, you would use a struct field of `query:"pass"`. +This method is similar to [Body Binding](#body), but for query parameters. +It is important to use the struct tag `query`. For example, if you want to parse a query parameter with a field called `Pass`, you would use a struct field with `query:"pass"`. ```go title="Signature" func (b *Bind) Query(out any) error ``` ```go title="Example" -// Field names should start with an uppercase letter type Person struct { - Name string `query:"name"` - Pass string `query:"pass"` - Products []string `query:"products"` + Name string `query:"name"` + Pass string `query:"pass"` + Products []string `query:"products"` } app.Get("/", func(c fiber.Ctx) error { @@ -314,40 +321,41 @@ app.Get("/", func(c fiber.Ctx) error { return err } - log.Println(p.Name) // john - log.Println(p.Pass) // doe - // fiber.Config{EnableSplittingOnParsers: false} - default - log.Println(p.Products) // ["shoe,hat"] - // fiber.Config{EnableSplittingOnParsers: true} + log.Println(p.Name) // john + log.Println(p.Pass) // doe + // Depending on fiber.Config{EnableSplittingOnParsers: false} - default + log.Println(p.Products) // ["shoe,hat"] + // With fiber.Config{EnableSplittingOnParsers: true} // log.Println(p.Products) // ["shoe", "hat"] - // ... }) -// Run tests with the following curl command +``` -// curl "http://localhost:3000/?name=john&pass=doe&products=shoe,hat" +Run tests with the following `curl` command: + +```bash +curl "http://localhost:3000/?name=john&pass=doe&products=shoe,hat" ``` :::info -For more parser settings please look here [Config](fiber.md#enablesplittingonparsers) +For more parser settings, please refer to [Config](fiber.md#enablesplittingonparsers) ::: ### RespHeader -This method is similar to [Body-Binding](#body), but for response headers. -It is important to use the struct tag "respHeader". For example, if you want to parse a request header with a field called Pass, you would use a struct field of `respHeader:"pass"`. +This method is similar to [Body Binding](#body), but for response headers. +It is important to use the struct tag `respHeader`. For example, if you want to parse a response header with a field called `Pass`, you would use a struct field with `respHeader:"pass"`. ```go title="Signature" -func (b *Bind) Header(out any) error +func (b *Bind) RespHeader(out any) error ``` ```go title="Example" -// Field names should start with an uppercase letter type Person struct { - Name string `respHeader:"name"` - Pass string `respHeader:"pass"` - Products []string `respHeader:"products"` + Name string `respHeader:"name"` + Pass string `respHeader:"pass"` + Products []string `respHeader:"products"` } app.Get("/", func(c fiber.Ctx) error { @@ -359,18 +367,22 @@ app.Get("/", func(c fiber.Ctx) error { log.Println(p.Name) // john log.Println(p.Pass) // doe - log.Println(p.Products) // [shoe, hat] + log.Println(p.Products) // [shoe hat] // ... }) -// Run tests with the following curl command +``` -// curl "http://localhost:3000/" -H "name: john" -H "pass: doe" -H "products: shoe,hat" +Run tests with the following `curl` command: + +```bash +curl "http://localhost:3000/" -H "name: john" -H "pass: doe" -H "products: shoe,hat" ``` ### URI -This method is similar to [Body-Binding](#body), but for path parameters. It is important to use the struct tag "uri". For example, if you want to parse a path parameter with a field called Pass, you would use a struct field of uri:"pass" +This method is similar to [Body Binding](#body), but for path parameters. +It is important to use the struct tag `uri`. For example, if you want to parse a path parameter with a field called `Pass`, you would use a struct field with `uri:"pass"`. ```go title="Signature" func (b *Bind) URI(out any) error @@ -379,20 +391,24 @@ func (b *Bind) URI(out any) error ```go title="Example" // GET http://example.com/user/111 app.Get("/user/:id", func(c fiber.Ctx) error { - param := struct {ID uint `uri:"id"`}{} + param := struct { + ID uint `uri:"id"` + }{} - c.Bind().URI(¶m) // "{"id": 111}" + if err := c.Bind().URI(¶m); err != nil { + return err + } // ... + return c.SendString(fmt.Sprintf("User ID: %d", param.ID)) }) - ``` ## Custom To use custom binders, you have to use this method. -You can register them from [RegisterCustomBinder](./app.md#registercustombinder) method of Fiber instance. +You can register them using the [RegisterCustomBinder](./app.md#registercustombinder) method of the Fiber instance. ```go title="Signature" func (b *Bind) Custom(name string, dest any) error @@ -402,47 +418,50 @@ func (b *Bind) Custom(name string, dest any) error app := fiber.New() // My custom binder -customBinder := &customBinder{} -// Name of custom binder, which will be used as Bind().Custom("name") -func (*customBinder) Name() string { +type customBinder struct{} + +func (cb *customBinder) Name() string { return "custom" } -// Is used in the Body Bind method to check if the binder should be used for custom mime types -func (*customBinder) MIMETypes() []string { + +func (cb *customBinder) MIMETypes() []string { return []string{"application/yaml"} } -// Parse the body and bind it to the out interface -func (*customBinder) Parse(c Ctx, out any) error { - // parse yaml body + +func (cb *customBinder) Parse(c fiber.Ctx, out any) error { + // parse YAML body return yaml.Unmarshal(c.Body(), out) } + // Register custom binder -app.RegisterCustomBinder(customBinder) +app.RegisterCustomBinder(&customBinder{}) + +type User struct { + Name string `yaml:"name"` +} // curl -X POST http://localhost:3000/custom -H "Content-Type: application/yaml" -d "name: John" -app.Post("/custom", func(c Ctx) error { +app.Post("/custom", func(c fiber.Ctx) error { var user User - // output: {Name:John} - // Custom binder is used by the name + // Use Custom binder by name if err := c.Bind().Custom("custom", &user); err != nil { return err } - // ... return c.JSON(user) }) ``` -Internally they are also used in the [Body](#body) method. -For this the MIMETypes method is used to check if the custom binder should be used for the given content type. +Internally, custom binders are also used in the [Body](#body) method. +The `MIMETypes` method is used to check if the custom binder should be used for the given content type. ## Options -For more control over the error handling, you can use the following methods. +For more control over error handling, you can use the following methods. ### Must -If you want to handle binder errors automatically, you can use Must. -If there's an error it'll return error and 400 as HTTP status. +If you want to handle binder errors automatically, you can use `Must`. +If there's an error, it will return the error and set HTTP status to `400 Bad Request`. ```go title="Signature" func (b *Bind) Must() *Bind @@ -450,8 +469,8 @@ func (b *Bind) Must() *Bind ### Should -To handle binder errors manually, you can prefer Should method. -It's default behavior of binder. +To handle binder errors manually, you can use the `Should` method. +It's the default behavior of the binder. ```go title="Signature" func (b *Bind) Should() *Bind @@ -459,7 +478,7 @@ func (b *Bind) Should() *Bind ## SetParserDecoder -Allow you to config BodyParser/QueryParser decoder, base on schema's options, providing possibility to add custom type for parsing. +Allows you to configure the BodyParser/QueryParser decoder based on schema options, providing the possibility to add custom types for parsing. ```go title="Signature" func SetParserDecoder(parserConfig fiber.ParserConfig{ @@ -477,34 +496,34 @@ func SetParserDecoder(parserConfig fiber.ParserConfig{ type CustomTime time.Time -// String() returns the time in string +// String returns the time in string format func (ct *CustomTime) String() string { t := time.Time(*ct).String() - return t - } - - // Register the converter for CustomTime type format as 2006-01-02 - var timeConverter = func(value string) reflect.Value { - fmt.Println("timeConverter", value) + return t +} + +// Converter for CustomTime type with format "2006-01-02" +var timeConverter = func(value string) reflect.Value { + fmt.Println("timeConverter:", value) if v, err := time.Parse("2006-01-02", value); err == nil { - return reflect.ValueOf(v) + return reflect.ValueOf(CustomTime(v)) } return reflect.Value{} } customTime := fiber.ParserType{ - Customtype: CustomTime{}, + CustomType: CustomTime{}, Converter: timeConverter, } -// Add setting to the Decoder +// Add custom type to the Decoder settings fiber.SetParserDecoder(fiber.ParserConfig{ IgnoreUnknownKeys: true, ParserType: []fiber.ParserType{customTime}, ZeroEmpty: true, }) -// Example to use CustomType, you pause custom time format not in RFC3339 +// Example using CustomTime with non-RFC3339 format type Demo struct { Date CustomTime `form:"date" query:"date"` Title string `form:"title" query:"title"` @@ -513,31 +532,38 @@ type Demo struct { app.Post("/body", func(c fiber.Ctx) error { var d Demo - c.BodyParser(&d) - fmt.Println("d.Date", d.Date.String()) + if err := c.Bind().Body(&d); err != nil { + return err + } + fmt.Println("d.Date:", d.Date.String()) return c.JSON(d) }) app.Get("/query", func(c fiber.Ctx) error { var d Demo - c.QueryParser(&d) - fmt.Println("d.Date", d.Date.String()) + if err := c.Bind().Query(&d); err != nil { + return err + } + fmt.Println("d.Date:", d.Date.String()) return c.JSON(d) }) -// curl -X POST -F title=title -F body=body -F date=2021-10-20 http://localhost:3000/body +// Run tests with the following curl commands: -// curl -X GET "http://localhost:3000/query?title=title&body=body&date=2021-10-20" +# Body Binding +curl -X POST -F title=title -F body=body -F date=2021-10-20 http://localhost:3000/body +# Query Binding +curl -X GET "http://localhost:3000/query?title=title&body=body&date=2021-10-20" ``` ## Validation Validation is also possible with the binding methods. You can specify your validation rules using the `validate` struct tag. -Specify your struct validator in the [config](./fiber.md#structvalidator) +Specify your struct validator in the [config](./fiber.md#structvalidator). -Setup your validator in the config: +### Setup Your Validator in the Config ```go title="Example" import "github.com/go-playground/validator/v10" @@ -546,18 +572,18 @@ type structValidator struct { validate *validator.Validate } -// Validator needs to implement the Validate method +// Validate method implementation func (v *structValidator) Validate(out any) error { return v.validate.Struct(out) } -// Setup your validator in the config +// Setup your validator in the Fiber config app := fiber.New(fiber.Config{ StructValidator: &structValidator{validate: validator.New()}, }) ``` -Usage of the validation in the binding methods: +### Usage of Validation in Binding Methods ```go title="Example" type Person struct { @@ -568,7 +594,7 @@ type Person struct { app.Post("/", func(c fiber.Ctx) error { p := new(Person) - if err := c.Bind().JSON(p); err != nil {// <- here you receive the validation errors + if err := c.Bind().JSON(p); err != nil { // Receives validation errors return err } }) @@ -578,13 +604,13 @@ app.Post("/", func(c fiber.Ctx) error { You can set default values for fields in the struct by using the `default` struct tag. Supported types: -- bool -- float variants (float32, float64) -- int variants (int, int8, int16, int32, int64) -- uint variants (uint, uint8, uint16, uint32, uint64) -- string -- a slice of the above types. As shown in the example above, **| should be used to separate between slice items**. -- a pointer to one of the above types **(pointer to slice and slice of pointers are not supported)**. +- `bool` +- Float variants (`float32`, `float64`) +- Int variants (`int`, `int8`, `int16`, `int32`, `int64`) +- Uint variants (`uint`, `uint8`, `uint16`, `uint32`, `uint64`) +- `string` +- A slice of the above types. Use `|` to separate slice items. +- A pointer to one of the above types (**pointers to slices and slices of pointers are not supported**). ```go title="Example" type Person struct { @@ -600,13 +626,16 @@ app.Get("/", func(c fiber.Ctx) error { return err } - log.Println(p.Name) // john - log.Println(p.Pass) // doe - log.Println(p.Products) // ["shoe,hat"] + log.Println(p.Name) // john + log.Println(p.Pass) // doe + log.Println(p.Products) // ["shoe", "hat"] // ... }) -// Run tests with the following curl command +``` + +Run tests with the following `curl` command: -// curl "http://localhost:3000/?pass=doe" +```bash +curl "http://localhost:3000/?pass=doe" ``` diff --git a/docs/api/hooks.md b/docs/api/hooks.md index 828d68a359..4852866602 100644 --- a/docs/api/hooks.md +++ b/docs/api/hooks.md @@ -7,7 +7,7 @@ sidebar_position: 7 import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -With Fiber v2.30.0, you can execute custom user functions when to run some methods. Here is a list of these hooks: +With Fiber you can execute custom user functions at specific method execution points. Here is a list of these hooks: - [OnRoute](#onroute) - [OnName](#onname) @@ -21,7 +21,7 @@ With Fiber v2.30.0, you can execute custom user functions when to run some metho ## Constants ```go -// Handlers define a function to create hooks for Fiber. +// Handlers define functions to create hooks for Fiber. type OnRouteHandler = func(Route) error type OnNameHandler = OnRouteHandler type OnGroupHandler = func(Group) error @@ -34,7 +34,7 @@ type OnMountHandler = func(*App) error ## OnRoute -OnRoute is a hook to execute user functions on each route registration. Also you can get route properties by **route** parameter. +`OnRoute` is a hook to execute user functions on each route registration. You can access route properties via the **route** parameter. ```go title="Signature" func (h *Hooks) OnRoute(handler ...OnRouteHandler) @@ -42,10 +42,10 @@ func (h *Hooks) OnRoute(handler ...OnRouteHandler) ## OnName -OnName is a hook to execute user functions on each route naming. Also you can get route properties by **route** parameter. +`OnName` is a hook to execute user functions on each route naming. You can access route properties via the **route** parameter. :::caution -OnName only works with naming routes, not groups. +`OnName` only works with named routes, not groups. ::: ```go title="Signature" @@ -73,13 +73,11 @@ func main() { app.Hooks().OnName(func(r fiber.Route) error { fmt.Print("Name: " + r.Name + ", ") - return nil }) app.Hooks().OnName(func(r fiber.Route) error { fmt.Print("Method: " + r.Method + "\n") - return nil }) @@ -104,7 +102,7 @@ func main() { ## OnGroup -OnGroup is a hook to execute user functions on each group registration. Also you can get group properties by **group** parameter. +`OnGroup` is a hook to execute user functions on each group registration. You can access group properties via the **group** parameter. ```go title="Signature" func (h *Hooks) OnGroup(handler ...OnGroupHandler) @@ -112,10 +110,10 @@ func (h *Hooks) OnGroup(handler ...OnGroupHandler) ## OnGroupName -OnGroupName is a hook to execute user functions on each group naming. Also you can get group properties by **group** parameter. +`OnGroupName` is a hook to execute user functions on each group naming. You can access group properties via the **group** parameter. :::caution -OnGroupName only works with naming groups, not routes. +`OnGroupName` only works with named groups, not routes. ::: ```go title="Signature" @@ -124,7 +122,7 @@ func (h *Hooks) OnGroupName(handler ...OnGroupNameHandler) ## OnListen -OnListen is a hook to execute user functions on Listen, ListenTLS, Listener. +`OnListen` is a hook to execute user functions on `Listen`, `ListenTLS`, and `Listener`. ```go title="Signature" func (h *Hooks) OnListen(handler ...OnListenHandler) @@ -134,23 +132,35 @@ func (h *Hooks) OnListen(handler ...OnListenHandler) ```go -app := fiber.New(fiber.Config{ - DisableStartupMessage: true, -}) - -app.Hooks().OnListen(func(listenData fiber.ListenData) error { - if fiber.IsChild() { - return nil - } - scheme := "http" - if data.TLS { - scheme = "https" - } - log.Println(scheme + "://" + listenData.Host + ":" + listenData.Port) - return nil -}) - -app.Listen(":5000") +package main + +import ( + "log" + "os" + + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/log" +) + +func main() { + app := fiber.New(fiber.Config{ + DisableStartupMessage: true, + }) + + app.Hooks().OnListen(func(listenData fiber.ListenData) error { + if fiber.IsChild() { + return nil + } + scheme := "http" + if listenData.TLS { + scheme = "https" + } + log.Println(scheme + "://" + listenData.Host + ":" + listenData.Port) + return nil + }) + + app.Listen(":5000") +} ``` @@ -158,7 +168,7 @@ app.Listen(":5000") ## OnFork -OnFork is a hook to execute user functions on Fork. +`OnFork` is a hook to execute user functions on fork. ```go title="Signature" func (h *Hooks) OnFork(handler ...OnForkHandler) @@ -166,7 +176,7 @@ func (h *Hooks) OnFork(handler ...OnForkHandler) ## OnShutdown -OnShutdown is a hook to execute user functions after Shutdown. +`OnShutdown` is a hook to execute user functions after shutdown. ```go title="Signature" func (h *Hooks) OnShutdown(handler ...OnShutdownHandler) @@ -174,10 +184,10 @@ func (h *Hooks) OnShutdown(handler ...OnShutdownHandler) ## OnMount -OnMount is a hook to execute user function after mounting process. The mount event is fired when sub-app is mounted on a parent app. The parent app is passed as a parameter. It works for app and group mounting. +`OnMount` is a hook to execute user functions after the mounting process. The mount event is fired when a sub-app is mounted on a parent app. The parent app is passed as a parameter. It works for both app and group mounting. ```go title="Signature" -func (h *Hooks) OnMount(handler ...OnMountHandler) +func (h *Hooks) OnMount(handler ...OnMountHandler) ``` @@ -193,24 +203,27 @@ import ( ) func main() { - app := New() + app := fiber.New() app.Get("/", testSimpleHandler).Name("x") - subApp := New() + subApp := fiber.New() subApp.Get("/test", testSimpleHandler) subApp.Hooks().OnMount(func(parent *fiber.App) error { - fmt.Print("Mount path of parent app: "+parent.MountPath()) - // ... - + fmt.Print("Mount path of parent app: " + parent.MountPath()) + // Additional custom logic... return nil }) app.Mount("/sub", subApp) } +func testSimpleHandler(c fiber.Ctx) error { + return c.SendString("Hello, Fiber!") +} + // Result: -// Mount path of parent app: +// Mount path of parent app: /sub ``` diff --git a/docs/api/log.md b/docs/api/log.md index 508be9a640..53f964a7dc 100644 --- a/docs/api/log.md +++ b/docs/api/log.md @@ -9,7 +9,7 @@ Logs serve as an essential tool for observing program behavior, diagnosing issue Fiber offers a default mechanism for logging to standard output. Additionally, it provides several global functions, including `log.Info`, `log.Errorf`, `log.Warnw`, among others, to facilitate comprehensive logging capabilities. -## Log levels +## Log Levels ```go const ( @@ -23,9 +23,9 @@ const ( ) ``` -## Custom log +## Custom Log -Fiber provides the `AllLogger` interface for adapting the various log libraries. +Fiber provides the `AllLogger` interface for adapting various log libraries. ```go type CommonLogger interface { @@ -41,13 +41,13 @@ type AllLogger interface { } ``` -## Print log +## Print Log -Note: The Fatal level method will terminate the program after printing the log message. Please use it with caution. +**Note:** The Fatal level method will terminate the program after printing the log message. Please use it with caution. ### Basic Logging -Logs of different levels can be directly printed. These will be entered into `messageKey`, with the default key being `msg`. +Logs of different levels can be directly printed. These logs will be entered into `messageKey`, with the default key being `msg`. ```go log.Info("Hello, World!") @@ -55,7 +55,7 @@ log.Debug("Are you OK?") log.Info("42 is the answer to life, the universe, and everything") log.Warn("We are under attack!") log.Error("Houston, we have a problem.") -log.Fatal("So Long, and Thanks for All the Fislog.") +log.Fatal("So Long, and Thanks for All the Fish.") log.Panic("The system is down.") ``` @@ -65,10 +65,10 @@ Logs of different levels can be formatted before printing. All such methods end ```go log.Debugf("Hello %s", "boy") -log.Infof("%d is the answer to life, the universe, and everything", 233) -log.Warnf("We are under attack %s!", "boss") +log.Infof("%d is the answer to life, the universe, and everything", 42) +log.Warnf("We are under attack, %s!", "boss") log.Errorf("%s, we have a problem.", "Master Shifu") -log.Fatalf("So Long, and Thanks for All the %s.", "banana") +log.Fatalf("So Long, and Thanks for All the %s.", "fish") ``` ### Key-Value Logging @@ -76,14 +76,14 @@ log.Fatalf("So Long, and Thanks for All the %s.", "banana") Print a message with key-value pairs. If the key and value are not paired correctly, the log will output `KEYVALS UNPAIRED`. ```go -log.Debugw("", "Hello", "boy") -log.Infow("", "number", 233) +log.Debugw("", "greeting", "Hello", "target", "boy") +log.Infow("", "number", 42) log.Warnw("", "job", "boss") log.Errorw("", "name", "Master Shifu") -log.Fatalw("", "fruit", "banana") +log.Fatalw("", "fruit", "fish") ``` -## Global log +## Global Log For projects that require a simple, global logging function to print messages at any time, Fiber provides a global log. @@ -96,7 +96,7 @@ log.Warn("warn") These global log functions allow you to log messages conveniently throughout your project. -The above example uses the default `log.DefaultLogger` for standard output. You can also find various pre-implemented adapters under the [contrib](https://github.com/gofiber/contrib) package such as `fiberzap` and `fiberzerolog`, or you can implement your own logger and set it as the global logger using `log.SetLogger`.This flexibility allows you to tailor the logging behavior to suit your project's needs. +The above example uses the default `log.DefaultLogger` for standard output. You can also find various pre-implemented adapters under the [contrib](https://github.com/gofiber/contrib) package such as `fiberzap` and `fiberzerolog`, or you can implement your own logger and set it as the global logger using `log.SetLogger`. This flexibility allows you to tailor the logging behavior to suit your project's needs. Here's an example using a custom logger: @@ -106,22 +106,25 @@ import ( fiberlog "github.com/gofiber/fiber/v3/log" ) -var _ log.AllLogger = (*customLogger)(nil) +var _ fiberlog.AllLogger = (*customLogger)(nil) type customLogger struct { stdlog *log.Logger } -// ... -// inject your custom logger -fiberlog.SetLogger(customLogger) +// Implement required methods for the AllLogger interface... + +// Inject your custom logger +fiberlog.SetLogger(&customLogger{ + stdlog: log.New(os.Stdout, "CUSTOM ", log.LstdFlags), +}) ``` ## Set Level `log.SetLevel` sets the minimum level of logs that will be output. The default log level is `LevelTrace`. -Note that this method is not **concurrent-safe**. +**Note:** This method is not **concurrent-safe**. ```go import "github.com/gofiber/fiber/v3/log" @@ -131,14 +134,14 @@ log.SetLevel(log.LevelInfo) Setting the log level allows you to control the verbosity of the logs, filtering out messages below the specified level. -## Set output +## Set Output `log.SetOutput` sets the output destination of the logger. By default, the logger outputs logs to the console. -### Writing logs to stderr +### Writing Logs to Stderr ```go -var logger AllLogger = &defaultLogger{ +var logger fiberlog.AllLogger = &defaultLogger{ stdlog: log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile|log.Lmicroseconds), depth: 4, } @@ -146,31 +149,34 @@ var logger AllLogger = &defaultLogger{ This allows you to customize where the logs are written, such as to a file, an external logging service, or any other desired destination. -### Writing logs to a file +### Writing Logs to a File -Set the output destination to the file, in this case `test.log`: +Set the output destination to a file, in this case `test.log`: ```go // Output to ./test.log file f, err := os.OpenFile("test.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) if err != nil { - return + log.Fatal("Failed to open log file:", err) } log.SetOutput(f) ``` -### Writing logs to both console and file +### Writing Logs to Both Console and File The following example will write the logs to both `test.log` and `stdout`: ```go // Output to ./test.log file -file, _ := os.OpenFile("test.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) +file, err := os.OpenFile("test.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) +if err != nil { + log.Fatal("Failed to open log file:", err) +} iw := io.MultiWriter(os.Stdout, file) log.SetOutput(iw) ``` -## Bind context +## Bind Context To bind a logger to a specific context, use the following method. This will return a `CommonLogger` instance that is bound to the specified context. diff --git a/docs/api/redirect.md b/docs/api/redirect.md index 79faa36d45..fa8bd1b3cc 100644 --- a/docs/api/redirect.md +++ b/docs/api/redirect.md @@ -6,14 +6,13 @@ sidebar_position: 5 toc_max_heading_level: 5 --- -Is used to redirect the ctx(request) to a different URL/Route. +The redirect methods are used to redirect the context (request) to a different URL or route. ## Redirect Methods ### To -Redirects to the URL derived from the specified path, with specified [status](#status), a positive integer that -corresponds to an HTTP status code. +Redirects to the URL derived from the specified path, with a specified [status](#status), a positive integer that corresponds to an HTTP status code. :::info If **not** specified, status defaults to **302 Found**. @@ -49,10 +48,10 @@ app.Get("/", func(c fiber.Ctx) error { ### Route -Redirects to the specific route along with the parameters and queries. +Redirects to a specific route along with the parameters and queries. :::info -If you want to send queries and params to route, you must use the [**RedirectConfig**](#redirectconfig) struct. +If you want to send queries and params to a route, you must use the [**RedirectConfig**](#redirectconfig) struct. ::: ```go title="Signature" @@ -71,7 +70,7 @@ app.Get("/", func(c fiber.Ctx) error { app.Get("/with-queries", func(c fiber.Ctx) error { // /user/fiber?data[0][name]=john&data[0][age]=10&test=doe - return c.Route("user", RedirectConfig{ + return c.Redirect().Route("user", fiber.RedirectConfig{ Params: fiber.Map{ "name": "fiber", }, @@ -90,8 +89,7 @@ app.Get("/user/:name", func(c fiber.Ctx) error { ### Back -Redirects back to refer URL. It redirects to fallback URL if refer header doesn't exists, with specified status, a -positive integer that corresponds to an HTTP status code. +Redirects back to the referer URL. It redirects to a fallback URL if the referer header doesn't exist, with a specified status, a positive integer that corresponds to an HTTP status code. :::info If **not** specified, status defaults to **302 Found**. @@ -105,6 +103,7 @@ func (r *Redirect) Back(fallback string) error app.Get("/", func(c fiber.Ctx) error { return c.SendString("Home page") }) + app.Get("/test", func(c fiber.Ctx) error { c.Set("Content-Type", "text/html") return c.SendString(`Back`) @@ -118,7 +117,7 @@ app.Get("/back", func(c fiber.Ctx) error { ## Controls :::info -Method are **chainable**. +Methods are **chainable**. ::: ### Status @@ -126,7 +125,7 @@ Method are **chainable**. Sets the HTTP status code for the redirect. :::info -Is used in conjunction with [**To**](#to), [**Route**](#route) and [**Back**](#back) methods. +It is used in conjunction with [**To**](#to), [**Route**](#route), and [**Back**](#back) methods. ::: ```go title="Signature" @@ -145,11 +144,11 @@ app.Get("/coffee", func(c fiber.Ctx) error { Sets the configuration for the redirect. :::info -Is used in conjunction with the [**Route**](#route) method. +It is used in conjunction with the [**Route**](#route) method. ::: -```go -// RedirectConfig A config to use with Redirect().Route() +```go title="Definition" +// RedirectConfig is a config to use with Redirect().Route() type RedirectConfig struct { Params fiber.Map // Route parameters Queries map[string]string // Query map @@ -158,7 +157,7 @@ type RedirectConfig struct { ### Flash Message -Similar to [Laravel](https://laravel.com/docs/11.x/redirects#redirecting-with-flashed-session-data) we can flash a message and retrieve it in the next request. +Similar to [Laravel](https://laravel.com/docs/11.x/redirects#redirecting-with-flashed-session-data), we can flash a message and retrieve it in the next request. #### Messages @@ -177,7 +176,7 @@ app.Get("/", func(c fiber.Ctx) error { #### Message -Get flash message by key. Check [With](#with) for more information. +Get a flash message by key. Check [With](#with) for more information. ```go title="Signature" func (r *Redirect) Message(key string) *Redirect @@ -241,10 +240,9 @@ app.Get("/", func(c fiber.Ctx) error { #### WithInput -You can send input data by using `WithInput()`. -They will be sent as a cookie. +You can send input data by using `WithInput()`. They will be sent as a cookie. -This method can send form, multipart form, query data to redirected route depending on the request content type. +This method can send form, multipart form, or query data to the redirected route depending on the request content type. ```go title="Signature" func (r *Redirect) WithInput() *Redirect From ba83a6e1ce4c13b4a31bd20469bc03f30bb6e3ec Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Mon, 18 Nov 2024 04:10:59 -0500 Subject: [PATCH 27/31] =?UTF-8?q?=F0=9F=93=9A=20Doc:=20Updates=20to=20Cont?= =?UTF-8?q?ext=20documentation=20(#3206)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update context documentation * Update docs/api/ctx.md --------- Co-authored-by: RW --- docs/api/ctx.md | 517 ++++++++++++++++++++++-------------------------- 1 file changed, 239 insertions(+), 278 deletions(-) diff --git a/docs/api/ctx.md b/docs/api/ctx.md index 56a2ea61b0..9a5bf3ccd4 100644 --- a/docs/api/ctx.md +++ b/docs/api/ctx.md @@ -2,7 +2,7 @@ id: ctx title: 🧠 Ctx description: >- - The Ctx interface represents the Context which hold the HTTP request and + The Ctx interface represents the Context which holds the HTTP request and response. It has methods for the request query string, parameters, body, HTTP headers, and so on. sidebar_position: 3 @@ -10,22 +10,20 @@ sidebar_position: 3 ## Accepts -Checks, if the specified **extensions** or **content** **types** are acceptable. +Checks if the specified **extensions** or **content** **types** are acceptable. :::info Based on the request’s [Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) HTTP header. ::: ```go title="Signature" -func (c Ctx) Accepts(offers ...string) string -func (c Ctx) AcceptsCharsets(offers ...string) string -func (c Ctx) AcceptsEncodings(offers ...string) string -func (c Ctx) AcceptsLanguages(offers ...string) string +func (c fiber.Ctx) Accepts(offers ...string) string +func (c fiber.Ctx) AcceptsCharsets(offers ...string) string +func (c fiber.Ctx) AcceptsEncodings(offers ...string) string +func (c fiber.Ctx) AcceptsLanguages(offers ...string) string ``` ```go title="Example" -// Accept: text/html, application/json; q=0.8, text/plain; q=0.5; charset="utf-8" - app.Get("/", func(c fiber.Ctx) error { c.Accepts("html") // "html" c.Accepts("text/html") // "text/html" @@ -44,7 +42,7 @@ app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error { c.Accepts("text/plain", "application/json") // "application/json", due to specificity c.Accepts("application/json", "text/html") // "text/html", due to first match - c.Accepts("image/png") // "", due to */* without q factor 0 is Not Acceptable + c.Accepts("image/png") // "", due to */* with q=0 is Not Acceptable // ... }) ``` @@ -61,7 +59,7 @@ app.Get("/", func(c fiber.Ctx) error { // An offer must contain all parameters present in the Accept type c.Accepts("application/json") // "" - // Parameter order and capitalization does not matter. Quotes on values are stripped. + // Parameter order and capitalization do not matter. Quotes on values are stripped. c.Accepts(`application/json;foo="bar";VERSION=1`) // "application/json;foo="bar";VERSION=1" }) ``` @@ -69,6 +67,7 @@ app.Get("/", func(c fiber.Ctx) error { ```go title="Example 4" // Accept: text/plain;format=flowed;q=0.9, text/plain // i.e., "I prefer text/plain;format=flowed less than other forms of text/plain" + app.Get("/", func(c fiber.Ctx) error { // Beware: the order in which offers are listed matters. // Although the client specified they prefer not to receive format=flowed, @@ -102,10 +101,10 @@ app.Get("/", func(c fiber.Ctx) error { ## App -Returns the [\*App](ctx.md) reference so you could easily access all application settings. +Returns the [\*App](app.md) reference so you can easily access all application settings. ```go title="Signature" -func (c Ctx) App() *App +func (c fiber.Ctx) App() *App ``` ```go title="Example" @@ -123,16 +122,16 @@ If the header is **not** already set, it creates the header with the specified v ::: ```go title="Signature" -func (c Ctx) Append(field string, values ...string) +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://localhost, http://google.com + // => Link: http://google.com, http://localhost c.Append("Link", "Test") - // => Link: http://localhost, http://google.com, Test + // => Link: http://google.com, http://localhost, Test // ... }) @@ -143,7 +142,7 @@ app.Get("/", func(c fiber.Ctx) error { 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 Ctx) Attachment(filename ...string) +func (c fiber.Ctx) Attachment(filename ...string) ``` ```go title="Example" @@ -170,7 +169,7 @@ If the header is **not** specified or there is **no** proper format, **text/plai ::: ```go title="Signature" -func (c Ctx) AutoFormat(body any) error +func (c fiber.Ctx) AutoFormat(body any) error ``` ```go title="Example" @@ -201,30 +200,30 @@ app.Get("/", func(c fiber.Ctx) error { ## BaseURL -Returns the base URL \(**protocol** + **host**\) as a `string`. +Returns the base URL (**protocol** + **host**) as a `string`. ```go title="Signature" -func (c Ctx) BaseURL() string +func (c fiber.Ctx) BaseURL() string ``` ```go title="Example" // GET https://example.com/page#chapter-1 app.Get("/", func(c fiber.Ctx) error { - c.BaseURL() // https://example.com + c.BaseURL() // "https://example.com" // ... }) ``` ## Bind -Bind is a method that support supports bindings for the request/response body, query parameters, URL parameters, cookies and much more. +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. +For detailed information, check the [Bind](./bind.md) documentation. ```go title="Signature" -func (c Ctx) Bind() *Bind +func (c fiber.Ctx) Bind() *Bind ``` ```go title="Example" @@ -240,7 +239,7 @@ app.Post("/", func(c fiber.Ctx) error { 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). ```go title="Signature" -func (c Ctx) Body() []byte +func (c fiber.Ctx) Body() []byte ``` ```go title="Example" @@ -253,10 +252,8 @@ app.Post("/", func(c fiber.Ctx) error { ``` :::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) - ::: ## BodyRaw @@ -264,7 +261,7 @@ Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more.. Returns the raw request **body**. ```go title="Signature" -func (c Ctx) BodyRaw() []byte +func (c fiber.Ctx) BodyRaw() []byte ``` ```go title="Example" @@ -277,18 +274,16 @@ app.Post("/", func(c fiber.Ctx) error { ``` :::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) - ::: ## ClearCookie -Expire a client cookie \(_or all cookies if left empty\)_ +Expires a client cookie (or all cookies if left empty). ```go title="Signature" -func (c Ctx) ClearCookie(key ...string) +func (c fiber.Ctx) ClearCookie(key ...string) ``` ```go title="Example" @@ -306,7 +301,7 @@ app.Get("/", func(c fiber.Ctx) error { ``` :::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. +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" @@ -337,11 +332,11 @@ app.Get("/delete", func(c fiber.Ctx) error { ## ClientHelloInfo -ClientHelloInfo contains information from a ClientHello message in order to guide application logic in the GetCertificate and GetConfigForClient callbacks. +`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. ```go title="Signature" -func (c Ctx) ClientHelloInfo() *tls.ClientHelloInfo +func (c fiber.Ctx) ClientHelloInfo() *tls.ClientHelloInfo ``` ```go title="Example" @@ -354,10 +349,10 @@ app.Get("/hello", func(c fiber.Ctx) error { ## Context -Context returns a context implementation that was set by user earlier or returns a non-nil, empty context, if it was not set earlier. +`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 Ctx) Context() context.Context +func (c fiber.Ctx) Context() context.Context ``` ```go title="Example" @@ -371,10 +366,10 @@ app.Get("/", func(c fiber.Ctx) error { ## Cookie -Set cookie +Sets a cookie. ```go title="Signature" -func (c Ctx) Cookie(cookie *Cookie) +func (c fiber.Ctx) Cookie(cookie *Cookie) ``` ```go @@ -408,9 +403,7 @@ app.Get("/", func(c fiber.Ctx) error { ``` :::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" @@ -419,7 +412,7 @@ app.Get("/", func(c fiber.Ctx) error { 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 embeded into another website + 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) @@ -429,10 +422,10 @@ app.Get("/", func(c fiber.Ctx) error { ## Cookies -Get cookie value by key, you could pass an optional default value that will be returned if the cookie key does not exist. +Gets a cookie value by key. You can pass an optional default value that will be returned if the cookie key does not exist. ```go title="Signature" -func (c Ctx) Cookies(key string, defaultValue ...string) string +func (c fiber.Ctx) Cookies(key string, defaultValue ...string) string ``` ```go title="Example" @@ -445,30 +438,27 @@ app.Get("/", func(c fiber.Ctx) error { ``` :::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) - ::: ## Download -Transfers the file from 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_\). +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 Ctx) Download(file string, filename ...string) error +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"); + return c.Download("./files/report-12345.pdf") // => Download report-12345.pdf - return c.Download("./files/report-12345.pdf", "report.pdf"); + return c.Download("./files/report-12345.pdf", "report.pdf") // => Download report.pdf }) ``` @@ -482,7 +472,7 @@ If the Accept header is **not** specified, the first handler will be used. ::: ```go title="Signature" -func (c Ctx) Format(handlers ...ResFmt) error +func (c fiber.Ctx) Format(handlers ...ResFmt) error ``` ```go title="Example" @@ -531,7 +521,7 @@ app.Get("/default", func(c fiber.Ctx) error { MultipartForm files can be retrieved by name, the **first** file from the given key is returned. ```go title="Signature" -func (c Ctx) FormFile(key string) (*multipart.FileHeader, error) +func (c fiber.Ctx) FormFile(key string) (*multipart.FileHeader, error) ``` ```go title="Example" @@ -546,10 +536,10 @@ app.Post("/", func(c fiber.Ctx) error { ## FormValue -Any form values can be retrieved by name, the **first** value from the given key is returned. +Form values can be retrieved by name, the **first** value for the given key is returned. ```go title="Signature" -func (c Ctx) FormValue(key string, defaultValue ...string) string +func (c fiber.Ctx) FormValue(key string, defaultValue ...string) string ``` ```go title="Example" @@ -578,7 +568,7 @@ When a client sends the Cache-Control: no-cache request header to indicate an en Read more on [https://expressjs.com/en/4x/api.html\#req.fresh](https://expressjs.com/en/4x/api.html#req.fresh) ```go title="Signature" -func (c Ctx) Fresh() bool +func (c fiber.Ctx) Fresh() bool ``` ## Get @@ -590,7 +580,7 @@ The match is **case-insensitive**. ::: ```go title="Signature" -func (c Ctx) Get(key string, defaultValue ...string) string +func (c fiber.Ctx) Get(key string, defaultValue ...string) string ``` ```go title="Example" @@ -603,10 +593,8 @@ app.Get("/", func(c fiber.Ctx) error { ``` :::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) - ::: ## GetReqHeaders @@ -614,14 +602,12 @@ Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more.. 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 Ctx) GetReqHeaders() map[string][]string +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 @@ -633,7 +619,7 @@ The match is **case-insensitive**. ::: ```go title="Signature" -func (c Ctx) GetRespHeader(key string, defaultValue ...string) string +func (c fiber.Ctx) GetRespHeader(key string, defaultValue ...string) string ``` ```go title="Example" @@ -646,10 +632,8 @@ app.Get("/", func(c fiber.Ctx) error { ``` :::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 @@ -657,14 +641,12 @@ Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more.. 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 Ctx) GetRespHeaders() map[string][]string +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 @@ -672,7 +654,7 @@ Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more.. Generates URLs to named routes, with parameters. URLs are relative, for example: "/user/1831" ```go title="Signature" -func (c Ctx) GetRouteURL(routeName string, params Map) (string, error) +func (c fiber.Ctx) GetRouteURL(routeName string, params Map) (string, error) ``` ```go title="Example" @@ -699,25 +681,23 @@ Returns the host derived from the [Host](https://developer.mozilla.org/en-US/doc In a network context, [`Host`](#host) refers to the combination of a hostname and potentially a port number used for connecting, while [`Hostname`](#hostname) refers specifically to the name assigned to a device on a network, excluding any port information. ```go title="Signature" -func (c Ctx) Host() string +func (c fiber.Ctx) Host() string ``` ```go title="Example" // GET http://google.com:8080/search app.Get("/", func(c fiber.Ctx) error { - c.Host() // "google.com:8080" - c.Hostname() // "google.com" + c.Host() // "google.com:8080" + c.Hostname() // "google.com" // ... }) ``` :::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) - ::: ## Hostname @@ -725,7 +705,7 @@ Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more.. Returns the hostname derived from the [Host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) HTTP header. ```go title="Signature" -func (c Ctx) Hostname() string +func (c fiber.Ctx) Hostname() string ``` ```go title="Example" @@ -739,10 +719,8 @@ app.Get("/", func(c fiber.Ctx) error { ``` :::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) - ::: ## IP @@ -750,7 +728,7 @@ Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more.. Returns the remote IP address of the request. ```go title="Signature" -func (c Ctx) IP() string +func (c fiber.Ctx) IP() string ``` ```go title="Example" @@ -761,7 +739,7 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` -When registering the proxy request header in the fiber app, the ip address of the header is returned [(Fiber configuration)](fiber.md#proxyheader) +When registering the proxy request header in the Fiber app, the IP address of the header is returned [(Fiber configuration)](fiber.md#proxyheader) ```go app := fiber.New(fiber.Config{ @@ -774,7 +752,7 @@ app := fiber.New(fiber.Config{ 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. ```go title="Signature" -func (c Ctx) IPs() []string +func (c fiber.Ctx) IPs() []string ``` ```go title="Example" @@ -800,7 +778,7 @@ If the request has **no** body, it returns **false**. ::: ```go title="Signature" -func (c Ctx) Is(extension string) bool +func (c fiber.Ctx) Is(extension string) bool ``` ```go title="Example" @@ -817,16 +795,15 @@ app.Get("/", func(c fiber.Ctx) error { ## IsFromLocal -Returns true if request came from localhost +Returns `true` if the request came from localhost. ```go title="Signature" -func (c Ctx) IsFromLocal() bool { +func (c fiber.Ctx) IsFromLocal() bool ``` ```go title="Example" - app.Get("/", func(c fiber.Ctx) error { - // If request came from localhost, return true else return false + // If request came from localhost, return true; else return false c.IsFromLocal() // ... @@ -835,34 +812,31 @@ app.Get("/", func(c fiber.Ctx) error { ## IsProxyTrusted -Checks trustworthiness of remote ip. -If [`TrustProxy`](fiber.md#trustproxy) false, it returns true -IsProxyTrusted can check remote ip by proxy ranges and ip map. +Checks the trustworthiness of the remote IP. +If [`TrustProxy`](fiber.md#trustproxy) is `false`, it returns `true`. +`IsProxyTrusted` can check the remote IP by proxy ranges and IP map. ```go title="Signature" -func (c Ctx) IsProxyTrusted() bool +func (c fiber.Ctx) IsProxyTrusted() bool ``` ```go title="Example" - app := fiber.New(fiber.Config{ // TrustProxy enables the trusted proxy check TrustProxy: true, // TrustProxyConfig allows for configuring trusted proxies. // Proxies is a list of trusted proxy IP ranges/addresses TrustProxyConfig: fiber.TrustProxyConfig{ - Proxies: []string{"0.8.0.0", "0.8.0.1"}, - } + Proxies: []string{"0.8.0.0", "1.1.1.1/30"}, // IP address or IP address range + }, }) - app.Get("/", func(c fiber.Ctx) error { - // If request came from trusted proxy, return true else return false + // If request came from trusted proxy, return true; else return false c.IsProxyTrusted() // ... }) - ``` ## JSON @@ -874,7 +848,7 @@ JSON also sets the content header to the `ctype` parameter. If no `ctype` is pas ::: ```go title="Signature" -func (c Ctx) JSON(data any, ctype ...string) error +func (c fiber.Ctx) JSON(data any, ctype ...string) error ``` ```go title="Example" @@ -892,20 +866,20 @@ app.Get("/json", func(c fiber.Ctx) error { return c.JSON(data) // => Content-Type: application/json - // => "{"Name": "Grame", "Age": 20}" + // => {"Name": "Grame", "Age": 20} return c.JSON(fiber.Map{ "name": "Grame", - "age": 20, + "age": 20, }) // => Content-Type: application/json - // => "{"name": "Grame", "age": 20}" + // => {"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.", + "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 @@ -921,32 +895,32 @@ app.Get("/json", func(c fiber.Ctx) error { ## 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. +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 Ctx) JSONP(data any, callback ...string) error +func (c fiber.Ctx) JSONP(data any, callback ...string) error ``` ```go title="Example" type SomeStruct struct { - name string - age uint8 + Name string + Age uint8 } app.Get("/", func(c fiber.Ctx) error { // Create data struct: data := SomeStruct{ - name: "Grame", - age: 20, + Name: "Grame", + Age: 20, } return c.JSONP(data) - // => callback({"name": "Grame", "age": 20}) + // => callback({"Name": "Grame", "Age": 20}) return c.JSONP(data, "customFunc") - // => customFunc({"name": "Grame", "age": 20}) + // => customFunc({"Name": "Grame", "Age": 20}) }) ``` @@ -955,7 +929,7 @@ app.Get("/", func(c fiber.Ctx) error { 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 Ctx) Links(link ...string) +func (c fiber.Ctx) Links(link ...string) ``` ```go title="Example" @@ -980,7 +954,7 @@ This is useful if you want to pass some **specific** data to the next middleware ::: ```go title="Signature" -func (c Ctx) Locals(key any, value ...any) any +func (c fiber.Ctx) Locals(key any, value ...any) any ``` ```go title="Example" @@ -1008,37 +982,36 @@ app.Get("/admin", func(c fiber.Ctx) error { }) ``` -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. +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 Ctx, key any, value ...V) V +func Locals[V any](c fiber.Ctx, key any, value ...V) V ``` ```go title="Example" -app.Use(func(c Ctx) error { +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 Ctx) error { - fiber.Locals[string](c, "john") // "doe" - fiber.Locals[int](c, "age") // 18 - fiber.Locals[bool](c, "isHuman") // true + +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. +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/ru/docs/Web/HTTP/Headers/Location) HTTP header to the specified path parameter. +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 Ctx) Location(path string) +func (c fiber.Ctx) Location(path string) ``` ```go title="Example" @@ -1053,19 +1026,19 @@ app.Post("/", func(c fiber.Ctx) error { ## Method -Returns a string corresponding to the HTTP method of the request: `GET`, `POST`, `PUT`, and so on. -Optionally, you could override the method by passing a string. +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. ```go title="Signature" -func (c Ctx) Method(override ...string) string +func (c fiber.Ctx) Method(override ...string) string ``` ```go title="Example" -app.Post("/", func(c fiber.Ctx) error { - c.Method() // "POST" +app.Post("/override", func(c fiber.Ctx) error { + c.Method() // "POST" c.Method("GET") - c.Method() // GET + c.Method() // "GET" // ... }) @@ -1073,10 +1046,10 @@ app.Post("/", func(c fiber.Ctx) error { ## MultipartForm -To access multipart form entries, you can parse the binary with `MultipartForm()`. This returns a `map[string][]string`, so given a key, the value will be a string slice. +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. ```go title="Signature" -func (c Ctx) MultipartForm() (*multipart.Form, error) +func (c fiber.Ctx) MultipartForm() (*multipart.Form, error) ``` ```go title="Example" @@ -1106,7 +1079,7 @@ app.Post("/", func(c fiber.Ctx) error { } } - return err + return nil }) ``` @@ -1115,7 +1088,7 @@ app.Post("/", func(c fiber.Ctx) error { 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 Ctx) Next() error +func (c fiber.Ctx) Next() error ``` ```go title="Example" @@ -1140,7 +1113,7 @@ app.Get("/", func(c fiber.Ctx) error { Returns the original request URL. ```go title="Signature" -func (c Ctx) OriginalURL() string +func (c fiber.Ctx) OriginalURL() string ``` ```go title="Example" @@ -1154,22 +1127,20 @@ app.Get("/", func(c fiber.Ctx) error { ``` :::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) - ::: ## Params -Method can be used to get the route parameters, you could pass an optional default value that will be returned if the param key does not exist. +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. :::info -Defaults to empty string \(`""`\), if the param **doesn't** exist. +Defaults to an empty string \(`""`\) if the param **doesn't** exist. ::: ```go title="Signature" -func (c Ctx) Params(key string, defaultValue ...string) string +func (c fiber.Ctx) Params(key string, defaultValue ...string) string ``` ```go title="Example" @@ -1189,7 +1160,7 @@ app.Get("/user/*", func(c fiber.Ctx) error { }) ``` -Unnamed route parameters\(\*, +\) can be fetched by the **character** and the **counter** in the route. +Unnamed route parameters \(\*, +\) can be fetched by the **character** and the **counter** in the route. ```go title="Example" // ROUTE: /v1/*/shop/* @@ -1202,61 +1173,58 @@ For reasons of **downward compatibility**, the first parameter segment for the p ```go title="Example" app.Get("/v1/*/shop/*", func(c fiber.Ctx) error { - c.Params("*") // outputs the values of the first wildcard segment + c.Params("*") // outputs the value of the first wildcard segment }) ``` :::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) - ::: In certain scenarios, it can be useful to have an alternative approach to handle different types of parameters, not -just strings. This can be achieved using a generic Query function known as `Params[V GenericType](c Ctx, key string, defaultValue ...V) V`. -This function is capable of parsing a query string and returning a value of a type that is assumed and specified by `V GenericType`. +just strings. This can be achieved using a generic `Params` function known as `Params[V GenericType](c fiber.Ctx, key string, defaultValue ...V) V`. +This function is capable of parsing a route parameter and returning a value of a type that is assumed and specified by `V GenericType`. ```go title="Signature" -func Params[v GenericType](c Ctx, key string, default value ...V) V +func Params[V GenericType](c fiber.Ctx, key string, defaultValue ...V) V ``` ```go title="Example" - -// Get http://example.com/user/114 +// GET http://example.com/user/114 app.Get("/user/:id", func(c fiber.Ctx) error{ fiber.Params[string](c, "id") // returns "114" as string. - fiber.Params[int](c, "id") // returns 114 as integer - fiber.Params[string](c, "number") // retunrs "" (default string type) - fiber.Params[int](c, "number") // returns 0 (default integer value type) + fiber.Params[int](c, "id") // returns 114 as integer + fiber.Params[string](c, "number") // returns "" (default string type) + fiber.Params[int](c, "number") // returns 0 (default integer value type) }) ``` -The generic Params function supports returning the following data types based on V GenericType: +The generic `Params` function supports returning the following data types based on `V GenericType`: -- Integer: int, int8, int16, int32, int64 -- Unsigned integer: uint, uint8, uint16, uint32, uint64 -- Floating-point numbers: float32, float64 -- Boolean: bool -- String: string -- Byte array: []byte +- Integer: `int`, `int8`, `int16`, `int32`, `int64` +- Unsigned integer: `uint`, `uint8`, `uint16`, `uint32`, `uint64` +- Floating-point numbers: `float32`, `float64` +- Boolean: `bool` +- String: `string` +- Byte array: `[]byte` ## Path -Contains the path part of the request URL. Optionally, you could 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). +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). ```go title="Signature" -func (c Ctx) Path(override ...string) string +func (c fiber.Ctx) Path(override ...string) string ``` ```go title="Example" // GET http://example.com/users?sort=desc app.Get("/users", func(c fiber.Ctx) error { - c.Path() // "/users" + c.Path() // "/users" c.Path("/john") - c.Path() // "/john" + c.Path() // "/john" // ... }) @@ -1267,11 +1235,12 @@ app.Get("/users", func(c fiber.Ctx) error { Returns the remote port of the request. ```go title="Signature" -func (c Ctx) Port() string +func (c fiber.Ctx) Port() string ``` ```go title="Example" // GET http://example.com:8080 + app.Get("/", func(c fiber.Ctx) error { c.Port() // "8080" @@ -1284,7 +1253,7 @@ app.Get("/", func(c fiber.Ctx) error { Contains the request protocol string: `http` or `https` for **TLS** requests. ```go title="Signature" -func (c Ctx) Protocol() string +func (c fiber.Ctx) Protocol() string ``` ```go title="Example" @@ -1299,10 +1268,10 @@ app.Get("/", func(c fiber.Ctx) error { ## Queries -Queries is a function that returns an object containing a property for each query string parameter in the route. +`Queries` is a function that returns an object containing a property for each query string parameter in the route. ```go title="Signature" -func (c Ctx) Queries() map[string]string +func (c fiber.Ctx) Queries() map[string]string ``` ```go title="Example" @@ -1310,9 +1279,9 @@ func (c Ctx) Queries() map[string]string app.Get("/", func(c fiber.Ctx) error { m := c.Queries() - m["name"] // "alex" - m["want_pizza"] // "false" - m["id"] // "" + m["name"] // "alex" + m["want_pizza"] // "false" + m["id"] // "" // ... }) ``` @@ -1323,7 +1292,7 @@ app.Get("/", func(c fiber.Ctx) error { app.Get("/", func (c fiber.Ctx) error { m := c.Queries() m["field1"] // "value2" - m["field2"] // value3 + m["field2"] // "value3" }) ``` @@ -1363,14 +1332,14 @@ app.Get("/", func(c fiber.Ctx) error { ## Query -This property is an object containing a property for each query string parameter in the route, you could pass an optional default value that will be returned if the query key does not exist. +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. :::info If there is **no** query string, it returns an **empty string**. ::: ```go title="Signature" -func (c Ctx) Query(key string, defaultValue ...string) string +func (c fiber.Ctx) Query(key string, defaultValue ...string) string ``` ```go title="Example" @@ -1386,24 +1355,20 @@ app.Get("/", func(c fiber.Ctx) error { ``` :::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) - ::: In certain scenarios, it can be useful to have an alternative approach to handle different types of query parameters, not -just strings. This can be achieved using a generic Query function known as `Query[V GenericType](c Ctx, key string, defaultValue ...V) V`. +just strings. This can be achieved using a generic `Query` function known as `Query[V GenericType](c fiber.Ctx, key string, defaultValue ...V) V`. This function is capable of parsing a query string and returning a value of a type that is assumed and specified by `V GenericType`. -Here is the signature for the generic Query function: +Here is the signature for the generic `Query` function: ```go title="Signature" -func Query[V GenericType](c Ctx, key string, defaultValue ...V) V +func Query[V GenericType](c fiber.Ctx, key string, defaultValue ...V) V ``` -Consider this example: - ```go title="Example" // GET http://example.com/?page=1&brand=nike&new=true @@ -1416,35 +1381,31 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` -In this case, `Query[V GenericType](c Ctx, key string, defaultValue ...V) V` can retrieve 'page' as an integer, 'brand' -as a string, and 'new' as a boolean. The function uses the appropriate parsing function for each specified type to ensure -the correct type is returned. This simplifies the retrieval process of different types of query parameters, making your -controller actions cleaner. - -The generic Query function supports returning the following data types based on V GenericType: +In this case, `Query[V GenericType](c Ctx, key string, defaultValue ...V) V` can retrieve `page` as an integer, `brand` as a string, and `new` as a boolean. The function uses the appropriate parsing function for each specified type to ensure the correct type is returned. This simplifies the retrieval process of different types of query parameters, making your controller actions cleaner. +The generic `Query` function supports returning the following data types based on `V GenericType`: -- Integer: int, int8, int16, int32, int64 -- Unsigned integer: uint, uint8, uint16, uint32, uint64 -- Floating-point numbers: float32, float64 -- Boolean: bool -- String: string -- Byte array: []byte +- Integer: `int`, `int8`, `int16`, `int32`, `int64` +- Unsigned integer: `uint`, `uint8`, `uint16`, `uint32`, `uint64` +- Floating-point numbers: `float32`, `float64` +- Boolean: `bool` +- String: `string` +- Byte array: `[]byte` ## Range -A struct containing the type and a slice of ranges will be returned. +Returns a struct containing the type and a slice of ranges. ```go title="Signature" -func (c Ctx) Range(size int) (Range, error) +func (c fiber.Ctx) Range(size int) (Range, error) ``` ```go title="Example" // Range: bytes=500-700, 700-900 app.Get("/", func(c fiber.Ctx) error { - b := c.Range(1000) - if b.Type == "bytes" { - for r := range r.Ranges { - fmt.Println(r) + r := c.Range(1000) + if r.Type == "bytes" { + for _, rng := range r.Ranges { + fmt.Println(rng) // [500, 700] } } @@ -1455,10 +1416,10 @@ app.Get("/", func(c fiber.Ctx) error { Returns the Redirect reference. -For detailed information check the [Redirect](./redirect.md) documentation. +For detailed information, check the [Redirect](./redirect.md) documentation. ```go title="Signature" -func (c Ctx) Redirect() *Redirect +func (c fiber.Ctx) Redirect() *Redirect ``` ```go title="Example" @@ -1473,18 +1434,18 @@ app.Get("/teapot", func(c fiber.Ctx) error { ## 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). +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 Ctx) Render(name string, bind Map, layouts ...string) error +func (c fiber.Ctx) Render(name string, bind Map, layouts ...string) error ``` ## Request -Request return the [\*fasthttp.Request](https://godoc.org/github.com/valyala/fasthttp#Request) pointer +Returns the [*fasthttp.Request](https://pkg.go.dev/github.com/valyala/fasthttp#Request) pointer. ```go title="Signature" -func (c Ctx) Request() *fasthttp.Request +func (c fiber.Ctx) Request() *fasthttp.Request ``` ```go title="Example" @@ -1496,10 +1457,10 @@ app.Get("/", func(c fiber.Ctx) error { ## RequestCtx -Returns [\*fasthttp.RequestCtx](https://godoc.org/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. +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 Ctx) RequestCtx() *fasthttp.RequestCtx +func (c fiber.Ctx) RequestCtx() *fasthttp.RequestCtx ``` :::info @@ -1508,10 +1469,10 @@ Please read the [Fasthttp Documentation](https://pkg.go.dev/github.com/valyala/f ## Response -Response return the [\*fasthttp.Response](https://godoc.org/github.com/valyala/fasthttp#Response) pointer +Returns the [\*fasthttp.Response](https://pkg.go.dev/github.com/valyala/fasthttp#Response) pointer. ```go title="Signature" -func (c Ctx) Response() *fasthttp.Response +func (c fiber.Ctx) Response() *fasthttp.Response ``` ```go title="Example" @@ -1524,20 +1485,20 @@ app.Get("/", func(c fiber.Ctx) error { ## Reset -Reset the context fields by given request when to use server handlers. +Resets the context fields by the given request when using server handlers. ```go title="Signature" -func (c Ctx) Reset(fctx *fasthttp.RequestCtx) +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. +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 Ctx) RestartRouting() error +func (c fiber.Ctx) RestartRouting() error ``` ```go title="Example" @@ -1556,13 +1517,12 @@ app.Get("/old", func(c fiber.Ctx) error { Returns the matched [Route](https://pkg.go.dev/github.com/gofiber/fiber?tab=doc#Route) struct. ```go title="Signature" -func (c Ctx) Route() *Route +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) @@ -1592,7 +1552,7 @@ func MyMiddleware() fiber.Handler { Method is used to save **any** multipart file to disk. ```go title="Signature" -func (c Ctx) SaveFile(fh *multipart.FileHeader, path string) error +func (c fiber.Ctx) SaveFile(fh *multipart.FileHeader, path string) error ``` ```go title="Example" @@ -1625,7 +1585,7 @@ app.Post("/", func(c fiber.Ctx) error { Method is used to save **any** multipart file to an external storage system. ```go title="Signature" -func (c Ctx) SaveFileToStorage(fileheader *multipart.FileHeader, path string, storage Storage) error +func (c fiber.Ctx) SaveFileToStorage(fileheader *multipart.FileHeader, path string, storage Storage) error ``` ```go title="Example" @@ -1657,18 +1617,19 @@ app.Post("/", func(c fiber.Ctx) error { ## Schema -Contains the request protocol string: http or https for TLS requests. +Contains the request protocol string: `http` or `https` for TLS requests. :::info -Please use [`Config.TrustProxy`](fiber.md#trustproxy) to prevent header spoofing, in case when your app is behind the proxy. +Please use [`Config.TrustProxy`](fiber.md#trustproxy) to prevent header spoofing if your app is behind a proxy. ::: ```go title="Signature" -func (c Ctx) Schema() string +func (c fiber.Ctx) Schema() string ``` ```go title="Example" // GET http://example.com + app.Get("/", func(c fiber.Ctx) error { c.Schema() // "http" @@ -1678,10 +1639,10 @@ app.Get("/", func(c fiber.Ctx) error { ## Secure -A boolean property that is `true` , if a **TLS** connection is established. +A boolean property that is `true` if a **TLS** connection is established. ```go title="Signature" -func (c Ctx) Secure() bool +func (c fiber.Ctx) Secure() bool ``` ```go title="Example" @@ -1694,7 +1655,7 @@ c.Protocol() == "https" Sets the HTTP response body. ```go title="Signature" -func (c Ctx) Send(body []byte) error +func (c fiber.Ctx) Send(body []byte) error ``` ```go title="Example" @@ -1710,8 +1671,8 @@ Use this if you **don't need** type assertion, recommended for **faster** perfor ::: ```go title="Signature" -func (c Ctx) SendString(body string) error -func (c Ctx) SendStream(stream io.Reader, size ...int) error +func (c fiber.Ctx) SendString(body string) error +func (c fiber.Ctx) SendStream(stream io.Reader, size ...int) error ``` ```go title="Example" @@ -1770,22 +1731,22 @@ type SendFile struct { ``` ```go title="Signature" title="Signature" -func (c Ctx) SendFile(file string, config ...SendFile) error +func (c fiber.Ctx) SendFile(file string, config ...SendFile) error ``` ```go title="Example" app.Get("/not-found", func(c fiber.Ctx) error { - return c.SendFile("./public/404.html"); + return c.SendFile("./public/404.html") // Disable compression return c.SendFile("./static/index.html", fiber.SendFile{ Compress: false, - }); + }) }) ``` :::info -If the file contains an url specific character you have to escape it before passing the file path into the `sendFile` function. +If the file contains a URL-specific character, you have to escape it before passing the file path into the `SendFile` function. ::: ```go title="Example" @@ -1795,7 +1756,7 @@ app.Get("/file-with-url-chars", func(c fiber.Ctx) error { ``` :::info -You can set `CacheDuration` config property to `-1` to disable caching. +You can set the `CacheDuration` config property to `-1` to disable caching. ::: ```go title="Example" @@ -1807,7 +1768,7 @@ app.Get("/file", func(c fiber.Ctx) error { ``` :::info -You can use multiple SendFile with different configurations in single route. Fiber creates different filesystem handler per config. +You can use multiple `SendFile` calls with different configurations in a single route. Fiber creates different filesystem handlers per config. ::: ```go title="Example" @@ -1835,19 +1796,19 @@ app.Get("/file", func(c fiber.Ctx) error { ``` :::info -For sending multiple files from embedded file system [this functionality](../middleware/static.md#serving-files-using-embedfs) can be used +For sending multiple files from an embedded file system, [this functionality](../middleware/static.md#serving-files-using-embedfs) can be used. ::: ## SendStatus -Sets the status code and the correct status message in the body, if the response body is **empty**. +Sets the status code and the correct status message in the body if the response body is **empty**. :::tip You can find all used status codes and messages [here](https://github.com/gofiber/fiber/blob/dffab20bcdf4f3597d2c74633a7705a517d2c8c2/utils.go#L183-L244). ::: ```go title="Signature" -func (c Ctx) SendStatus(status int) error +func (c fiber.Ctx) SendStatus(status int) error ``` ```go title="Example" @@ -1863,10 +1824,10 @@ app.Get("/not-found", func(c fiber.Ctx) error { ## SendStream -Sets response body to a stream of data and add optional body size. +Sets the response body to a stream of data and adds an optional body size. ```go title="Signature" -func (c Ctx) SendStream(stream io.Reader, size ...int) error +func (c fiber.Ctx) SendStream(stream io.Reader, size ...int) error ``` ```go title="Example" @@ -1881,7 +1842,7 @@ app.Get("/", func(c fiber.Ctx) error { Sets the response body to a string. ```go title="Signature" -func (c Ctx) SendString(body string) error +func (c fiber.Ctx) SendString(body string) error ``` ```go title="Example" @@ -1896,13 +1857,13 @@ app.Get("/", func(c fiber.Ctx) error { Sets the response’s HTTP header field to the specified `key`, `value`. ```go title="Signature" -func (c Ctx) Set(key string, val string) +func (c fiber.Ctx) Set(key string, val string) ``` ```go title="Example" app.Get("/", func(c fiber.Ctx) error { c.Set("Content-Type", "text/plain") - // => "Content-type: text/plain" + // => "Content-Type: text/plain" // ... }) @@ -1910,10 +1871,10 @@ app.Get("/", func(c fiber.Ctx) error { ## SetContext -Sets the user specified implementation for context.Context interface. +Sets the user-specified implementation for the `context.Context` interface. ```go title="Signature" -func (c Ctx) SetContext(ctx context.Context) +func (c fiber.Ctx) SetContext(ctx context.Context) ``` ```go title="Example" @@ -1928,10 +1889,10 @@ app.Get("/", func(c fiber.Ctx) error { ## Stale -[https://expressjs.com/en/4x/api.html\#req.stale](https://expressjs.com/en/4x/api.html#req.stale) +[https://expressjs.com/en/4x/api.html#req.stale](https://expressjs.com/en/4x/api.html#req.stale) ```go title="Signature" -func (c Ctx) Stale() bool +func (c fiber.Ctx) Stale() bool ``` ## Status @@ -1939,22 +1900,22 @@ func (c Ctx) Stale() bool Sets the HTTP status for the response. :::info -Method is a **chainable**. +This method is **chainable**. ::: ```go title="Signature" -func (c Ctx) Status(status int) Ctx +func (c fiber.Ctx) Status(status int) fiber.Ctx ``` ```go title="Example" app.Get("/fiber", func(c fiber.Ctx) error { c.Status(fiber.StatusOK) return nil -} +}) app.Get("/hello", func(c fiber.Ctx) error { return c.Status(fiber.StatusBadRequest).SendString("Bad Request") -} +}) app.Get("/world", func(c fiber.Ctx) error { return c.Status(fiber.StatusNotFound).SendFile("./public/gopher.png") @@ -1963,10 +1924,10 @@ app.Get("/world", func(c fiber.Ctx) error { ## String -Returns unique string representation of the ctx. +Returns a unique string representation of the context. ```go title="Signature" -func (c Ctx) String() string +func (c fiber.Ctx) String() string ``` ```go title="Example" @@ -1979,20 +1940,20 @@ app.Get("/", func(c fiber.Ctx) error { ## Subdomains -Returns a string slice of subdomains in the domain name of the request. +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. +The application property `subdomain offset`, which defaults to `2`, is used for determining the beginning of the subdomain segments. ```go title="Signature" -func (c Ctx) Subdomains(offset ...int) []string +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"] + c.Subdomains() // ["ferrets", "tobi"] + c.Subdomains(1) // ["tobi"] // ... }) @@ -2003,11 +1964,11 @@ app.Get("/", func(c fiber.Ctx) error { 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**. :::info -Method is a **chainable**. +This method is **chainable**. ::: ```go title="Signature" -func (c Ctx) Type(ext string, charset ...string) Ctx +func (c fiber.Ctx) Type(ext string, charset ...string) fiber.Ctx ``` ```go title="Example" @@ -2024,14 +1985,14 @@ app.Get("/", func(c fiber.Ctx) error { ## 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 leaves it listed in the current location. +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. :::info Multiple fields are **allowed**. ::: ```go title="Signature" -func (c Ctx) Vary(fields ...string) +func (c fiber.Ctx) Vary(fields ...string) ``` ```go title="Example" @@ -2051,11 +2012,11 @@ app.Get("/", func(c fiber.Ctx) error { ## ViewBind -Add vars to default view var map binding to template engine. -Variables are read by the Render method and may be overwritten. +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 Ctx) ViewBind(vars Map) error +func (c fiber.Ctx) ViewBind(vars Map) error ``` ```go title="Example" @@ -2063,35 +2024,36 @@ 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 Title variable + return c.Render("xxx.tmpl", fiber.Map{}) // Render will use the Title variable }) ``` ## Write -Write adopts the Writer interface +Adopts the `Writer` interface. ```go title="Signature" -func (c Ctx) Write(p []byte) (n int, err error) +func (c fiber.Ctx) Write(p []byte) (n int, err error) ``` ```go title="Example" app.Get("/", func(c fiber.Ctx) error { c.Write([]byte("Hello, World!")) // => "Hello, World!" - fmt.Fprintf(c, "%s\n", "Hello, World!") // "Hello, World!Hello, World!" + fmt.Fprintf(c, "%s\n", "Hello, World!") // => "Hello, World!" }) ``` ## Writef -Writef adopts the string with variables +Writes a formatted string using a format specifier. ```go title="Signature" -func (c Ctx) Writef(f string, a ...any) (n int, err error) +func (c fiber.Ctx) Writef(format string, a ...any) (n int, err error) ``` ```go title="Example" @@ -2099,32 +2061,31 @@ app.Get("/", func(c fiber.Ctx) error { world := "World!" c.Writef("Hello, %s", world) // => "Hello, World!" - fmt.Fprintf(c, "%s\n", "Hello, World!") // "Hello, World!Hello, World!" + fmt.Fprintf(c, "%s\n", "Hello, World!") // => "Hello, World!" }) ``` ## WriteString -WriteString adopts the string +Writes a string to the response body. ```go title="Signature" -func (c Ctx) WriteString(s string) (n int, err error) +func (c fiber.Ctx) WriteString(s string) (n int, err error) ``` ```go title="Example" app.Get("/", func(c fiber.Ctx) error { - c.WriteString("Hello, World!") // => "Hello, World!" - - fmt.Fprintf(c, "%s\n", "Hello, World!") // "Hello, World!Hello, World!" + return c.WriteString("Hello, World!") + // => "Hello, World!" }) ``` ## 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/)\). +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 Ctx) XHR() bool +func (c fiber.Ctx) XHR() bool ``` ```go title="Example" @@ -2142,11 +2103,11 @@ app.Get("/", func(c fiber.Ctx) error { Converts any **interface** or **string** to XML using the standard `encoding/xml` package. :::info -XML also sets the content header to **application/xml**. +XML also sets the content header to `application/xml`. ::: ```go title="Signature" -func (c Ctx) XML(data any) error +func (c fiber.Ctx) XML(data any) error ``` ```go title="Example" @@ -2166,7 +2127,7 @@ app.Get("/", func(c fiber.Ctx) error { return c.XML(data) // // Grame - // 20 + // 20 // }) ``` From 7ac82d7b092c6ab35f26994bceeeaed730e7b492 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:41:49 +0000 Subject: [PATCH 28/31] build(deps): bump codecov/codecov-action from 5.0.0 to 5.0.2 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.0.0 to 5.0.2. - [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.0.0...v5.0.2) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-patch ... 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 e86be2956b..e196693223 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.0.0 + uses: codecov/codecov-action@v5.0.2 with: token: ${{ secrets.CODECOV_TOKEN }} file: ./coverage.txt From 329759db51d5c5d96353c9ad78bd038dbc71c17c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Nov 2024 12:49:06 +0000 Subject: [PATCH 29/31] build(deps): bump codecov/codecov-action from 5.0.2 to 5.0.4 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.0.2 to 5.0.4. - [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.0.2...v5.0.4) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-patch ... 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 e196693223..79391c9d58 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.0.2 + uses: codecov/codecov-action@v5.0.4 with: token: ${{ secrets.CODECOV_TOKEN }} file: ./coverage.txt From 5697b9d45e69a7c1987fc76047a48bc952df9f1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Nov 2024 12:31:46 +0000 Subject: [PATCH 30/31] build(deps): bump codecov/codecov-action from 5.0.4 to 5.0.7 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.0.4 to 5.0.7. - [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.0.4...v5.0.7) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-patch ... 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 79391c9d58..78a832c20d 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.0.4 + uses: codecov/codecov-action@v5.0.7 with: token: ${{ secrets.CODECOV_TOKEN }} file: ./coverage.txt From f8b490f89ee5a58108f7902ff5b02e5006f2802d Mon Sep 17 00:00:00 2001 From: Giovanni Rivera Date: Thu, 21 Nov 2024 23:43:38 -0800 Subject: [PATCH 31/31] =?UTF-8?q?=F0=9F=94=A5=20Feature:=20Add=20TestConfi?= =?UTF-8?q?g=20to=20app.Test()=20for=20configurable=20testing=20(#3161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔥 Feature: Add thread-safe reading from a closed testConn * 🔥 Feature: Add TestConfig to app.Test() This commit is summarized as: - Add the struct `TestConfig` as a parameter for `app.Test()` instead of `timeout` - Add documentation of `TestConfig` to docs/api/app.md and in-line - Modify middleware to use `TestConfig` instead of the previous implementation Fixes #3149 * 📚 Doc: Add more details about TestConfig in docs * 🩹 Fix: Correct testConn tests - Fixes Test_Utils_TestConn_Closed_Write - Fixes missing regular write test * 🎨 Style: Respect linter in Add App Test Config * 🎨 Styles: Update app.go to respect linter * ♻️ Refactor: Rename TestConfig's ErrOnTimeout to FailOnTimeout - Rename TestConfig.ErrOnTimeout to TestConfig.FailOnTimeout - Update documentation to use changed name - Also fix stale documentation about passing Timeout as a single argument * 🩹 Fix: Fix typo in TestConfig struct comment in app.go * ♻️ Refactor: Change app.Test() fail on timeouterror to os.ErrDeadlineExceeded * ♻️ Refactor:Update middleware that use the same TestConfig to use a global variable * 🩹 Fix: Update error from FailOnTimeout to os.ErrDeadlineExceeded in tests * 🩹 Fix: Remove errors import from middlware/proxy/proxy_test.go * 📚 Doc: Add `app.Test()` config changes to docs/whats_new.md * ♻ Refactor: Change app.Test() and all uses to accept 0 as no timeout instead of -1 * 📚 Doc: Add TestConfig option details to docs/whats_new.md * 🎨 Styles: Update docs/whats_new.md to respect markdown-lint * 🎨 Styles: Update docs/whats_new.md to use consistent style for TestConfig options description --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> --- app.go | 46 ++++++++++++--- app_test.go | 37 ++++++++++-- ctx_test.go | 5 +- docs/api/app.md | 29 +++++++++- docs/whats_new.md | 65 +++++++++++++++++++++- helpers.go | 33 +++++++++-- helpers_test.go | 42 ++++++++++++++ middleware/compress/compress_test.go | 17 ++++-- middleware/idempotency/idempotency_test.go | 5 +- middleware/keyauth/keyauth_test.go | 12 ++-- middleware/logger/logger_test.go | 10 +++- middleware/pprof/pprof_test.go | 9 ++- middleware/proxy/proxy_test.go | 59 +++++++++++++++----- middleware/static/static_test.go | 15 +++-- 14 files changed, 328 insertions(+), 56 deletions(-) diff --git a/app.go b/app.go index e1e607e6fa..c4a26e7772 100644 --- a/app.go +++ b/app.go @@ -14,9 +14,11 @@ import ( "encoding/xml" "errors" "fmt" + "io" "net" "net/http" "net/http/httputil" + "os" "reflect" "strconv" "strings" @@ -901,13 +903,33 @@ func (app *App) Hooks() *Hooks { return app.hooks } +// TestConfig is a struct holding Test settings +type TestConfig struct { + // Timeout defines the maximum duration a + // test can run before timing out. + // Default: time.Second + Timeout time.Duration + + // FailOnTimeout specifies whether the test + // should return a timeout error if the HTTP response + // exceeds the Timeout duration. + // Default: true + FailOnTimeout bool +} + // Test is used for internal debugging by passing a *http.Request. -// Timeout is optional and defaults to 1s, -1 will disable it completely. -func (app *App) Test(req *http.Request, timeout ...time.Duration) (*http.Response, error) { - // Set timeout - to := 1 * time.Second - if len(timeout) > 0 { - to = timeout[0] +// Config is optional and defaults to a 1s error on timeout, +// 0 timeout will disable it completely. +func (app *App) Test(req *http.Request, config ...TestConfig) (*http.Response, error) { + // Default config + cfg := TestConfig{ + Timeout: time.Second, + FailOnTimeout: true, + } + + // Override config if provided + if len(config) > 0 { + cfg = config[0] } // Add Content-Length if not provided with body @@ -946,12 +968,15 @@ func (app *App) Test(req *http.Request, timeout ...time.Duration) (*http.Respons }() // Wait for callback - if to >= 0 { + if cfg.Timeout > 0 { // With timeout select { case err = <-channel: - case <-time.After(to): - return nil, fmt.Errorf("test: timeout error after %s", to) + case <-time.After(cfg.Timeout): + conn.Close() //nolint:errcheck, revive // It is fine to ignore the error here + if cfg.FailOnTimeout { + return nil, os.ErrDeadlineExceeded + } } } else { // Without timeout @@ -969,6 +994,9 @@ func (app *App) Test(req *http.Request, timeout ...time.Duration) (*http.Respons // Convert raw http response to *http.Response res, err := http.ReadResponse(buffer, req) if err != nil { + if errors.Is(err, io.ErrUnexpectedEOF) { + return nil, errors.New("test: got empty response") + } return nil, fmt.Errorf("failed to read response: %w", err) } diff --git a/app_test.go b/app_test.go index 9699c85bce..a99796a2c1 100644 --- a/app_test.go +++ b/app_test.go @@ -16,6 +16,7 @@ import ( "net" "net/http" "net/http/httptest" + "os" "reflect" "regexp" "runtime" @@ -1124,7 +1125,9 @@ func Test_Test_Timeout(t *testing.T) { app.Get("/", testEmptyHandler) - resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil), -1) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil), TestConfig{ + Timeout: 0, + }) require.NoError(t, err, "app.Test(req)") require.Equal(t, 200, resp.StatusCode, "Status code") @@ -1133,7 +1136,10 @@ func Test_Test_Timeout(t *testing.T) { return nil }) - _, err = app.Test(httptest.NewRequest(MethodGet, "/timeout", nil), 20*time.Millisecond) + _, err = app.Test(httptest.NewRequest(MethodGet, "/timeout", nil), TestConfig{ + Timeout: 20 * time.Millisecond, + FailOnTimeout: true, + }) require.Error(t, err, "app.Test(req)") } @@ -1432,7 +1438,9 @@ func Test_App_Test_no_timeout_infinitely(t *testing.T) { }) req := httptest.NewRequest(MethodGet, "/", nil) - _, err = app.Test(req, -1) + _, err = app.Test(req, TestConfig{ + Timeout: 0, + }) }() tk := time.NewTimer(5 * time.Second) @@ -1460,8 +1468,27 @@ func Test_App_Test_timeout(t *testing.T) { return nil }) - _, err := app.Test(httptest.NewRequest(MethodGet, "/", nil), 100*time.Millisecond) - require.Equal(t, errors.New("test: timeout error after 100ms"), err) + _, err := app.Test(httptest.NewRequest(MethodGet, "/", nil), TestConfig{ + Timeout: 100 * time.Millisecond, + FailOnTimeout: true, + }) + require.Equal(t, os.ErrDeadlineExceeded, err) +} + +func Test_App_Test_timeout_empty_response(t *testing.T) { + t.Parallel() + + app := New() + app.Get("/", func(_ Ctx) error { + time.Sleep(1 * time.Second) + return nil + }) + + _, err := app.Test(httptest.NewRequest(MethodGet, "/", nil), TestConfig{ + Timeout: 100 * time.Millisecond, + FailOnTimeout: false, + }) + require.Equal(t, errors.New("test: got empty response"), err) } func Test_App_SetTLSHandler(t *testing.T) { diff --git a/ctx_test.go b/ctx_test.go index af56197c58..6254f5dfdf 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -3243,7 +3243,10 @@ func Test_Static_Compress(t *testing.T) { req := httptest.NewRequest(MethodGet, "/file", nil) req.Header.Set("Accept-Encoding", algo) - resp, err := app.Test(req, 10*time.Second) + resp, err := app.Test(req, TestConfig{ + Timeout: 10 * time.Second, + FailOnTimeout: true, + }) require.NoError(t, err, "app.Test(req)") require.Equal(t, 200, resp.StatusCode, "Status code") diff --git a/docs/api/app.md b/docs/api/app.md index 8c7f8979bc..6582159c0f 100644 --- a/docs/api/app.md +++ b/docs/api/app.md @@ -641,10 +641,10 @@ func (app *App) SetTLSHandler(tlsHandler *TLSHandler) ## Test -Testing your application is done with the `Test` method. Use this method for creating `_test.go` files or when you need to debug your routing logic. The default timeout is `1s`; to disable a timeout altogether, pass `-1` as the second argument. +Testing your application is done with the `Test` method. Use this method for creating `_test.go` files or when you need to debug your routing logic. The default timeout is `1s`; to disable a timeout altogether, pass a `TestConfig` struct with `Timeout: 0`. ```go title="Signature" -func (app *App) Test(req *http.Request, msTimeout ...int) (*http.Response, error) +func (app *App) Test(req *http.Request, config ...TestConfig) (*http.Response, error) ``` ```go title="Example" @@ -685,6 +685,31 @@ func main() { } ``` +If not provided, TestConfig is set to the following defaults: + +```go title="Default TestConfig" +config := fiber.TestConfig{ + Timeout: time.Second(), + FailOnTimeout: true, +} +``` + +:::caution + +This is **not** the same as supplying an empty `TestConfig{}` to +`app.Test(), but rather be the equivalent of supplying: + +```go title="Empty TestConfig" +cfg := fiber.TestConfig{ + Timeout: 0, + FailOnTimeout: false, +} +``` + +This would make a Test that has no timeout. + +::: + ## Hooks `Hooks` is a method to return the [hooks](./hooks.md) property. diff --git a/docs/whats_new.md b/docs/whats_new.md index 0a47dca491..8677a0080f 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -75,7 +75,8 @@ We have made several changes to the Fiber app, including: ### Methods changes -- Test -> timeout changed to 1 second +- Test -> Replaced timeout with a config parameter + - -1 represents no timeout -> 0 represents no timeout - Listen -> has a config parameter - Listener -> has a config parameter @@ -184,6 +185,68 @@ To enable the routing changes above we had to slightly adjust the signature of t + Add(methods []string, path string, handler Handler, middleware ...Handler) Router ``` +### Test Config + +The `app.Test()` method now allows users to customize their test configurations: + +
+Example + +```go +// Create a test app with a handler to test +app := fiber.New() +app.Get("/", func(c fiber.Ctx) { + return c.SendString("hello world") +}) + +// Define the HTTP request and custom TestConfig to test the handler +req := httptest.NewRequest(MethodGet, "/", nil) +testConfig := fiber.TestConfig{ + Timeout: 0, + FailOnTimeout: false, +} + +// Test the handler using the request and testConfig +resp, err := app.Test(req, testConfig) +``` + +
+ +To provide configurable testing capabilities, we had to change +the signature of the `Test` method. + +```diff +- Test(req *http.Request, timeout ...time.Duration) (*http.Response, error) ++ Test(req *http.Request, config ...fiber.TestConfig) (*http.Response, error) +``` + +The `TestConfig` struct provides the following configuration options: + +- `Timeout`: The duration to wait before timing out the test. Use 0 for no timeout. +- `FailOnTimeout`: Controls the behavior when a timeout occurs: + - When true, the test will return an `os.ErrDeadlineExceeded` if the test exceeds the `Timeout` duration. + - When false, the test will return the partial response received before timing out. + +If a custom `TestConfig` isn't provided, then the following will be used: + +```go +testConfig := fiber.TestConfig{ + Timeout: time.Second, + FailOnTimeout: true, +} +``` + +**Note:** Using this default is **NOT** the same as providing an empty `TestConfig` as an argument to `app.Test()`. + +An empty `TestConfig` is the equivalent of: + +```go +testConfig := fiber.TestConfig{ + Timeout: 0, + FailOnTimeout: false, +} +``` + --- ## 🧠 Context diff --git a/helpers.go b/helpers.go index 90621b1c42..526074032a 100644 --- a/helpers.go +++ b/helpers.go @@ -613,13 +613,36 @@ func isNoCache(cacheControl string) bool { } type testConn struct { - r bytes.Buffer - w bytes.Buffer + r bytes.Buffer + w bytes.Buffer + isClosed bool + sync.Mutex } -func (c *testConn) Read(b []byte) (int, error) { return c.r.Read(b) } //nolint:wrapcheck // This must not be wrapped -func (c *testConn) Write(b []byte) (int, error) { return c.w.Write(b) } //nolint:wrapcheck // This must not be wrapped -func (*testConn) Close() error { return nil } +func (c *testConn) Read(b []byte) (int, error) { + c.Lock() + defer c.Unlock() + + return c.r.Read(b) //nolint:wrapcheck // This must not be wrapped +} + +func (c *testConn) Write(b []byte) (int, error) { + c.Lock() + defer c.Unlock() + + if c.isClosed { + return 0, errors.New("testConn is closed") + } + return c.w.Write(b) //nolint:wrapcheck // This must not be wrapped +} + +func (c *testConn) Close() error { + c.Lock() + defer c.Unlock() + + c.isClosed = true + return nil +} func (*testConn) LocalAddr() net.Addr { return &net.TCPAddr{Port: 0, Zone: "", IP: net.IPv4zero} } func (*testConn) RemoteAddr() net.Addr { return &net.TCPAddr{Port: 0, Zone: "", IP: net.IPv4zero} } diff --git a/helpers_test.go b/helpers_test.go index ddee434098..28a5df2ae7 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -514,6 +514,48 @@ func Test_Utils_TestConn_Deadline(t *testing.T) { require.NoError(t, conn.SetWriteDeadline(time.Time{})) } +func Test_Utils_TestConn_ReadWrite(t *testing.T) { + t.Parallel() + conn := &testConn{} + + // Verify read of request + _, err := conn.r.Write([]byte("Request")) + require.NoError(t, err) + + req := make([]byte, 7) + _, err = conn.Read(req) + require.NoError(t, err) + require.Equal(t, []byte("Request"), req) + + // Verify write of response + _, err = conn.Write([]byte("Response")) + require.NoError(t, err) + + res := make([]byte, 8) + _, err = conn.w.Read(res) + require.NoError(t, err) + require.Equal(t, []byte("Response"), res) +} + +func Test_Utils_TestConn_Closed_Write(t *testing.T) { + t.Parallel() + conn := &testConn{} + + // Verify write of response + _, err := conn.Write([]byte("Response 1\n")) + require.NoError(t, err) + + // Close early, write should fail + conn.Close() //nolint:errcheck, revive // It is fine to ignore the error here + _, err = conn.Write([]byte("Response 2\n")) + require.Error(t, err) + + res := make([]byte, 11) + _, err = conn.w.Read(res) + require.NoError(t, err) + require.Equal(t, []byte("Response 1\n"), res) +} + func Test_Utils_IsNoCache(t *testing.T) { t.Parallel() testCases := []struct { diff --git a/middleware/compress/compress_test.go b/middleware/compress/compress_test.go index f258ba4460..da482fe77b 100644 --- a/middleware/compress/compress_test.go +++ b/middleware/compress/compress_test.go @@ -16,6 +16,11 @@ import ( var filedata []byte +var testConfig = fiber.TestConfig{ + Timeout: 10 * time.Second, + FailOnTimeout: true, +} + func init() { dat, err := os.ReadFile("../../.github/README.md") if err != nil { @@ -39,7 +44,7 @@ func Test_Compress_Gzip(t *testing.T) { req := httptest.NewRequest(fiber.MethodGet, "/", nil) req.Header.Set("Accept-Encoding", "gzip") - resp, err := app.Test(req, 10*time.Second) + resp, err := app.Test(req, testConfig) require.NoError(t, err, "app.Test(req)") require.Equal(t, 200, resp.StatusCode, "Status code") require.Equal(t, "gzip", resp.Header.Get(fiber.HeaderContentEncoding)) @@ -72,7 +77,7 @@ func Test_Compress_Different_Level(t *testing.T) { req := httptest.NewRequest(fiber.MethodGet, "/", nil) req.Header.Set("Accept-Encoding", algo) - resp, err := app.Test(req, 10*time.Second) + resp, err := app.Test(req, testConfig) require.NoError(t, err, "app.Test(req)") require.Equal(t, 200, resp.StatusCode, "Status code") require.Equal(t, algo, resp.Header.Get(fiber.HeaderContentEncoding)) @@ -99,7 +104,7 @@ func Test_Compress_Deflate(t *testing.T) { req := httptest.NewRequest(fiber.MethodGet, "/", nil) req.Header.Set("Accept-Encoding", "deflate") - resp, err := app.Test(req, 10*time.Second) + resp, err := app.Test(req, testConfig) require.NoError(t, err, "app.Test(req)") require.Equal(t, 200, resp.StatusCode, "Status code") require.Equal(t, "deflate", resp.Header.Get(fiber.HeaderContentEncoding)) @@ -123,7 +128,7 @@ func Test_Compress_Brotli(t *testing.T) { req := httptest.NewRequest(fiber.MethodGet, "/", nil) req.Header.Set("Accept-Encoding", "br") - resp, err := app.Test(req, 10*time.Second) + resp, err := app.Test(req, testConfig) require.NoError(t, err, "app.Test(req)") require.Equal(t, 200, resp.StatusCode, "Status code") require.Equal(t, "br", resp.Header.Get(fiber.HeaderContentEncoding)) @@ -147,7 +152,7 @@ func Test_Compress_Zstd(t *testing.T) { req := httptest.NewRequest(fiber.MethodGet, "/", nil) req.Header.Set("Accept-Encoding", "zstd") - resp, err := app.Test(req, 10*time.Second) + resp, err := app.Test(req, testConfig) require.NoError(t, err, "app.Test(req)") require.Equal(t, 200, resp.StatusCode, "Status code") require.Equal(t, "zstd", resp.Header.Get(fiber.HeaderContentEncoding)) @@ -171,7 +176,7 @@ func Test_Compress_Disabled(t *testing.T) { req := httptest.NewRequest(fiber.MethodGet, "/", nil) req.Header.Set("Accept-Encoding", "br") - resp, err := app.Test(req, 10*time.Second) + resp, err := app.Test(req, testConfig) require.NoError(t, err, "app.Test(req)") require.Equal(t, 200, resp.StatusCode, "Status code") require.Equal(t, "", resp.Header.Get(fiber.HeaderContentEncoding)) diff --git a/middleware/idempotency/idempotency_test.go b/middleware/idempotency/idempotency_test.go index 91394ca26a..56b10d9c03 100644 --- a/middleware/idempotency/idempotency_test.go +++ b/middleware/idempotency/idempotency_test.go @@ -82,7 +82,10 @@ func Test_Idempotency(t *testing.T) { if idempotencyKey != "" { req.Header.Set("X-Idempotency-Key", idempotencyKey) } - resp, err := app.Test(req, 15*time.Second) + resp, err := app.Test(req, fiber.TestConfig{ + Timeout: 15 * time.Second, + FailOnTimeout: true, + }) require.NoError(t, err) body, err := io.ReadAll(resp.Body) require.NoError(t, err) diff --git a/middleware/keyauth/keyauth_test.go b/middleware/keyauth/keyauth_test.go index 9da675fe8f..72c9d3c1b4 100644 --- a/middleware/keyauth/keyauth_test.go +++ b/middleware/keyauth/keyauth_test.go @@ -14,6 +14,10 @@ import ( const CorrectKey = "specials: !$%,.#\"!?~`<>@$^*(){}[]|/\\123" +var testConfig = fiber.TestConfig{ + Timeout: 0, +} + func Test_AuthSources(t *testing.T) { // define test cases testSources := []string{"header", "cookie", "query", "param", "form"} @@ -104,7 +108,7 @@ func Test_AuthSources(t *testing.T) { req.URL.Path = r } - res, err := app.Test(req, -1) + res, err := app.Test(req, testConfig) require.NoError(t, err, test.description) @@ -209,7 +213,7 @@ func TestMultipleKeyLookup(t *testing.T) { q.Add("key", CorrectKey) req.URL.RawQuery = q.Encode() - res, err := app.Test(req, -1) + res, err := app.Test(req, testConfig) require.NoError(t, err) @@ -226,7 +230,7 @@ func TestMultipleKeyLookup(t *testing.T) { // construct a second request without proper key req, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/foo", nil) require.NoError(t, err) - res, err = app.Test(req, -1) + res, err = app.Test(req, testConfig) require.NoError(t, err) errBody, err := io.ReadAll(res.Body) require.NoError(t, err) @@ -350,7 +354,7 @@ func Test_MultipleKeyAuth(t *testing.T) { req.Header.Set("key", test.APIKey) } - res, err := app.Test(req, -1) + res, err := app.Test(req, testConfig) require.NoError(t, err, test.description) diff --git a/middleware/logger/logger_test.go b/middleware/logger/logger_test.go index b69668f336..946db05d80 100644 --- a/middleware/logger/logger_test.go +++ b/middleware/logger/logger_test.go @@ -300,7 +300,10 @@ func Test_Logger_WithLatency(t *testing.T) { sleepDuration = 1 * tu.div // Create a new HTTP request to the test route - resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), 3*time.Second) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), fiber.TestConfig{ + Timeout: 3 * time.Second, + FailOnTimeout: true, + }) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) @@ -341,7 +344,10 @@ func Test_Logger_WithLatency_DefaultFormat(t *testing.T) { sleepDuration = 1 * tu.div // Create a new HTTP request to the test route - resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), 2*time.Second) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), fiber.TestConfig{ + Timeout: 2 * time.Second, + FailOnTimeout: true, + }) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) diff --git a/middleware/pprof/pprof_test.go b/middleware/pprof/pprof_test.go index 7a279d488d..874ca104a9 100644 --- a/middleware/pprof/pprof_test.go +++ b/middleware/pprof/pprof_test.go @@ -11,6 +11,11 @@ import ( "github.com/stretchr/testify/require" ) +var testConfig = fiber.TestConfig{ + Timeout: 5 * time.Second, + FailOnTimeout: true, +} + func Test_Non_Pprof_Path(t *testing.T) { app := fiber.New() @@ -105,7 +110,7 @@ func Test_Pprof_Subs(t *testing.T) { if sub == "profile" { target += "?seconds=1" } - resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, target, nil), 5*time.Second) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, target, nil), testConfig) require.NoError(t, err) require.Equal(t, 200, resp.StatusCode) }) @@ -132,7 +137,7 @@ func Test_Pprof_Subs_WithPrefix(t *testing.T) { if sub == "profile" { target += "?seconds=1" } - resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, target, nil), 5*time.Second) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, target, nil), testConfig) require.NoError(t, err) require.Equal(t, 200, resp.StatusCode) }) diff --git a/middleware/proxy/proxy_test.go b/middleware/proxy/proxy_test.go index 713488a52b..a810825135 100644 --- a/middleware/proxy/proxy_test.go +++ b/middleware/proxy/proxy_test.go @@ -2,10 +2,10 @@ package proxy import ( "crypto/tls" - "errors" "io" "net" "net/http/httptest" + "os" "strings" "testing" "time" @@ -110,7 +110,10 @@ func Test_Proxy(t *testing.T) { return c.SendStatus(fiber.StatusTeapot) }) - resp, err := target.Test(httptest.NewRequest(fiber.MethodGet, "/", nil), 2*time.Second) + resp, err := target.Test(httptest.NewRequest(fiber.MethodGet, "/", nil), fiber.TestConfig{ + Timeout: 2 * time.Second, + FailOnTimeout: true, + }) require.NoError(t, err) require.Equal(t, fiber.StatusTeapot, resp.StatusCode) @@ -172,7 +175,10 @@ func Test_Proxy_Balancer_IPv6_Upstream(t *testing.T) { return c.SendStatus(fiber.StatusTeapot) }) - resp, err := target.Test(httptest.NewRequest(fiber.MethodGet, "/", nil), 2*time.Second) + resp, err := target.Test(httptest.NewRequest(fiber.MethodGet, "/", nil), fiber.TestConfig{ + Timeout: 2 * time.Second, + FailOnTimeout: true, + }) require.NoError(t, err) require.Equal(t, fiber.StatusTeapot, resp.StatusCode) @@ -195,7 +201,10 @@ func Test_Proxy_Balancer_IPv6_Upstream_With_DialDualStack(t *testing.T) { return c.SendStatus(fiber.StatusTeapot) }) - resp, err := target.Test(httptest.NewRequest(fiber.MethodGet, "/", nil), 2*time.Second) + resp, err := target.Test(httptest.NewRequest(fiber.MethodGet, "/", nil), fiber.TestConfig{ + Timeout: 2 * time.Second, + FailOnTimeout: true, + }) require.NoError(t, err) require.Equal(t, fiber.StatusTeapot, resp.StatusCode) @@ -221,7 +230,10 @@ func Test_Proxy_Balancer_IPv4_Upstream_With_DialDualStack(t *testing.T) { return c.SendStatus(fiber.StatusTeapot) }) - resp, err := target.Test(httptest.NewRequest(fiber.MethodGet, "/", nil), 2*time.Second) + resp, err := target.Test(httptest.NewRequest(fiber.MethodGet, "/", nil), fiber.TestConfig{ + Timeout: 2 * time.Second, + FailOnTimeout: true, + }) require.NoError(t, err) require.Equal(t, fiber.StatusTeapot, resp.StatusCode) @@ -399,7 +411,10 @@ func Test_Proxy_Timeout_Slow_Server(t *testing.T) { Timeout: 600 * time.Millisecond, })) - resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil), 2*time.Second) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil), fiber.TestConfig{ + Timeout: 2 * time.Second, + FailOnTimeout: true, + }) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) @@ -423,7 +438,10 @@ func Test_Proxy_With_Timeout(t *testing.T) { Timeout: 100 * time.Millisecond, })) - resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil), 2*time.Second) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil), fiber.TestConfig{ + Timeout: 2 * time.Second, + FailOnTimeout: true, + }) require.NoError(t, err) require.Equal(t, fiber.StatusInternalServerError, resp.StatusCode) @@ -521,7 +539,10 @@ func Test_Proxy_DoRedirects_RestoreOriginalURL(t *testing.T) { return DoRedirects(c, "http://google.com", 1) }) - resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), 2*time.Second) + resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), fiber.TestConfig{ + Timeout: 2 * time.Second, + FailOnTimeout: true, + }) require.NoError(t, err1) _, err := io.ReadAll(resp.Body) require.NoError(t, err) @@ -559,7 +580,10 @@ func Test_Proxy_DoTimeout_RestoreOriginalURL(t *testing.T) { return DoTimeout(c, "http://"+addr, time.Second) }) - resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), 2*time.Second) + 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) @@ -580,7 +604,10 @@ func Test_Proxy_DoTimeout_Timeout(t *testing.T) { return DoTimeout(c, "http://"+addr, time.Second) }) - resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), 2*time.Second) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), fiber.TestConfig{ + Timeout: 2 * time.Second, + FailOnTimeout: true, + }) require.NoError(t, err) body, err := io.ReadAll(resp.Body) require.NoError(t, err) @@ -624,8 +651,11 @@ func Test_Proxy_DoDeadline_PastDeadline(t *testing.T) { return DoDeadline(c, "http://"+addr, time.Now().Add(2*time.Second)) }) - _, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), 1*time.Second) - require.Equal(t, errors.New("test: timeout error after 1s"), err1) + _, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), fiber.TestConfig{ + Timeout: 1 * time.Second, + FailOnTimeout: true, + }) + require.Equal(t, os.ErrDeadlineExceeded, err1) } // go test -race -run Test_Proxy_Do_HTTP_Prefix_URL @@ -717,7 +747,10 @@ func Test_ProxyBalancer_Custom_Client(t *testing.T) { return c.SendStatus(fiber.StatusTeapot) }) - resp, err := target.Test(httptest.NewRequest(fiber.MethodGet, "/", nil), 2*time.Second) + resp, err := target.Test(httptest.NewRequest(fiber.MethodGet, "/", nil), fiber.TestConfig{ + Timeout: 2 * time.Second, + FailOnTimeout: true, + }) require.NoError(t, err) require.Equal(t, fiber.StatusTeapot, resp.StatusCode) diff --git a/middleware/static/static_test.go b/middleware/static/static_test.go index 4d736b0c43..4e1d7a96d8 100644 --- a/middleware/static/static_test.go +++ b/middleware/static/static_test.go @@ -15,6 +15,11 @@ import ( "github.com/stretchr/testify/require" ) +var testConfig = fiber.TestConfig{ + Timeout: 10 * time.Second, + FailOnTimeout: true, +} + // go test -run Test_Static_Index_Default func Test_Static_Index_Default(t *testing.T) { t.Parallel() @@ -738,7 +743,7 @@ func Test_Static_Compress(t *testing.T) { // request non-compressable file (less than 200 bytes), Content Lengh will remain the same req := httptest.NewRequest(fiber.MethodGet, "/css/style.css", nil) req.Header.Set("Accept-Encoding", algo) - resp, err := app.Test(req, 10*time.Second) + resp, err := app.Test(req, testConfig) require.NoError(t, err, "app.Test(req)") require.Equal(t, 200, resp.StatusCode, "Status code") @@ -748,7 +753,7 @@ func Test_Static_Compress(t *testing.T) { // request compressable file, ContentLenght will change req = httptest.NewRequest(fiber.MethodGet, "/index.html", nil) req.Header.Set("Accept-Encoding", algo) - resp, err = app.Test(req, 10*time.Second) + resp, err = app.Test(req, testConfig) require.NoError(t, err, "app.Test(req)") require.Equal(t, 200, resp.StatusCode, "Status code") @@ -769,7 +774,7 @@ func Test_Static_Compress_WithoutEncoding(t *testing.T) { // request compressable file without encoding req := httptest.NewRequest(fiber.MethodGet, "/index.html", nil) - resp, err := app.Test(req, 10*time.Second) + resp, err := app.Test(req, testConfig) require.NoError(t, err, "app.Test(req)") require.Equal(t, 200, resp.StatusCode, "Status code") @@ -792,7 +797,7 @@ func Test_Static_Compress_WithoutEncoding(t *testing.T) { req = httptest.NewRequest(fiber.MethodGet, "/"+fileName, nil) req.Header.Set("Accept-Encoding", algo) - resp, err = app.Test(req, 10*time.Second) + resp, err = app.Test(req, testConfig) require.NoError(t, err, "app.Test(req)") require.Equal(t, 200, resp.StatusCode, "Status code") @@ -833,7 +838,7 @@ func Test_Static_Compress_WithFileSuffixes(t *testing.T) { req := httptest.NewRequest(fiber.MethodGet, "/"+fileName, nil) req.Header.Set("Accept-Encoding", algo) - resp, err := app.Test(req, 10*time.Second) + resp, err := app.Test(req, testConfig) require.NoError(t, err, "app.Test(req)") require.Equal(t, 200, resp.StatusCode, "Status code")