diff --git a/.github/README.md b/.github/README.md index df3d8f5de8..cb7b19b376 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.23` 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 @@ -124,7 +124,7 @@ We **listen** to our users in [issues](https://github.com/gofiber/fiber/issues), ## ⚠️ Limitations -- Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber v3 has been tested with Go versions 1.22 and 1.23. +- Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber v3 has been tested with Go version 1.23. - Fiber is not compatible with net/http interfaces. This means you will not be able to use projects like gqlgen, go-swagger, or any others which are part of the net/http ecosystem. ## πŸ‘€ Examples @@ -708,7 +708,7 @@ List of externally hosted middleware modules and maintained by the [Fiber team]( | :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------- | | [contrib](https://github.com/gofiber/contrib) | Third-party middlewares | | [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | -| [template](https://github.com/gofiber/template) | This package contains 9 template engines that can be used with Fiber `v3`. Go version 1.22 or higher is required. | +| [template](https://github.com/gofiber/template) | This package contains 9 template engines that can be used with Fiber `v3`. Go version 1.23 or higher is required. | ## πŸ•ΆοΈ Awesome List diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 075d1db6cc..a086223dd9 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -31,10 +31,40 @@ jobs: uses: actions/setup-go@v5 with: # NOTE: Keep this in sync with the version from go.mod - go-version: "1.22.x" + go-version: "1.23.x" - name: Run Benchmark run: set -o pipefail; go test ./... -benchmem -run=^$ -bench . | tee output.txt + ### hack because of the problem with duplicated benchmark names - https://github.com/benchmark-action/github-action-benchmark/issues/264 + - name: Extract Module Name + id: extract-module + run: | + MODULE_NAME=$(awk '/^module / {print $2}' go.mod) + echo "MODULE_NAME=$MODULE_NAME" >> $GITHUB_ENV + + - name: Identify Duplicate Benchmark Names + run: | + awk '/^Benchmark/ {print $1}' output.txt | sort | uniq -d > duplicate_benchmarks.txt + + - name: Add Normalized Package Prefix to Duplicate Benchmark Names + run: | + awk -v MODULE_NAME="$MODULE_NAME" ' + FNR==NR {duplicates[$1]; next} + /^pkg: / { package=$2 } + /^Benchmark/ { + if ($1 in duplicates) { + sub("^" MODULE_NAME "/?", "", package) + gsub("/", "_", package) + print $1 "_" package substr($0, length($1) + 1) + } else { + print $0 + } + next + } + { print } + ' duplicate_benchmarks.txt output.txt > output_prefixed.txt + mv output_prefixed.txt output.txt + ### end # NOTE: Benchmarks could change with different CPU types - name: Get GitHub Runner System Information diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 2dc54bc585..bd2c0bce4c 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -30,7 +30,7 @@ jobs: - uses: actions/setup-go@v5 with: # NOTE: Keep this in sync with the version from go.mod - go-version: "1.22.x" + go-version: "1.23.x" cache: false - name: golangci-lint diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 78a832c20d..8ae8cd36d9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: unit: strategy: matrix: - go-version: [1.22.x, 1.23.x] + go-version: [1.23.x] platform: [ubuntu-latest, windows-latest, macos-latest, macos-13] runs-on: ${{ matrix.platform }} steps: diff --git a/app.go b/app.go index c4a26e7772..7f9193a1a1 100644 --- a/app.go +++ b/app.go @@ -25,9 +25,9 @@ import ( "sync" "time" + "github.com/fxamacker/cbor/v2" "github.com/gofiber/fiber/v3/log" "github.com/gofiber/utils/v2" - "github.com/valyala/fasthttp" ) @@ -320,6 +320,20 @@ type Config struct { //nolint:govet // Aligning the struct fields is not necessa // Default: json.Unmarshal JSONDecoder utils.JSONUnmarshal `json:"-"` + // When set by an external client of Fiber it will use the provided implementation of a + // CBORMarshal + // + // Allowing for flexibility in using another cbor library for encoding + // Default: cbor.Marshal + CBOREncoder utils.CBORMarshal `json:"-"` + + // When set by an external client of Fiber it will use the provided implementation of a + // CBORUnmarshal + // + // Allowing for flexibility in using another cbor library for decoding + // Default: cbor.Unmarshal + CBORDecoder utils.CBORUnmarshal `json:"-"` + // XMLEncoder set by an external client of Fiber it will use the provided implementation of a // XMLMarshal // @@ -537,6 +551,12 @@ func New(config ...Config) *App { if app.config.JSONDecoder == nil { app.config.JSONDecoder = json.Unmarshal } + if app.config.CBOREncoder == nil { + app.config.CBOREncoder = cbor.Marshal + } + if app.config.CBORDecoder == nil { + app.config.CBORDecoder = cbor.Unmarshal + } if app.config.XMLEncoder == nil { app.config.XMLEncoder = xml.Marshal } diff --git a/bind.go b/bind.go index ad76594265..5af83743a0 100644 --- a/bind.go +++ b/bind.go @@ -23,7 +23,7 @@ type Bind struct { dontHandleErrs bool } -// If you want to handle binder errors manually, you can use `WithoutAutoHandling`. +// WithoutAutoHandling If you want to handle binder errors manually, you can use `WithoutAutoHandling`. // It's default behavior of binder. func (b *Bind) WithoutAutoHandling() *Bind { b.dontHandleErrs = true @@ -31,7 +31,7 @@ func (b *Bind) WithoutAutoHandling() *Bind { return b } -// If you want to handle binder errors automatically, you can use `WithAutoHandling`. +// WithAutoHandling If you want to handle binder errors automatically, you can use `WithAutoHandling`. // If there's an error, it will return the error and set HTTP status to `400 Bad Request`. // You must still return on error explicitly func (b *Bind) WithAutoHandling() *Bind { @@ -121,6 +121,14 @@ func (b *Bind) JSON(out any) error { return b.validateStruct(out) } +// CBOR binds the body string into the struct. +func (b *Bind) CBOR(out any) error { + if err := b.returnErr(binder.CBORBinder.Bind(b.ctx.Body(), b.ctx.App().Config().CBORDecoder, out)); err != nil { + return err + } + return b.validateStruct(out) +} + // XML binds the body string into the struct. func (b *Bind) XML(out any) error { if err := b.returnErr(binder.XMLBinder.Bind(b.ctx.Body(), out)); err != nil { @@ -183,6 +191,8 @@ func (b *Bind) Body(out any) error { return b.JSON(out) case MIMETextXML, MIMEApplicationXML: return b.XML(out) + case MIMEApplicationCBOR: + return b.CBOR(out) case MIMEApplicationForm: return b.Form(out) case MIMEMultipartForm: diff --git a/bind_test.go b/bind_test.go index a75722b3b4..55d2dd75e9 100644 --- a/bind_test.go +++ b/bind_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "github.com/fxamacker/cbor/v2" "github.com/gofiber/fiber/v3/binder" "github.com/stretchr/testify/require" "github.com/valyala/fasthttp" @@ -157,7 +158,7 @@ func Test_Bind_Query_WithSetParserDecoder(t *testing.T) { } nonRFCTime := binder.ParserType{ - Customtype: NonRFCTime{}, + CustomType: NonRFCTime{}, Converter: nonRFCConverter, } @@ -411,7 +412,7 @@ func Test_Bind_Header_WithSetParserDecoder(t *testing.T) { } nonRFCTime := binder.ParserType{ - Customtype: NonRFCTime{}, + CustomType: NonRFCTime{}, Converter: nonRFCConverter, } @@ -922,11 +923,11 @@ func Test_Bind_Body(t *testing.T) { testCompressedBody(t, compressedBody, "zstd") }) - testDecodeParser := func(t *testing.T, contentType, body string) { + testDecodeParser := func(t *testing.T, contentType string, body []byte) { t.Helper() c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Request().Header.SetContentType(contentType) - c.Request().SetBody([]byte(body)) + c.Request().SetBody(body) c.Request().Header.SetContentLength(len(body)) d := new(Demo) require.NoError(t, c.Bind().Body(d)) @@ -934,19 +935,36 @@ func Test_Bind_Body(t *testing.T) { } t.Run("JSON", func(t *testing.T) { - testDecodeParser(t, MIMEApplicationJSON, `{"name":"john"}`) + testDecodeParser(t, MIMEApplicationJSON, []byte(`{"name":"john"}`)) + }) + t.Run("CBOR", func(t *testing.T) { + enc, err := cbor.Marshal(&Demo{Name: "john"}) + if err != nil { + t.Error(err) + } + testDecodeParser(t, MIMEApplicationCBOR, enc) + + // Test invalid CBOR data + t.Run("Invalid", func(t *testing.T) { + invalidData := []byte{0xFF, 0xFF} // Invalid CBOR data + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().Header.SetContentType(MIMEApplicationCBOR) + c.Request().SetBody(invalidData) + d := new(Demo) + require.Error(t, c.Bind().Body(d)) + }) }) t.Run("XML", func(t *testing.T) { - testDecodeParser(t, MIMEApplicationXML, `john`) + testDecodeParser(t, MIMEApplicationXML, []byte(`john`)) }) t.Run("Form", func(t *testing.T) { - testDecodeParser(t, MIMEApplicationForm, "name=john") + testDecodeParser(t, MIMEApplicationForm, []byte("name=john")) }) t.Run("MultipartForm", func(t *testing.T) { - testDecodeParser(t, MIMEMultipartForm+`;boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--") + testDecodeParser(t, MIMEMultipartForm+`;boundary="b"`, []byte("--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--")) }) testDecodeParserError := func(t *testing.T, contentType, body string) { @@ -1009,7 +1027,7 @@ func Test_Bind_Body_WithSetParserDecoder(t *testing.T) { } customTime := binder.ParserType{ - Customtype: CustomTime{}, + CustomType: CustomTime{}, Converter: timeConverter, } @@ -1100,6 +1118,35 @@ func Benchmark_Bind_Body_XML(b *testing.B) { require.Equal(b, "john", d.Name) } +// go test -v -run=^$ -bench=Benchmark_Bind_Body_CBOR -benchmem -count=4 +func Benchmark_Bind_Body_CBOR(b *testing.B) { + var err error + + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + + type Demo struct { + Name string `json:"name"` + } + body, err := cbor.Marshal(&Demo{Name: "john"}) + if err != nil { + b.Error(err) + } + c.Request().SetBody(body) + c.Request().Header.SetContentType(MIMEApplicationCBOR) + c.Request().Header.SetContentLength(len(body)) + d := new(Demo) + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + err = c.Bind().Body(d) + } + require.NoError(b, err) + require.Equal(b, "john", d.Name) +} + // go test -v -run=^$ -bench=Benchmark_Bind_Body_Form -benchmem -count=4 func Benchmark_Bind_Body_Form(b *testing.B) { var err error @@ -1404,7 +1451,7 @@ func Test_Bind_Cookie_WithSetParserDecoder(t *testing.T) { } nonRFCTime := binder.ParserType{ - Customtype: NonRFCTime{}, + CustomType: NonRFCTime{}, Converter: nonRFCConverter, } @@ -1720,8 +1767,12 @@ func Test_Bind_RepeatParserWithSameStruct(t *testing.T) { require.Equal(t, "body_param", r.BodyParam) } + cb, err := cbor.Marshal(&Request{BodyParam: "body_param"}) + require.NoError(t, err, "Failed to marshal CBOR data") + testDecodeParser(MIMEApplicationJSON, `{"body_param":"body_param"}`) testDecodeParser(MIMEApplicationXML, `body_param`) + testDecodeParser(MIMEApplicationCBOR, string(cb)) testDecodeParser(MIMEApplicationForm, "body_param=body_param") testDecodeParser(MIMEMultipartForm+`;boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"body_param\"\r\n\r\nbody_param\r\n--b--") } diff --git a/binder/README.md b/binder/README.md index 6794a3b93b..4d75fd1b90 100644 --- a/binder/README.md +++ b/binder/README.md @@ -22,6 +22,7 @@ Fiber provides several default binders out of the box: - [Cookie](cookie.go) - [JSON](json.go) - [XML](xml.go) +- [CBOR](cbor.go) ## Guides diff --git a/binder/binder.go b/binder/binder.go index fb7ac12dab..bb3fc2b394 100644 --- a/binder/binder.go +++ b/binder/binder.go @@ -20,4 +20,5 @@ var ( URIBinder = &uriBinding{} XMLBinder = &xmlBinding{} JSONBinder = &jsonBinding{} + CBORBinder = &cborBinding{} ) diff --git a/binder/cbor.go b/binder/cbor.go new file mode 100644 index 0000000000..6f47893531 --- /dev/null +++ b/binder/cbor.go @@ -0,0 +1,18 @@ +package binder + +import ( + "github.com/gofiber/utils/v2" +) + +// cborBinding is the CBOR binder for CBOR request body. +type cborBinding struct{} + +// Name returns the binding name. +func (*cborBinding) Name() string { + return "cbor" +} + +// Bind parses the request body as CBOR and returns the result. +func (*cborBinding) Bind(body []byte, cborDecoder utils.CBORUnmarshal, out any) error { + return cborDecoder(body, out) +} diff --git a/binder/cookie.go b/binder/cookie.go index 0f5c650c33..62271c8e38 100644 --- a/binder/cookie.go +++ b/binder/cookie.go @@ -8,12 +8,15 @@ import ( "github.com/valyala/fasthttp" ) +// cookieBinding is the cookie binder for cookie request body. type cookieBinding struct{} +// Name returns the binding name. func (*cookieBinding) Name() string { return "cookie" } +// Bind parses the request cookie and returns the result. func (b *cookieBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error { data := make(map[string][]string) var err error diff --git a/binder/form.go b/binder/form.go index f45407fe93..e0f1acd302 100644 --- a/binder/form.go +++ b/binder/form.go @@ -8,12 +8,15 @@ import ( "github.com/valyala/fasthttp" ) +// formBinding is the form binder for form request body. type formBinding struct{} +// Name returns the binding name. func (*formBinding) Name() string { return "form" } +// Bind parses the request body and returns the result. func (b *formBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error { data := make(map[string][]string) var err error @@ -47,6 +50,7 @@ func (b *formBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error { return parse(b.Name(), out, data) } +// BindMultipart parses the request body and returns the result. func (b *formBinding) BindMultipart(reqCtx *fasthttp.RequestCtx, out any) error { data, err := reqCtx.MultipartForm() if err != nil { diff --git a/binder/header.go b/binder/header.go index 196163694d..258a0b2229 100644 --- a/binder/header.go +++ b/binder/header.go @@ -8,12 +8,15 @@ import ( "github.com/valyala/fasthttp" ) +// headerBinding is the header binder for header request body. type headerBinding struct{} +// Name returns the binding name. func (*headerBinding) Name() string { return "header" } +// Bind parses the request header and returns the result. func (b *headerBinding) Bind(req *fasthttp.Request, out any) error { data := make(map[string][]string) req.Header.VisitAll(func(key, val []byte) { diff --git a/binder/json.go b/binder/json.go index 6c0d80c89b..7889aee8a2 100644 --- a/binder/json.go +++ b/binder/json.go @@ -4,12 +4,15 @@ import ( "github.com/gofiber/utils/v2" ) +// jsonBinding is the JSON binder for JSON request body. type jsonBinding struct{} +// Name returns the binding name. func (*jsonBinding) Name() string { return "json" } +// Bind parses the request body as JSON and returns the result. func (*jsonBinding) Bind(body []byte, jsonDecoder utils.JSONUnmarshal, out any) error { return jsonDecoder(body, out) } diff --git a/binder/mapping.go b/binder/mapping.go index ea67ace200..055345fe26 100644 --- a/binder/mapping.go +++ b/binder/mapping.go @@ -24,7 +24,7 @@ type ParserConfig struct { // ParserType require two element, type and converter for register. // Use ParserType with BodyParser for parsing custom type in form data. type ParserType struct { - Customtype any + CustomType any Converter func(string) reflect.Value } @@ -51,7 +51,7 @@ func decoderBuilder(parserConfig ParserConfig) any { decoder.SetAliasTag(parserConfig.SetAliasTag) } for _, v := range parserConfig.ParserType { - decoder.RegisterConverter(reflect.ValueOf(v.Customtype).Interface(), v.Converter) + decoder.RegisterConverter(reflect.ValueOf(v.CustomType).Interface(), v.Converter) } decoder.ZeroEmpty(parserConfig.ZeroEmpty) return decoder diff --git a/binder/query.go b/binder/query.go index 25b69f5bc3..8f029d30c4 100644 --- a/binder/query.go +++ b/binder/query.go @@ -8,12 +8,15 @@ import ( "github.com/valyala/fasthttp" ) +// queryBinding is the query binder for query request body. type queryBinding struct{} +// Name returns the binding name. func (*queryBinding) Name() string { return "query" } +// Bind parses the request query and returns the result. func (b *queryBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error { data := make(map[string][]string) var err error diff --git a/binder/resp_header.go b/binder/resp_header.go index 0455185ba1..ef14255315 100644 --- a/binder/resp_header.go +++ b/binder/resp_header.go @@ -8,12 +8,15 @@ import ( "github.com/valyala/fasthttp" ) +// respHeaderBinding is the respHeader binder for response header. type respHeaderBinding struct{} +// Name returns the binding name. func (*respHeaderBinding) Name() string { return "respHeader" } +// Bind parses the response header and returns the result. func (b *respHeaderBinding) Bind(resp *fasthttp.Response, out any) error { data := make(map[string][]string) resp.Header.VisitAll(func(key, val []byte) { diff --git a/binder/uri.go b/binder/uri.go index 2759f7b464..b58d9d49c4 100644 --- a/binder/uri.go +++ b/binder/uri.go @@ -1,11 +1,14 @@ package binder +// uriBinding is the URI binder for URI parameters. type uriBinding struct{} +// Name returns the binding name. func (*uriBinding) Name() string { return "uri" } +// Bind parses the URI parameters and returns the result. func (b *uriBinding) Bind(params []string, paramsFunc func(key string, defaultValue ...string) string, out any) error { data := make(map[string][]string, len(params)) for _, param := range params { diff --git a/binder/xml.go b/binder/xml.go index 35d1de86bc..58da2b9b07 100644 --- a/binder/xml.go +++ b/binder/xml.go @@ -5,12 +5,15 @@ import ( "fmt" ) +// xmlBinding is the XML binder for XML request body. type xmlBinding struct{} +// Name returns the binding name. func (*xmlBinding) Name() string { return "xml" } +// Bind parses the request body as XML and returns the result. func (*xmlBinding) Bind(body []byte, out any) error { if err := xml.Unmarshal(body, out); err != nil { return fmt.Errorf("failed to unmarshal xml: %w", err) diff --git a/client/client.go b/client/client.go index 820be4ee41..2c4086aed3 100644 --- a/client/client.go +++ b/client/client.go @@ -13,6 +13,7 @@ import ( "sync" "time" + "github.com/fxamacker/cbor/v2" "github.com/gofiber/fiber/v3/log" "github.com/gofiber/utils/v2" @@ -44,6 +45,8 @@ type Client struct { jsonUnmarshal utils.JSONUnmarshal xmlMarshal utils.XMLMarshal xmlUnmarshal utils.XMLUnmarshal + cborMarshal utils.CBORMarshal + cborUnmarshal utils.CBORUnmarshal cookieJar *CookieJar @@ -150,6 +153,28 @@ func (c *Client) SetXMLUnmarshal(f utils.XMLUnmarshal) *Client { return c } +// CBORMarshal returns CBOR marshal function in Core. +func (c *Client) CBORMarshal() utils.CBORMarshal { + return c.cborMarshal +} + +// SetCBORMarshal sets CBOR encoder. +func (c *Client) SetCBORMarshal(f utils.CBORMarshal) *Client { + c.cborMarshal = f + return c +} + +// CBORUnmarshal returns CBOR unmarshal function in Core. +func (c *Client) CBORUnmarshal() utils.CBORUnmarshal { + return c.cborUnmarshal +} + +// SetCBORUnmarshal sets CBOR decoder. +func (c *Client) SetCBORUnmarshal(f utils.CBORUnmarshal) *Client { + c.cborUnmarshal = f + return c +} + // TLSConfig returns tlsConfig in client. // If client don't have tlsConfig, this function will init it. func (c *Client) TLSConfig() *tls.Config { @@ -706,6 +731,8 @@ func NewWithClient(c *fasthttp.Client) *Client { jsonMarshal: json.Marshal, jsonUnmarshal: json.Unmarshal, xmlMarshal: xml.Marshal, + cborMarshal: cbor.Marshal, + cborUnmarshal: cbor.Unmarshal, xmlUnmarshal: xml.Unmarshal, logger: log.DefaultLogger(), } diff --git a/client/client_test.go b/client/client_test.go index 0a1ad14ab5..f6722114f9 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -3,6 +3,7 @@ package client import ( "context" "crypto/tls" + "encoding/hex" "errors" "io" "net" @@ -225,6 +226,33 @@ func Test_Client_Marshal(t *testing.T) { require.Equal(t, errors.New("empty xml"), err) }) + t.Run("set cbor marshal", func(t *testing.T) { + t.Parallel() + bs, err := hex.DecodeString("f6") + if err != nil { + t.Error(err) + } + client := New(). + SetCBORMarshal(func(_ any) ([]byte, error) { + return bs, nil + }) + val, err := client.CBORMarshal()(nil) + + require.NoError(t, err) + require.Equal(t, bs, val) + }) + + t.Run("set cbor marshal error", func(t *testing.T) { + t.Parallel() + client := New().SetCBORMarshal(func(_ any) ([]byte, error) { + return nil, errors.New("invalid struct") + }) + + val, err := client.CBORMarshal()(nil) + require.Nil(t, val) + require.Equal(t, errors.New("invalid struct"), err) + }) + t.Run("set xml unmarshal", func(t *testing.T) { t.Parallel() client := New(). @@ -1443,11 +1471,12 @@ func Test_Set_Config_To_Request(t *testing.T) { t.Run("set ctx", func(t *testing.T) { t.Parallel() - key := struct{}{} - ctx := context.Background() - ctx = context.WithValue(ctx, key, "v1") //nolint: staticcheck // not needed for tests + type ctxKey struct{} + var key ctxKey = struct{}{} + ctx := context.Background() + ctx = context.WithValue(ctx, key, "v1") req := AcquireRequest() setConfigToRequest(req, Config{Ctx: ctx}) diff --git a/client/hooks.go b/client/hooks.go index f11f9865f3..ec3987938e 100644 --- a/client/hooks.go +++ b/client/hooks.go @@ -23,6 +23,7 @@ var ( headerAccept = "Accept" applicationJSON = "application/json" + applicationCBOR = "application/cbor" applicationXML = "application/xml" applicationForm = "application/x-www-form-urlencoded" multipartFormData = "multipart/form-data" @@ -129,6 +130,8 @@ func parserRequestHeader(c *Client, req *Request) error { req.RawRequest.Header.Set(headerAccept, applicationJSON) case xmlBody: req.RawRequest.Header.SetContentType(applicationXML) + case cborBody: + req.RawRequest.Header.SetContentType(applicationCBOR) case formBody: req.RawRequest.Header.SetContentType(applicationForm) case filesBody: @@ -189,6 +192,12 @@ func parserRequestBody(c *Client, req *Request) error { return err } req.RawRequest.SetBody(body) + case cborBody: + body, err := c.cborMarshal(req.body) + if err != nil { + return err + } + req.RawRequest.SetBody(body) case formBody: req.RawRequest.SetBody(req.formData.QueryString()) case filesBody: diff --git a/client/hooks_test.go b/client/hooks_test.go index 8e91392930..8a02c7ab49 100644 --- a/client/hooks_test.go +++ b/client/hooks_test.go @@ -10,6 +10,7 @@ import ( "strings" "testing" + "github.com/fxamacker/cbor/v2" "github.com/gofiber/fiber/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -456,6 +457,30 @@ func Test_Parser_Request_Body(t *testing.T) { require.Equal(t, []byte("foo"), req.RawRequest.Body()) }) + t.Run("CBOR body", func(t *testing.T) { + t.Parallel() + type cborData struct { + Name string `cbor:"name"` + Age int `cbor:"age"` + } + + data := cborData{ + Name: "foo", + Age: 12, + } + + client := New() + req := AcquireRequest(). + SetCBOR(data) + + err := parserRequestBody(client, req) + require.NoError(t, err) + + encoded, err := cbor.Marshal(data) + require.NoError(t, err) + require.Equal(t, encoded, req.RawRequest.Body()) + }) + t.Run("form data body", func(t *testing.T) { t.Parallel() client := New() diff --git a/client/request.go b/client/request.go index 61b5798c57..a86d927c4e 100644 --- a/client/request.go +++ b/client/request.go @@ -34,6 +34,7 @@ const ( formBody filesBody rawBody + cborBody ) var ErrClientNil = errors.New("client can not be nil") @@ -337,6 +338,13 @@ func (r *Request) SetXML(v any) *Request { return r } +// SetCBOR method sets CBOR body in request. +func (r *Request) SetCBOR(v any) *Request { + r.body = v + r.bodyType = cborBody + return r +} + // SetRawBody method sets body with raw data in request. func (r *Request) SetRawBody(v []byte) *Request { r.body = v diff --git a/client/request_test.go b/client/request_test.go index 0593d04824..f62865a342 100644 --- a/client/request_test.go +++ b/client/request_test.go @@ -80,11 +80,12 @@ func Test_Request_Context(t *testing.T) { req := AcquireRequest() ctx := req.Context() - key := struct{}{} + type ctxKey struct{} + var key ctxKey = struct{}{} require.Nil(t, ctx.Value(key)) - ctx = context.WithValue(ctx, key, "string") //nolint: staticcheck // not needed for tests + ctx = context.WithValue(ctx, key, "string") req.SetContext(ctx) ctx = req.Context() @@ -1006,6 +1007,25 @@ func Test_Request_Body_With_Server(t *testing.T) { ) }) + t.Run("cbor body", func(t *testing.T) { + t.Parallel() + testRequest(t, + func(c fiber.Ctx) error { + require.Equal(t, "application/cbor", string(c.Request().Header.ContentType())) + return c.SendString(string(c.Request().Body())) + }, + func(agent *Request) { + type args struct { + Content string `cbor:"content"` + } + agent.SetCBOR(args{ + Content: "hello", + }) + }, + "\xa1gcontentehello", + ) + }) + t.Run("formdata", func(t *testing.T) { t.Parallel() testRequest(t, diff --git a/client/response.go b/client/response.go index a8a032b6c5..e60c6bd0fb 100644 --- a/client/response.go +++ b/client/response.go @@ -75,6 +75,11 @@ func (r *Response) JSON(v any) error { return r.client.jsonUnmarshal(r.Body(), v) } +// CBOR method will unmarshal body to CBOR. +func (r *Response) CBOR(v any) error { + return r.client.cborUnmarshal(r.Body(), v) +} + // XML method will unmarshal body to xml. func (r *Response) XML(v any) error { return r.client.xmlUnmarshal(r.Body(), v) diff --git a/client/response_test.go b/client/response_test.go index 0d27ee7ed4..bf12e75161 100644 --- a/client/response_test.go +++ b/client/response_test.go @@ -240,6 +240,18 @@ func Test_Response_Body(t *testing.T) { app.Get("/xml", func(c fiber.Ctx) error { return c.SendString("success") }) + + app.Get("/cbor", func(c fiber.Ctx) error { + type cborData struct { + Name string `cbor:"name"` + Age int `cbor:"age"` + } + + return c.CBOR(cborData{ + Name: "foo", + Age: 12, + }) + }) }) return server @@ -327,6 +339,36 @@ func Test_Response_Body(t *testing.T) { require.Equal(t, "success", tmp.Status) resp.Close() }) + + t.Run("cbor body", func(t *testing.T) { + t.Parallel() + type cborData struct { + Name string `cbor:"name"` + Age int `cbor:"age"` + } + + data := cborData{ + Name: "foo", + Age: 12, + } + + server := setupApp() + defer server.stop() + + client := New().SetDial(server.dial()) + + resp, err := AcquireRequest(). + SetClient(client). + Get("http://example.com/cbor") + + require.NoError(t, err) + + tmp := &cborData{} + err = resp.CBOR(tmp) + require.NoError(t, err) + require.Equal(t, data, *tmp) + resp.Close() + }) } func Test_Response_Save(t *testing.T) { diff --git a/constants.go b/constants.go index 4717204094..782195a2c0 100644 --- a/constants.go +++ b/constants.go @@ -23,6 +23,7 @@ const ( MIMETextCSS = "text/css" MIMEApplicationXML = "application/xml" MIMEApplicationJSON = "application/json" + MIMEApplicationCBOR = "application/cbor" // Deprecated: use MIMETextJavaScript instead MIMEApplicationJavaScript = "application/javascript" MIMEApplicationForm = "application/x-www-form-urlencoded" diff --git a/ctx.go b/ctx.go index 0e5d732b1f..5dde2f89d0 100644 --- a/ctx.go +++ b/ctx.go @@ -884,6 +884,24 @@ func (c *DefaultCtx) JSON(data any, ctype ...string) error { return nil } +// CBOR converts any interface or string to CBOR encoded bytes. +// If the ctype parameter is given, this method will set the +// Content-Type header equal to ctype. If ctype is not given, +// The Content-Type header will be set to application/cbor. +func (c *DefaultCtx) CBOR(data any, ctype ...string) error { + raw, err := c.app.config.CBOREncoder(data) + if err != nil { + return err + } + c.fasthttp.Response.SetBodyRaw(raw) + if len(ctype) > 0 { + c.fasthttp.Response.Header.SetContentType(ctype[0]) + } else { + c.fasthttp.Response.Header.SetContentType(MIMEApplicationCBOR) + } + return nil +} + // JSONP sends a JSON response with JSONP support. // This method is identical to JSON, except that it opts-in to JSONP callback support. // By default, the callback name is simply callback. diff --git a/ctx_interface_gen.go b/ctx_interface_gen.go index f5d0919113..859563f638 100644 --- a/ctx_interface_gen.go +++ b/ctx_interface_gen.go @@ -164,6 +164,11 @@ type Ctx interface { // Content-Type header equal to ctype. If ctype is not given, // The Content-Type header will be set to application/json. JSON(data any, ctype ...string) error + // CBOR converts any interface or string to CBOR encoded bytes. + // If the ctype parameter is given, this method will set the + // Content-Type header equal to ctype. If ctype is not given, + // The Content-Type header will be set to application/cbor. + CBOR(data any, ctype ...string) error // JSONP sends a JSON response with JSONP support. // This method is identical to JSON, except that it opts-in to JSONP callback support. // By default, the callback name is simply callback. diff --git a/ctx_test.go b/ctx_test.go index 71b9e86221..72c415a9ca 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -12,6 +12,7 @@ import ( "context" "crypto/tls" "embed" + "encoding/hex" "encoding/xml" "errors" "fmt" @@ -3618,6 +3619,98 @@ func Benchmark_Ctx_JSON(b *testing.B) { require.JSONEq(b, `{"Name":"Grame","Age":20}`, string(c.Response().Body())) } +// go test -run Test_Ctx_CBOR +func Test_Ctx_CBOR(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + + require.Error(t, c.CBOR(complex(1, 1))) + + type dummyStruct struct { + Name string + Age int + } + + // Test without ctype + err := c.CBOR(dummyStruct{ // map has no order + Name: "Grame", + Age: 20, + }) + require.NoError(t, err) + require.Equal(t, `a2644e616d65654772616d656341676514`, hex.EncodeToString(c.Response().Body())) + require.Equal(t, "application/cbor", string(c.Response().Header.Peek("content-type"))) + + // Test with ctype + err = c.CBOR(dummyStruct{ // map has no order + Name: "Grame", + Age: 20, + }, "application/problem+cbor") + require.NoError(t, err) + require.Equal(t, `a2644e616d65654772616d656341676514`, hex.EncodeToString(c.Response().Body())) + require.Equal(t, "application/problem+cbor", string(c.Response().Header.Peek("content-type"))) + + testEmpty := func(v any, r string) { + err := c.CBOR(v) + require.NoError(t, err) + require.Equal(t, r, hex.EncodeToString(c.Response().Body())) + } + + testEmpty(nil, "f6") + testEmpty("", `60`) + testEmpty(0, "00") + testEmpty([]int{}, "80") + + // Test invalid types + err = c.CBOR(make(chan int)) + require.Error(t, err) + + err = c.CBOR(func() {}) + require.Error(t, err) + + t.Run("custom cbor encoder", func(t *testing.T) { + t.Parallel() + + app := New(Config{ + CBOREncoder: func(_ any) ([]byte, error) { + return []byte(hex.EncodeToString([]byte("random"))), nil + }, + }) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + + err := c.CBOR(Map{ // map has no order + "Name": "Grame", + "Age": 20, + }) + require.NoError(t, err) + require.Equal(t, `72616e646f6d`, string(c.Response().Body())) + require.Equal(t, "application/cbor", string(c.Response().Header.Peek("content-type"))) + }) +} + +// go test -run=^$ -bench=Benchmark_Ctx_CBOR -benchmem -count=4 +func Benchmark_Ctx_CBOR(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + + type SomeStruct struct { + Name string + Age uint8 + } + data := SomeStruct{ + Name: "Grame", + Age: 20, + } + var err error + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + err = c.CBOR(data) + } + require.NoError(b, err) + require.Equal(b, `a2644e616d65654772616d656341676514`, hex.EncodeToString(c.Response().Body())) +} + // go test -run=^$ -bench=Benchmark_Ctx_JSON_Ctype -benchmem -count=4 func Benchmark_Ctx_JSON_Ctype(b *testing.B) { app := New() diff --git a/docs/api/bind.md b/docs/api/bind.md index a1cbedd18d..e91fed4273 100644 --- a/docs/api/bind.md +++ b/docs/api/bind.md @@ -20,6 +20,7 @@ Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more.. - [JSON](#json) - [MultipartForm](#multipartform) - [XML](#xml) + - [CBOR](#cbor) - [Cookie](#cookie) - [Header](#header) - [Query](#query) @@ -226,6 +227,43 @@ Run tests with the following `curl` command: curl -X POST -H "Content-Type: application/xml" --data "johndoe" localhost:3000 ``` +### CBOR + +Binds the request CBOR 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 CBOR body with a field called `Pass`, you would use a struct field with `cbor:"pass"`. + +```go title="Signature" +func (b *Bind) CBOR(out any) error +``` + +```go title="Example" +// Field names should start with an uppercase letter +type Person struct { + Name string `cbor:"name"` + Pass string `cbor:"pass"` +} + +app.Post("/", func(c fiber.Ctx) error { + p := new(Person) + + if err := c.Bind().CBOR(p); err != nil { + return err + } + + log.Println(p.Name) // john + log.Println(p.Pass) // doe + + // ... +}) +``` + +Run tests with the following `curl` command: + +```bash +curl -X POST -H "Content-Type: application/cbor" --data "\xa2dnamedjohndpasscdoe" localhost:3000 +``` + ### Cookie This method is similar to [Body Binding](#body), but for cookie parameters. diff --git a/docs/api/constants.md b/docs/api/constants.md index 9fbff533cb..53bbb25c66 100644 --- a/docs/api/constants.md +++ b/docs/api/constants.md @@ -26,29 +26,31 @@ const ( ```go const ( - MIMETextXML = "text/xml" - MIMETextHTML = "text/html" - MIMETextPlain = "text/plain" - MIMETextJavaScript = "text/javascript" - MIMETextCSS = "text/css" - MIMEApplicationXML = "application/xml" - MIMEApplicationJSON = "application/json" - MIMEApplicationJavaScript = "application/javascript" - MIMEApplicationForm = "application/x-www-form-urlencoded" - MIMEOctetStream = "application/octet-stream" - MIMEMultipartForm = "multipart/form-data" + MIMETextXML = "text/xml" + MIMETextHTML = "text/html" + MIMETextPlain = "text/plain" + MIMETextJavaScript = "text/javascript" + MIMETextCSS = "text/css" + MIMEApplicationXML = "application/xml" + MIMEApplicationJSON = "application/json" + MIMEApplicationCBOR = "application/cbor" + MIMEApplicationJavaScript = "application/javascript" + MIMEApplicationForm = "application/x-www-form-urlencoded" + MIMEOctetStream = "application/octet-stream" + MIMEMultipartForm = "multipart/form-data" - MIMETextXMLCharsetUTF8 = "text/xml; charset=utf-8" - MIMETextHTMLCharsetUTF8 = "text/html; charset=utf-8" - MIMETextPlainCharsetUTF8 = "text/plain; charset=utf-8" - MIMETextJavaScriptCharsetUTF8 = "text/javascript; charset=utf-8" - MIMETextCSSCharsetUTF8 = "text/css; charset=utf-8" - MIMEApplicationXMLCharsetUTF8 = "application/xml; charset=utf-8" - MIMEApplicationJSONCharsetUTF8 = "application/json; charset=utf-8" + MIMETextXMLCharsetUTF8 = "text/xml; charset=utf-8" + MIMETextHTMLCharsetUTF8 = "text/html; charset=utf-8" + MIMETextPlainCharsetUTF8 = "text/plain; charset=utf-8" + MIMETextJavaScriptCharsetUTF8 = "text/javascript; charset=utf-8" + MIMETextCSSCharsetUTF8 = "text/css; charset=utf-8" + MIMEApplicationXMLCharsetUTF8 = "application/xml; charset=utf-8" + MIMEApplicationJSONCharsetUTF8 = "application/json; charset=utf-8" MIMEApplicationJavaScriptCharsetUTF8 = "application/javascript; charset=utf-8" -)``` +) +``` -### HTTP status codes were copied from net/http. +### HTTP status codes were copied from net/http ```go const ( @@ -72,6 +74,7 @@ const ( StatusSeeOther = 303 // RFC 7231, 6.4.4 StatusNotModified = 304 // RFC 7232, 4.1 StatusUseProxy = 305 // RFC 7231, 6.4.5 + StatusSwitchProxy = 306 // RFC 9110, 15.4.7 (Unused) StatusTemporaryRedirect = 307 // RFC 7231, 6.4.7 StatusPermanentRedirect = 308 // RFC 7538, 3 StatusBadRequest = 400 // RFC 7231, 6.5.1 @@ -168,127 +171,129 @@ HTTP Headers were copied from net/http. ```go const ( - HeaderAuthorization = "Authorization" - HeaderProxyAuthenticate = "Proxy-Authenticate" - HeaderProxyAuthorization = "Proxy-Authorization" - HeaderWWWAuthenticate = "WWW-Authenticate" - HeaderAge = "Age" - HeaderCacheControl = "Cache-Control" - HeaderClearSiteData = "Clear-Site-Data" - HeaderExpires = "Expires" - HeaderPragma = "Pragma" - HeaderWarning = "Warning" - HeaderAcceptCH = "Accept-CH" - HeaderAcceptCHLifetime = "Accept-CH-Lifetime" - HeaderContentDPR = "Content-DPR" - HeaderDPR = "DPR" - HeaderEarlyData = "Early-Data" - HeaderSaveData = "Save-Data" - HeaderViewportWidth = "Viewport-Width" - HeaderWidth = "Width" - HeaderETag = "ETag" - HeaderIfMatch = "If-Match" - HeaderIfModifiedSince = "If-Modified-Since" - HeaderIfNoneMatch = "If-None-Match" - HeaderIfUnmodifiedSince = "If-Unmodified-Since" - HeaderLastModified = "Last-Modified" - HeaderVary = "Vary" - HeaderConnection = "Connection" - HeaderKeepAlive = "Keep-Alive" - HeaderAccept = "Accept" - HeaderAcceptCharset = "Accept-Charset" - HeaderAcceptEncoding = "Accept-Encoding" - HeaderAcceptLanguage = "Accept-Language" - HeaderCookie = "Cookie" - HeaderExpect = "Expect" - HeaderMaxForwards = "Max-Forwards" - HeaderSetCookie = "Set-Cookie" - HeaderAccessControlAllowCredentials = "Access-Control-Allow-Credentials" - HeaderAccessControlAllowHeaders = "Access-Control-Allow-Headers" - HeaderAccessControlAllowMethods = "Access-Control-Allow-Methods" - HeaderAccessControlAllowOrigin = "Access-Control-Allow-Origin" - HeaderAccessControlExposeHeaders = "Access-Control-Expose-Headers" - HeaderAccessControlMaxAge = "Access-Control-Max-Age" - HeaderAccessControlRequestHeaders = "Access-Control-Request-Headers" - HeaderAccessControlRequestMethod = "Access-Control-Request-Method" - HeaderOrigin = "Origin" - HeaderTimingAllowOrigin = "Timing-Allow-Origin" - HeaderXPermittedCrossDomainPolicies = "X-Permitted-Cross-Domain-Policies" - HeaderDNT = "DNT" - HeaderTk = "Tk" - HeaderContentDisposition = "Content-Disposition" - HeaderContentEncoding = "Content-Encoding" - HeaderContentLanguage = "Content-Language" - HeaderContentLength = "Content-Length" - HeaderContentLocation = "Content-Location" - HeaderContentType = "Content-Type" - HeaderForwarded = "Forwarded" - HeaderVia = "Via" - HeaderXForwardedFor = "X-Forwarded-For" - HeaderXForwardedHost = "X-Forwarded-Host" - HeaderXForwardedProto = "X-Forwarded-Proto" - HeaderXForwardedProtocol = "X-Forwarded-Protocol" - HeaderXForwardedSsl = "X-Forwarded-Ssl" - HeaderXUrlScheme = "X-Url-Scheme" - HeaderLocation = "Location" - HeaderFrom = "From" - HeaderHost = "Host" - HeaderReferer = "Referer" - HeaderReferrerPolicy = "Referrer-Policy" - HeaderUserAgent = "User-Agent" - HeaderAllow = "Allow" - HeaderServer = "Server" - HeaderAcceptRanges = "Accept-Ranges" - HeaderContentRange = "Content-Range" - HeaderIfRange = "If-Range" - HeaderRange = "Range" - HeaderContentSecurityPolicy = "Content-Security-Policy" - HeaderContentSecurityPolicyReportOnly = "Content-Security-Policy-Report-Only" - HeaderCrossOriginResourcePolicy = "Cross-Origin-Resource-Policy" - HeaderExpectCT = "Expect-CT" - HeaderFeaturePolicy = "Feature-Policy" - HeaderPublicKeyPins = "Public-Key-Pins" - HeaderPublicKeyPinsReportOnly = "Public-Key-Pins-Report-Only" - HeaderStrictTransportSecurity = "Strict-Transport-Security" - HeaderUpgradeInsecureRequests = "Upgrade-Insecure-Requests" - HeaderXContentTypeOptions = "X-Content-Type-Options" - HeaderXDownloadOptions = "X-Download-Options" - HeaderXFrameOptions = "X-Frame-Options" - HeaderXPoweredBy = "X-Powered-By" - HeaderXXSSProtection = "X-XSS-Protection" - HeaderLastEventID = "Last-Event-ID" - HeaderNEL = "NEL" - HeaderPingFrom = "Ping-From" - HeaderPingTo = "Ping-To" - HeaderReportTo = "Report-To" - HeaderTE = "TE" - HeaderTrailer = "Trailer" - HeaderTransferEncoding = "Transfer-Encoding" - HeaderSecWebSocketAccept = "Sec-WebSocket-Accept" - HeaderSecWebSocketExtensions = "Sec-WebSocket-Extensions" - HeaderSecWebSocketKey = "Sec-WebSocket-Key" - HeaderSecWebSocketProtocol = "Sec-WebSocket-Protocol" - HeaderSecWebSocketVersion = "Sec-WebSocket-Version" - HeaderAcceptPatch = "Accept-Patch" - HeaderAcceptPushPolicy = "Accept-Push-Policy" - HeaderAcceptSignature = "Accept-Signature" - HeaderAltSvc = "Alt-Svc" - HeaderDate = "Date" - HeaderIndex = "Index" - HeaderLargeAllocation = "Large-Allocation" - HeaderLink = "Link" - HeaderPushPolicy = "Push-Policy" - HeaderRetryAfter = "Retry-After" - HeaderServerTiming = "Server-Timing" - HeaderSignature = "Signature" - HeaderSignedHeaders = "Signed-Headers" - HeaderSourceMap = "SourceMap" - HeaderUpgrade = "Upgrade" - HeaderXDNSPrefetchControl = "X-DNS-Prefetch-Control" - HeaderXPingback = "X-Pingback" - HeaderXRequestID = "X-Request-ID" - HeaderXRequestedWith = "X-Requested-With" - HeaderXRobotsTag = "X-Robots-Tag" - HeaderXUACompatible = "X-UA-Compatible" + HeaderAuthorization = "Authorization" + HeaderProxyAuthenticate = "Proxy-Authenticate" + HeaderProxyAuthorization = "Proxy-Authorization" + HeaderWWWAuthenticate = "WWW-Authenticate" + HeaderAge = "Age" + HeaderCacheControl = "Cache-Control" + HeaderClearSiteData = "Clear-Site-Data" + HeaderExpires = "Expires" + HeaderPragma = "Pragma" + HeaderWarning = "Warning" + HeaderAcceptCH = "Accept-CH" + HeaderAcceptCHLifetime = "Accept-CH-Lifetime" + HeaderContentDPR = "Content-DPR" + HeaderDPR = "DPR" + HeaderEarlyData = "Early-Data" + HeaderSaveData = "Save-Data" + HeaderViewportWidth = "Viewport-Width" + HeaderWidth = "Width" + HeaderETag = "ETag" + HeaderIfMatch = "If-Match" + HeaderIfModifiedSince = "If-Modified-Since" + HeaderIfNoneMatch = "If-None-Match" + HeaderIfUnmodifiedSince = "If-Unmodified-Since" + HeaderLastModified = "Last-Modified" + HeaderVary = "Vary" + HeaderConnection = "Connection" + HeaderKeepAlive = "Keep-Alive" + HeaderAccept = "Accept" + HeaderAcceptCharset = "Accept-Charset" + HeaderAcceptEncoding = "Accept-Encoding" + HeaderAcceptLanguage = "Accept-Language" + HeaderCookie = "Cookie" + HeaderExpect = "Expect" + HeaderMaxForwards = "Max-Forwards" + HeaderSetCookie = "Set-Cookie" + HeaderAccessControlAllowCredentials = "Access-Control-Allow-Credentials" + HeaderAccessControlAllowHeaders = "Access-Control-Allow-Headers" + HeaderAccessControlAllowMethods = "Access-Control-Allow-Methods" + HeaderAccessControlAllowOrigin = "Access-Control-Allow-Origin" + HeaderAccessControlExposeHeaders = "Access-Control-Expose-Headers" + HeaderAccessControlMaxAge = "Access-Control-Max-Age" + HeaderAccessControlRequestHeaders = "Access-Control-Request-Headers" + HeaderAccessControlRequestMethod = "Access-Control-Request-Method" + HeaderOrigin = "Origin" + HeaderTimingAllowOrigin = "Timing-Allow-Origin" + HeaderXPermittedCrossDomainPolicies = "X-Permitted-Cross-Domain-Policies" + HeaderDNT = "DNT" + HeaderTk = "Tk" + HeaderContentDisposition = "Content-Disposition" + HeaderContentEncoding = "Content-Encoding" + HeaderContentLanguage = "Content-Language" + HeaderContentLength = "Content-Length" + HeaderContentLocation = "Content-Location" + HeaderContentType = "Content-Type" + HeaderForwarded = "Forwarded" + HeaderVia = "Via" + HeaderXForwardedFor = "X-Forwarded-For" + HeaderXForwardedHost = "X-Forwarded-Host" + HeaderXForwardedProto = "X-Forwarded-Proto" + HeaderXForwardedProtocol = "X-Forwarded-Protocol" + HeaderXForwardedSsl = "X-Forwarded-Ssl" + HeaderXUrlScheme = "X-Url-Scheme" + HeaderLocation = "Location" + HeaderFrom = "From" + HeaderHost = "Host" + HeaderReferer = "Referer" + HeaderReferrerPolicy = "Referrer-Policy" + HeaderUserAgent = "User-Agent" + HeaderAllow = "Allow" + HeaderServer = "Server" + HeaderAcceptRanges = "Accept-Ranges" + HeaderContentRange = "Content-Range" + HeaderIfRange = "If-Range" + HeaderRange = "Range" + HeaderContentSecurityPolicy = "Content-Security-Policy" + HeaderContentSecurityPolicyReportOnly = "Content-Security-Policy-Report-Only" + HeaderCrossOriginResourcePolicy = "Cross-Origin-Resource-Policy" + HeaderExpectCT = "Expect-CT" + HeaderFeaturePolicy = "Feature-Policy" + HeaderPublicKeyPins = "Public-Key-Pins" + HeaderPublicKeyPinsReportOnly = "Public-Key-Pins-Report-Only" + HeaderStrictTransportSecurity = "Strict-Transport-Security" + HeaderUpgradeInsecureRequests = "Upgrade-Insecure-Requests" + HeaderXContentTypeOptions = "X-Content-Type-Options" + HeaderXDownloadOptions = "X-Download-Options" + HeaderXFrameOptions = "X-Frame-Options" + HeaderXPoweredBy = "X-Powered-By" + HeaderXXSSProtection = "X-XSS-Protection" + HeaderLastEventID = "Last-Event-ID" + HeaderNEL = "NEL" + HeaderPingFrom = "Ping-From" + HeaderPingTo = "Ping-To" + HeaderReportTo = "Report-To" + HeaderTE = "TE" + HeaderTrailer = "Trailer" + HeaderTransferEncoding = "Transfer-Encoding" + HeaderSecWebSocketAccept = "Sec-WebSocket-Accept" + HeaderSecWebSocketExtensions = "Sec-WebSocket-Extensions" + HeaderSecWebSocketKey = "Sec-WebSocket-Key" + HeaderSecWebSocketProtocol = "Sec-WebSocket-Protocol" + HeaderSecWebSocketVersion = "Sec-WebSocket-Version" + HeaderAcceptPatch = "Accept-Patch" + HeaderAcceptPushPolicy = "Accept-Push-Policy" + HeaderAcceptSignature = "Accept-Signature" + HeaderAltSvc = "Alt-Svc" + HeaderDate = "Date" + HeaderIndex = "Index" + HeaderLargeAllocation = "Large-Allocation" + HeaderLink = "Link" + HeaderPushPolicy = "Push-Policy" + HeaderRetryAfter = "Retry-After" + HeaderServerTiming = "Server-Timing" + HeaderSignature = "Signature" + HeaderSignedHeaders = "Signed-Headers" + HeaderSourceMap = "SourceMap" + HeaderUpgrade = "Upgrade" + HeaderXDNSPrefetchControl = "X-DNS-Prefetch-Control" + HeaderXPingback = "X-Pingback" + HeaderXRequestID = "X-Request-ID" + HeaderXRequestedWith = "X-Requested-With" + HeaderXRobotsTag = "X-Robots-Tag" + HeaderXUACompatible = "X-UA-Compatible" + HeaderAccessControlAllowPrivateNetwork = "Access-Control-Allow-Private-Network" + HeaderAccessControlRequestPrivateNetwork = "Access-Control-Request-Private-Network" ) ``` diff --git a/docs/api/ctx.md b/docs/api/ctx.md index 2171a0ba80..6f4b1ae4c0 100644 --- a/docs/api/ctx.md +++ b/docs/api/ctx.md @@ -924,6 +924,54 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` +## CBOR + +CBOR converts any interface or string to CBOR encoded bytes. + +:::info +CBOR also sets the content header to the `ctype` parameter. If no `ctype` is passed in, the header is set to `application/cbor`. +::: + +```go title="Signature" +func (c fiber.Ctx) CBOR(data any, ctype ...string) error +``` + +```go title="Example" +type SomeStruct struct { + Name string `cbor:"name"` + Age uint8 `cbor:"age"` +} + +app.Get("/cbor", func(c fiber.Ctx) error { + // Create data struct: + data := SomeStruct{ + Name: "Grame", + Age: 20, + } + + return c.CBOR(data) + // => Content-Type: application/cbor + // => \xa2dnameeGramecage\x14 + + return c.CBOR(fiber.Map{ + "name": "Grame", + "age": 20, + }) + // => Content-Type: application/cbor + // => \xa2dnameeGramecage\x14 + + return c.CBOR(fiber.Map{ + "type": "https://example.com/probs/out-of-credit", + "title": "You do not have enough credit.", + "status": 403, + "detail": "Your current balance is 30, but that costs 50.", + "instance": "/account/12345/msgs/abc", + }) + // => Content-Type: application/cbor + // => \xa5dtypex'https://example.com/probs/out-of-creditetitlex\x1eYou do not have enough credit.fstatus\x19\x01\x93fdetailx.Your current balance is 30, but that costs 50.hinstancew/account/12345/msgs/abc +}) +``` + ## Links Joins the links followed by the property to populate the response’s [Link](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link) HTTP header field. diff --git a/docs/api/fiber.md b/docs/api/fiber.md index d8fd7b04f4..91e1c8d38f 100644 --- a/docs/api/fiber.md +++ b/docs/api/fiber.md @@ -42,45 +42,47 @@ 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` | +| 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` | +| 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` | +| JSONEncoder | `utils.JSONMarshal` | Allowing for flexibility in using another json library for encoding. | `json.Marshal` | +| JSONDecoder | `utils.JSONUnmarshal` | Allowing for flexibility in using another json library for decoding. | `json.Unmarshal` | +| CBOREncoder | `utils.CBORMarshal` | Allowing for flexibility in using another cbor library for encoding. | `cbor.Marshal` | +| CBORDecoder | `utils.CBORUnmarshal` | Allowing for flexibility in using another cbor library for decoding. | `cbor.Unmarshal` | +| 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/api/log.md b/docs/api/log.md index 53f964a7dc..91af92276b 100644 --- a/docs/api/log.md +++ b/docs/api/log.md @@ -36,7 +36,7 @@ type CommonLogger interface { type AllLogger interface { CommonLogger - ControlLogger + ConfigurableLogger WithLogger } ``` @@ -186,3 +186,19 @@ commonLogger.Info("info") ``` Binding the logger to a context allows you to include context-specific information in your logs, improving traceability and debugging. + +## Logger + +You can use Logger to retrieve the logger instance. It is useful when you need to access underlying methods of the logger. +To retrieve the Logger instance, use the following method: + +```go +logger := fiberlog.DefaultLogger() // Call DefaultLogger to get the default logger instance + +stdlogger, ok := logger.Logger().(*log.Logger) // Get the logger instance and assert it to *log.Logger +if !ok { + panic("logger is not *log.Logger") +} + +stdlogger.SetFlags(0) // Hide timestamp by setting flags to 0 +``` diff --git a/docs/client/request.md b/docs/client/request.md index d1126dae0e..08a04e65e0 100644 --- a/docs/client/request.md +++ b/docs/client/request.md @@ -657,6 +657,15 @@ SetXML method sets XML body in request. func (r *Request) SetXML(v any) *Request ``` +## SetCBOR + +SetCBOR method sets the request body using [CBOR](https://cbor.io/) encoding format. +It automatically sets the Content-Type header to `"application/cbor"`. + +```go title="Signature" +func (r *Request) SetCBOR(v any) *Request +``` + ## SetRawBody SetRawBody method sets body with raw data in request. diff --git a/docs/client/response.md b/docs/client/response.md index 2256d400e3..0dba3c6b48 100644 --- a/docs/client/response.md +++ b/docs/client/response.md @@ -187,6 +187,14 @@ XML method will unmarshal body to xml. func (r *Response) XML(v any) error ``` +## CBOR + +CBOR method will unmarshal body to CBOR. + +```go title="Signature" +func (r *Response) CBOR(v any) error +``` + ## Save Save method will save the body to a file or io.Writer. diff --git a/docs/client/rest.md b/docs/client/rest.md index 9718131120..04c7a5c44c 100644 --- a/docs/client/rest.md +++ b/docs/client/rest.md @@ -81,6 +81,8 @@ type Client struct { jsonUnmarshal utils.JSONUnmarshal xmlMarshal utils.XMLMarshal xmlUnmarshal utils.XMLUnmarshal + cborMarshal utils.CBORMarshal + cborUnmarshal utils.CBORUnmarshal cookieJar *CookieJar @@ -314,6 +316,40 @@ SetXMLUnmarshal sets the XML decoder. func (c *Client) SetXMLUnmarshal(f utils.XMLUnmarshal) *Client ``` +### CBOR + +#### CBORMarshal + +CBORMarshal returns CBOR marshal function in Core. + +```go title="Signature" +func (c *Client) CBORMarshal() utils.CBORMarshal +``` + +#### CBORUnmarshal + +CBORUnmarshal returns CBOR unmarshal function in Core. + +```go title="Signature" +func (c *Client) CBORUnmarshal() utils.CBORUnmarshal +``` + +#### SetCBORMarshal + +SetCBORMarshal sets CBOR encoder. + +```go title="Signature" +func (c *Client) SetCBORMarshal(f utils.CBORMarshal) *Client +``` + +#### SetCBORUnmarshal + +SetCBORUnmarshal sets CBOR decoder. + +```go title="Signature" +func (c *Client) SetCBORUnmarshal(f utils.CBORUnmarshal) *Client +``` + ### TLS #### TLSConfig diff --git a/docs/intro.md b/docs/intro.md index 4dc763df36..cc0d78f89d 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -12,7 +12,7 @@ These docs are for **Fiber v3**, which was released on **Month xx, 202x**. ### Installation -First, [download](https://go.dev/dl/) and install Go. Version `1.22` or higher is required. +First, [download](https://go.dev/dl/) and install Go. Version `1.23` 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: diff --git a/docs/middleware/logger.md b/docs/middleware/logger.md index 360aaacf07..07ff07c4de 100644 --- a/docs/middleware/logger.md +++ b/docs/middleware/logger.md @@ -90,6 +90,44 @@ app.Use(logger.New(logger.Config{ })) ``` +### Use Logger Middleware with Other Loggers + +In order to use Fiber logger middleware with other loggers such as zerolog, zap, logrus; you can use `LoggerToWriter` helper which converts Fiber logger to a writer, which is compatible with the middleware. + +```go +package main + +import ( + "github.com/gofiber/contrib/fiberzap/v2" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/log" + "github.com/gofiber/fiber/v3/middleware/logger" +) + +func main() { + // Create a new Fiber instance + app := fiber.New() + + // Create a new zap logger which is compatible with Fiber AllLogger interface + zap := fiberzap.NewLogger(fiberzap.LoggerConfig{ + ExtraKeys: []string{"request_id"}, + }) + + // Use the logger middleware with zerolog logger + app.Use(logger.New(logger.Config{ + Output: logger.LoggerToWriter(zap, log.LevelDebug), + })) + + // Define a route + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + // Start server on http://localhost:3000 + app.Listen(":3000") +} +``` + :::tip Writing to os.File is goroutine-safe, but if you are using a custom Output that is not goroutine-safe, make sure to implement locking to properly serialize writes. ::: @@ -108,6 +146,7 @@ Writing to os.File is goroutine-safe, but if you are using a custom Output that | TimeZone | `string` | TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc | `"Local"` | | TimeInterval | `time.Duration` | TimeInterval is the delay before the timestamp is updated. | `500 * time.Millisecond` | | Output | `io.Writer` | Output is a writer where logs are written. | `os.Stdout` | +| LoggerFunc | `func(c fiber.Ctx, data *Data, cfg Config) error` | Custom logger function for integration with logging libraries (Zerolog, Zap, Logrus, etc). Defaults to Fiber's default logger if not defined. | `see default_logger.go defaultLoggerInstance` | | DisableColors | `bool` | DisableColors defines if the logs output should be colorized. | `false` | | enableColors | `bool` | Internal field for enabling colors in the log output. (This is not a user-configurable field) | - | | enableLatency | `bool` | Internal field for enabling latency measurement in logs. (This is not a user-configurable field) | - | @@ -125,6 +164,7 @@ var ConfigDefault = Config{ TimeInterval: 500 * time.Millisecond, Output: os.Stdout, DisableColors: false, + LoggerFunc: defaultLoggerInstance, } ``` diff --git a/docs/whats_new.md b/docs/whats_new.md index a222a65ce5..3221f5dd5e 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -2,15 +2,9 @@ id: whats_new title: πŸ†• Whats New in v3 sidebar_position: 2 -toc_max_heading_level: 3 +toc_max_heading_level: 4 --- -:::caution - -It's a draft, not finished yet. - -::: - [//]: # (https://github.com/gofiber/fiber/releases/tag/v3.0.0-beta.2) ## πŸŽ‰ Welcome @@ -28,10 +22,12 @@ Here's a quick overview of the changes in Fiber `v3`: - [πŸ”„οΈ Redirect](#-redirect) - [🌎 Client package](#-client-package) - [🧰 Generic functions](#-generic-functions) +- [πŸ“ƒ Log](#-log) - [🧬 Middlewares](#-middlewares) - [CORS](#cors) - [CSRF](#csrf) - [Session](#session) + - [Logger](#logger) - [Filesystem](#filesystem) - [Monitor](#monitor) - [Healthcheck](#healthcheck) @@ -39,50 +35,100 @@ Here's a quick overview of the changes in Fiber `v3`: ## Drop for old Go versions -Fiber `v3` drops support for Go versions below `1.22`. We recommend upgrading to Go `1.22` or higher to use Fiber `v3`. +Fiber `v3` drops support for Go versions below `1.23`. We recommend upgrading to Go `1.23` or higher to use Fiber `v3`. ## πŸš€ App -:::caution -DRAFT section -::: - We have made several changes to the Fiber app, including: -- Listen -> unified with config -- Static -> has been removed and moved to [static middleware](./middleware/static.md) -- app.Config properties moved to listen config - - DisableStartupMessage - - EnablePrefork -> previously Prefork - - EnablePrintRoutes - - ListenerNetwork -> previously Network -- app.Config.EnabledTrustedProxyCheck -> has been moved to app.Config.TrustProxy - - TrustedProxies -> has been moved to TrustProxyConfig.Proxies +- **Listen**: The `Listen` method has been unified with the configuration, allowing for more streamlined setup. +- **Static**: The `Static` method has been removed and its functionality has been moved to the [static middleware](./middleware/static.md). +- **app.Config properties**: Several properties have been moved to the listen configuration: + - `DisableStartupMessage` + - `EnablePrefork` (previously `Prefork`) + - `EnablePrintRoutes` + - `ListenerNetwork` (previously `Network`) +- **Trusted Proxy Configuration**: The `EnabledTrustedProxyCheck` has been moved to `app.Config.TrustProxy`, and `TrustedProxies` has been moved to `TrustProxyConfig.Proxies`. -### new methods +### New Methods -- RegisterCustomBinder -- RegisterCustomConstraint -- NewCtxFunc +- **RegisterCustomBinder**: Allows for the registration of custom binders. +- **RegisterCustomConstraint**: Allows for the registration of custom constraints. +- **NewCtxFunc**: Introduces a new context function. -### removed methods +### Removed Methods -- Mount -> Use app.Use() instead -- ListenTLS -> Use app.Listen() with tls.Config -- ListenTLSWithCertificate -> Use app.Listen() with tls.Config -- ListenMutualTLS -> Use app.Listen() with tls.Config -- ListenMutualTLSWithCertificate -> Use app.Listen() with tls.Config +- **Mount**: Use `app.Use()` instead. +- **ListenTLS**: Use `app.Listen()` with `tls.Config`. +- **ListenTLSWithCertificate**: Use `app.Listen()` with `tls.Config`. +- **ListenMutualTLS**: Use `app.Listen()` with `tls.Config`. +- **ListenMutualTLSWithCertificate**: Use `app.Listen()` with `tls.Config`. -### Methods changes +### Method Changes -- 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 +- **Test**: The `Test` method has replaced the timeout parameter with a configuration parameter. `-1` represents no timeout, and `0` represents no timeout. +- **Listen**: Now has a configuration parameter. +- **Listener**: Now has a configuration parameter. -### CTX interface + customizable +### Custom Ctx Interface in Fiber v3 ---- +Fiber v3 introduces a customizable `Ctx` interface, allowing developers to extend and modify the context to fit their needs. This feature provides greater flexibility and control over request handling. + +#### Idea Behind Custom Ctx Classes + +The idea behind custom `Ctx` classes is to give developers the ability to extend the default context with additional methods and properties tailored to the specific requirements of their application. This allows for better request handling and easier implementation of specific logic. + +#### NewCtxFunc + +The `NewCtxFunc` method allows you to customize the `Ctx` struct as needed. + +```go title="Signature" +func (app *App) NewCtxFunc(function func(app *App) CustomCtx) +``` + +
+Example + +Here’s an example of how to customize the `Ctx` interface: + +```go +package main + +import ( + "log" + "github.com/gofiber/fiber/v3" +) + +type CustomCtx struct { + fiber.Ctx +} + +// Custom method +func (c *CustomCtx) CustomMethod() string { + return "custom value" +} + +func main() { + app := fiber.New() + + app.NewCtxFunc(func(app *fiber.App) fiber.Ctx { + return &CustomCtx{ + Ctx: *fiber.NewCtx(app), + } + }) + + app.Get("/", func(c fiber.Ctx) error { + customCtx := c.(*CustomCtx) + return c.SendString(customCtx.CustomMethod()) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +In this example, a custom context `CustomCtx` is created with an additional method `CustomMethod`. The `NewCtxFunc` method is used to replace the default context with the custom one. + +
## πŸ—Ί Router @@ -162,14 +208,14 @@ Registering a subapp is now also possible via the [`Use`](./api/app#use) method ```go // register mulitple prefixes -app.Use(["/v1", "/v2"], func(c *fiber.Ctx) error { +app.Use(["/v1", "/v2"], func(c fiber.Ctx) error { // Middleware for /v1 and /v2 return c.Next() }) // define subapp api := fiber.New() -api.Get("/user", func(c *fiber.Ctx) error { +api.Get("/user", func(c fiber.Ctx) error { return c.SendString("User") }) // register subapp @@ -247,55 +293,50 @@ testConfig := fiber.TestConfig{ } ``` ---- - ## 🧠 Context -:::caution -DRAFT section -::: - ### New Features - Cookie now allows Partitioned cookies for [CHIPS](https://developers.google.com/privacy-sandbox/3pcd/chips) support. CHIPS (Cookies Having Independent Partitioned State) is a feature that improves privacy by allowing cookies to be partitioned by top-level site, mitigating cross-site tracking. -### new methods - -- AutoFormat -> ExpressJs like -- Host -> ExpressJs like -- Port -> ExpressJs like -- IsProxyTrusted -- Reset -- Schema -> ExpressJs like -- SendStream -> ExpressJs like -- SendStreamWriter -- SendString -> ExpressJs like -- String -> ExpressJs like -- ViewBind -> instead of Bind - -### removed methods - -- AllParams -> c.Bind().URL() ? -- ParamsInt -> Params Generic -- QueryBool -> Query Generic -- QueryFloat -> Query Generic -- QueryInt -> Query Generic -- BodyParser -> c.Bind().Body() -- CookieParser -> c.Bind().Cookie() -- ParamsParser -> c.Bind().URL() -- RedirectToRoute -> c.Redirect().Route() -- RedirectBack -> c.Redirect().Back() -- ReqHeaderParser -> c.Bind().Header() - -### changed methods - -- Bind -> for Binding instead of View, us c.ViewBind() -- 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. +### New Methods + +- **AutoFormat**: Similar to Express.js, automatically formats the response based on the request's `Accept` header. +- **Host**: Similar to Express.js, returns the host name of the request. +- **Port**: Similar to Express.js, returns the port number of the request. +- **IsProxyTrusted**: Checks the trustworthiness of the remote IP. +- **Reset**: Resets context fields for server handlers. +- **Schema**: Similar to Express.js, returns the schema (HTTP or HTTPS) of the request. +- **SendStream**: Similar to Express.js, sends a stream as the response. +- **SendStreamWriter**: Sends a stream using a writer function. +- **SendString**: Similar to Express.js, sends a string as the response. +- **String**: Similar to Express.js, converts a value to a string. +- **ViewBind**: Binds data to a view, replacing the old `Bind` method. +- **CBOR**: Introducing [CBOR](https://cbor.io/) binary encoding format for both request & response body. CBOR is a binary data serialization format which is both compact and efficient, making it ideal for use in web applications. + +### Removed Methods + +- **AllParams**: Use `c.Bind().URL()` instead. +- **ParamsInt**: Use `Params` with generic types. +- **QueryBool**: Use `Query` with generic types. +- **QueryFloat**: Use `Query` with generic types. +- **QueryInt**: Use `Query` with generic types. +- **BodyParser**: Use `c.Bind().Body()` instead. +- **CookieParser**: Use `c.Bind().Cookie()` instead. +- **ParamsParser**: Use `c.Bind().URL()` instead. +- **RedirectToRoute**: Use `c.Redirect().Route()` instead. +- **RedirectBack**: Use `c.Redirect().Back()` instead. +- **ReqHeaderParser**: Use `c.Bind().Header()` instead. + +### Changed Methods + +- **Bind**: Now used for binding instead of view binding. Use `c.ViewBind()` for view binding. +- **Format**: Parameter changed from `body interface{}` to `handlers ...ResFmt`. +- **Redirect**: Use `c.Redirect().To()` instead. +- **SendFile**: Now supports different configurations using a config parameter. +- **Context**: Renamed to `RequestCtx` to correspond with the FastHTTP Request Context. +- **UserContext**: Renamed to `Context`, which returns a `context.Context` object. +- **SetUserContext**: Renamed to `SetContext`. ### SendStreamWriter @@ -343,21 +384,259 @@ You can take a look to [client docs](./client/rest.md) to see what's new with th ## πŸ“Ž Binding -:::caution -DRAFT section -::: +Fiber v3 introduces a new binding mechanism that simplifies the process of binding request data to structs. The new binding system supports binding from various sources such as URL parameters, query parameters, headers, and request bodies. This unified approach makes it easier to handle different types of request data in a consistent manner. + +### New Features + +- Unified binding from URL parameters, query parameters, headers, and request bodies. +- Support for custom binders and constraints. +- Improved error handling and validation. + +
+Example + +```go +type User struct { + ID int `params:"id"` + Name string `json:"name"` + Email string `json:"email"` +} + +app.Post("/user/:id", func(c fiber.Ctx) error { + var user User + if err := c.Bind().Body(&user); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(user) +}) +``` + +In this example, the `Bind` method is used to bind the request body to the `User` struct. The `Body` method of the `Bind` class performs the actual binding. + +
## πŸ”„ Redirect -:::caution -DRAFT section -::: +Fiber v3 enhances the redirect functionality by introducing new methods and improving existing ones. The new redirect methods provide more flexibility and control over the redirection process. + +### New Methods + +- `Redirect().To()`: Redirects to a specific URL. +- `Redirect().Route()`: Redirects to a named route. +- `Redirect().Back()`: Redirects to the previous URL. + +
+Example + +```go +app.Get("/old", func(c fiber.Ctx) error { + return c.Redirect().To("/new") +}) + +app.Get("/new", func(c fiber.Ctx) error { + return c.SendString("Welcome to the new route!") +}) +``` + +
## 🧰 Generic functions -:::caution -DRAFT section -::: +Fiber v3 introduces new generic functions that provide additional utility and flexibility for developers. These functions are designed to simplify common tasks and improve code readability. + +### New Generic Functions + +- **Convert**: Converts a value with a specified converter function and default value. +- **Locals**: Retrieves or sets local values within a request context. +- **Params**: Retrieves route parameters and can handle various types of route parameters. +- **Query**: Retrieves the value of a query parameter from the request URI and can handle various types of query parameters. +- **GetReqHeader**: Returns the HTTP request header specified by the field and can handle various types of header values. + +### Example + +
+Convert + +```go +package main + +import ( + "strconv" + "github.com/gofiber/fiber/v3" +) + +func main() { + app := fiber.New() + + app.Get("/convert", func(c fiber.Ctx) error { + value, err := Convert[string](c.Query("value"), strconv.Atoi, 0) + if err != nil { + return c.Status(fiber.StatusBadRequest).SendString(err.Error()) + } + return c.JSON(value) + }) + + app.Listen(":3000") +} +``` + +```sh +curl "http://localhost:3000/convert?value=123" +# Output: 123 + +curl "http://localhost:3000/convert?value=abc" +# Output: "failed to convert: strconv.Atoi: parsing \"abc\": invalid syntax" +``` + +
+ +
+Locals + +```go +package main + +import ( + "github.com/gofiber/fiber/v3" +) + +func main() { + app := fiber.New() + + app.Use("/user/:id", func(c fiber.Ctx) error { + // ask database for user + // ... + // set local values from database + fiber.Locals[string](c, "user", "john") + fiber.Locals[int](c, "age", 25) + // ... + + return c.Next() + }) + + app.Get("/user/*", func(c fiber.Ctx) error { + // get local values + name := fiber.Locals[string](c, "user") + age := fiber.Locals[int](c, "age") + // ... + return c.JSON(fiber.Map{"name": name, "age": age}) + }) + + app.Listen(":3000") +} +``` + +```sh +curl "http://localhost:3000/user/5" +# Output: {"name":"john","age":25} +``` + +
+ +
+Params + +```go +package main + +import ( + "github.com/gofiber/fiber/v3" +) + +func main() { + app := fiber.New() + + app.Get("/params/:id", func(c fiber.Ctx) error { + id := Params[int](c, "id", 0) + return c.JSON(id) + }) + + app.Listen(":3000") +} + +``` + +```sh +curl "http://localhost:3000/params/123" +# Output: 123 + +curl "http://localhost:3000/params/abc" +# Output: 0 +``` + +
+ +
+Query + +```go +package main + +import ( + "github.com/gofiber/fiber/v3" +) + +func main() { + app := fiber.New() + + app.Get("/query", func(c fiber.Ctx) error { + age := Query[int](c, "age", 0) + return c.JSON(age) + }) + + app.Listen(":3000") +} + +``` + +```sh + +curl "http://localhost:3000/query?age=25" +# Output: 25 + +curl "http://localhost:3000/query?age=abc" +# Output: 0 +``` + +
+ +
+GetReqHeader + +```go +package main + +import ( + "github.com/gofiber/fiber/v3" +) + +func main() { + app := fiber.New() + + app.Get("/header", func(c fiber.Ctx) error { + userAgent := GetReqHeader[string](c, "User-Agent", "Unknown") + return c.JSON(userAgent) + }) + + app.Listen(":3000") +} +``` + +```sh +curl -H "User-Agent: CustomAgent" "http://localhost:3000/header" +# Output: "CustomAgent" + +curl "http://localhost:3000/header" +# Output: "Unknown" +``` + +
+ +## πŸ“ƒ Log + +`fiber.AllLogger` interface now has a new method called `Logger`. This method can be used to get the underlying logger instance from the Fiber logger middleware. This is useful when you want to configure the logger middleware with a custom logger and still want to access the underlying logger instance. + +You can find more details about this feature in [/docs/api/log.md](./api/log.md#logger). ## 🧬 Middlewares @@ -434,6 +713,49 @@ The Session middleware has undergone key changes in v3 to improve functionality For more details on these changes and migration instructions, check the [Session Middleware Migration Guide](./middleware/session.md#migration-guide). +### Logger + +New helper function called `LoggerToWriter` has been added to the logger middleware. This function allows you to use 3rd party loggers such as `logrus` or `zap` with the Fiber logger middleware without any extra afford. For example, you can use `zap` with Fiber logger middleware like this: + +
+Example + +```go +package main + +import ( + "github.com/gofiber/contrib/fiberzap/v2" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/log" + "github.com/gofiber/fiber/v3/middleware/logger" +) + +func main() { + // Create a new Fiber instance + app := fiber.New() + + // Create a new zap logger which is compatible with Fiber AllLogger interface + zap := fiberzap.NewLogger(fiberzap.LoggerConfig{ + ExtraKeys: []string{"request_id"}, + }) + + // Use the logger middleware with zerolog logger + app.Use(logger.New(logger.Config{ + Output: logger.LoggerToWriter(zap, log.LevelDebug), + })) + + // Define a route + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + // Start server on http://localhost:3000 + app.Listen(":3000") +} +``` + +
+ ### Filesystem We've decided to remove filesystem middleware to clear up the confusion between static and filesystem middleware. @@ -552,16 +874,16 @@ To migrate [`Route`](#route-chaining) you need to read [this](#route-chaining). ```go // Before app.Route("/api", func(apiGrp Router) { - apiGrp.Route("/user/:id?", func(userGrp Router) { - userGrp.Get("/", func(c fiber.Ctx) error { - // Get user - return c.JSON(fiber.Map{"message": "Get user", "id": c.Params("id")}) - }) - userGrp.Post("/", func(c fiber.Ctx) error { - // Create user - return c.JSON(fiber.Map{"message": "User created"}) - }) + apiGrp.Route("/user/:id?", func(userGrp Router) { + userGrp.Get("/", func(c fiber.Ctx) error { + // Get user + return c.JSON(fiber.Map{"message": "Get user", "id": c.Params("id")}) }) + userGrp.Post("/", func(c fiber.Ctx) error { + // Create user + return c.JSON(fiber.Map{"message": "User created"}) + }) + }) }) ``` @@ -605,12 +927,296 @@ development mode. Avoid using it concurrently. ### 🧠 Context -### πŸ“Ž Parser +Fiber v3 introduces several new features and changes to the Ctx interface, enhancing its functionality and flexibility. + +- **ParamsInt**: Use `Params` with generic types. +- **QueryBool**: Use `Query` with generic types. +- **QueryFloat**: Use `Query` with generic types. +- **QueryInt**: Use `Query` with generic types. +- **Bind**: Now used for binding instead of view binding. Use `c.ViewBind()` for view binding. + +In Fiber v3, the `Ctx` parameter in handlers is now an interface, which means the `*` symbol is no longer used. Here is an example demonstrating this change: + +
+Example + +**Before**: + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" +) + +func main() { + app := fiber.New() + + // Route Handler with *fiber.Ctx + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + app.Listen(":3000") +} +``` + +**After**: + +```go +package main + +import ( + "github.com/gofiber/fiber/v3" +) + +func main() { + app := fiber.New() + + // Route Handler without *fiber.Ctx + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("Hello, World!") + }) -### πŸ”„ Redirect + app.Listen(":3000") +} +``` + +**Explanation**: + +In this example, the `Ctx` parameter in the handler is used as an interface (`fiber.Ctx`) instead of a pointer (`*fiber.Ctx`). This change allows for more flexibility and customization in Fiber v3. + +
+ +#### πŸ“Ž Parser + +The `Parser` section in Fiber v3 has undergone significant changes to improve functionality and flexibility. + +##### Migration Instructions + +1. **BodyParser**: Use `c.Bind().Body()` instead of `c.BodyParser()`. + +
+ Example + + ```go + // Before + app.Post("/user", func(c *fiber.Ctx) error { + var user User + if err := c.BodyParser(&user); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(user) + }) + ``` + + ```go + // After + app.Post("/user", func(c fiber.Ctx) error { + var user User + if err := c.Bind().Body(&user); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(user) + }) + ``` + +
+ +2. **ParamsParser**: Use `c.Bind().URL()` instead of `c.ParamsParser()`. + +
+ Example + + ```go + // Before + app.Get("/user/:id", func(c *fiber.Ctx) error { + var params Params + if err := c.ParamsParser(¶ms); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(params) + }) + ``` + + ```go + // After + app.Get("/user/:id", func(c fiber.Ctx) error { + var params Params + if err := c.Bind().URL(¶ms); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(params) + }) + ``` + +
+ +3. **QueryParser**: Use `c.Bind().Query()` instead of `c.QueryParser()`. + +
+ Example + + ```go + // Before + app.Get("/search", func(c *fiber.Ctx) error { + var query Query + if err := c.QueryParser(&query); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(query) + }) + ``` + + ```go + // After + app.Get("/search", func(c fiber.Ctx) error { + var query Query + if err := c.Bind().Query(&query); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(query) + }) + ``` + +
+ +4. **CookieParser**: Use `c.Bind().Cookie()` instead of `c.CookieParser()`. + +
+ Example + + ```go + // Before + app.Get("/cookie", func(c *fiber.Ctx) error { + var cookie Cookie + if err := c.CookieParser(&cookie); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(cookie) + }) + ``` + + ```go + // After + app.Get("/cookie", func(c fiber.Ctx) error { + var cookie Cookie + if err := c.Bind().Cookie(&cookie); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(cookie) + }) + ``` + +
+ +#### πŸ”„ Redirect + +Fiber v3 enhances the redirect functionality by introducing new methods and improving existing ones. The new redirect methods provide more flexibility and control over the redirection process. + +##### Migration Instructions + +1. **RedirectToRoute**: Use `c.Redirect().Route()` instead of `c.RedirectToRoute()`. + +
+ Example + + ```go + // Before + app.Get("/old", func(c *fiber.Ctx) error { + return c.RedirectToRoute("newRoute") + }) + ``` + + ```go + // After + app.Get("/old", func(c fiber.Ctx) error { + return c.Redirect().Route("newRoute") + }) + ``` + +
+ +2. **RedirectBack**: Use `c.Redirect().Back()` instead of `c.RedirectBack()`. + +
+ Example + + ```go + // Before + app.Get("/back", func(c *fiber.Ctx) error { + return c.RedirectBack() + }) + ``` + + ```go + // After + app.Get("/back", func(c fiber.Ctx) error { + return c.Redirect().Back() + }) + ``` + +
+ +3. **Redirect**: Use `c.Redirect().To()` instead of `c.Redirect()`. + +
+ Example + + ```go + // Before + app.Get("/old", func(c *fiber.Ctx) error { + return c.Redirect("/new") + }) + ``` + + ```go + // After + app.Get("/old", func(c fiber.Ctx) error { + return c.Redirect().To("/new") + }) + ``` + +
### 🌎 Client package +Fiber v3 introduces a completely rebuilt client package with numerous new features such as Cookiejar, request/response hooks, and more. Here is a guide to help you migrate from Fiber v2 to Fiber v3. + +#### New Features + +- **Cookiejar**: Manage cookies automatically. +- **Request/Response Hooks**: Customize request and response handling. +- **Improved Error Handling**: Better error management and reporting. + +#### Migration Instructions + +**Import Path**: + +Update the import path to the new client package. + +
+Before + +```go +import "github.com/gofiber/fiber/v2/client" +``` + +
+ +
+After + +```go +import "github.com/gofiber/fiber/v3/client" +``` + +
+ +:::caution +DRAFT section +::: + ### 🧬 Middlewares #### CORS @@ -692,11 +1298,11 @@ Previously, the Healthcheck middleware was configured with a combined setup for ```go //before app.Use(healthcheck.New(healthcheck.Config{ - LivenessProbe: func(c *fiber.Ctx) bool { + LivenessProbe: func(c fiber.Ctx) bool { return true }, LivenessEndpoint: "/live", - ReadinessProbe: func(c *fiber.Ctx) bool { + ReadinessProbe: func(c fiber.Ctx) bool { return serviceA.Ready() && serviceB.Ready() && ... }, ReadinessEndpoint: "/ready", @@ -710,7 +1316,7 @@ With the new version, each health check endpoint is configured separately, allow // Default liveness endpoint configuration app.Get(healthcheck.DefaultLivenessEndpoint, healthcheck.NewHealthChecker(healthcheck.Config{ - Probe: func(c *fiber.Ctx) bool { + Probe: func(c fiber.Ctx) bool { return true }, })) @@ -721,7 +1327,7 @@ app.Get(healthcheck.DefaultReadinessEndpoint, healthcheck.NewHealthChecker()) // New default startup endpoint configuration // Default endpoint is /startupz app.Get(healthcheck.DefaultStartupEndpoint, healthcheck.NewHealthChecker(healthcheck.Config{ - Probe: func(c *fiber.Ctx) bool { + Probe: func(c fiber.Ctx) bool { return serviceA.Ready() && serviceB.Ready() && ... }, })) diff --git a/go.mod b/go.mod index a94fadd1a8..f8fa9006e5 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/gofiber/fiber/v3 -go 1.22 +go 1.23 require ( github.com/gofiber/schema v1.2.0 @@ -17,10 +17,12 @@ require ( require ( github.com/andybalholm/brotli v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // direct github.com/klauspost/compress v1.17.11 // indirect github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect + github.com/x448/float16 v0.8.4 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/text v0.19.0 // indirect diff --git a/log/default.go b/log/default.go index 9a3c93b1c2..6de940a418 100644 --- a/log/default.go +++ b/log/default.go @@ -210,6 +210,11 @@ func (l *defaultLogger) SetOutput(writer io.Writer) { l.stdlog.SetOutput(writer) } +// Logger returns the logger instance. It can be used to adjust the logger configurations in case of need. +func (l *defaultLogger) Logger() any { + return l.stdlog +} + // DefaultLogger returns the default logger. func DefaultLogger() AllLogger { return logger diff --git a/log/default_test.go b/log/default_test.go index 4a6ff82e59..86a276bc81 100644 --- a/log/default_test.go +++ b/log/default_test.go @@ -221,6 +221,22 @@ func Test_SetLevel(t *testing.T) { require.Equal(t, "[?8] ", setLogger.level.toString()) } +func Test_Logger(t *testing.T) { + underlyingLogger := log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile|log.Lmicroseconds) + setLogger := &defaultLogger{ + stdlog: underlyingLogger, + depth: 4, + } + + require.Equal(t, underlyingLogger, setLogger.Logger()) + + logger, ok := setLogger.Logger().(*log.Logger) + require.True(t, ok) + + logger.SetFlags(log.LstdFlags | log.Lshortfile | log.Lmicroseconds) + require.Equal(t, log.LstdFlags|log.Lshortfile|log.Lmicroseconds, setLogger.stdlog.Flags()) +} + func Test_Debugw(t *testing.T) { initDefaultLogger() diff --git a/log/log.go b/log/log.go index 9d9cd8b0d2..4a7fd5fe0b 100644 --- a/log/log.go +++ b/log/log.go @@ -52,17 +52,27 @@ type CommonLogger interface { WithLogger } -// ControlLogger provides methods to config a logger. -type ControlLogger interface { +// ConfigurableLogger provides methods to config a logger. +type ConfigurableLogger interface { + // SetLevel sets logging level. + // + // Available levels: Trace, Debug, Info, Warn, Error, Fatal, Panic. SetLevel(level Level) + + // SetOutput sets the logger output. SetOutput(w io.Writer) + + // Logger returns the logger instance. It can be used to adjust the logger configurations in case of need. + Logger() any } -// AllLogger is the combination of Logger, FormatLogger, CtxLogger and ControlLogger. +// AllLogger is the combination of Logger, FormatLogger, CtxLogger and ConfigurableLogger. // Custom extensions can be made through AllLogger type AllLogger interface { CommonLogger - ControlLogger + ConfigurableLogger + + // WithContext returns a new logger with the given context. WithContext(ctx context.Context) CommonLogger } diff --git a/middleware/cache/manager_msgp.go b/middleware/cache/manager_msgp.go index bf5d615200..492e9a88bd 100644 --- a/middleware/cache/manager_msgp.go +++ b/middleware/cache/manager_msgp.go @@ -52,6 +52,9 @@ func (z *item) DecodeMsg(dc *msgp.Reader) (err error) { err = msgp.WrapError(err, "headers", za0001) return } + if za0002 == nil { + za0002 = make([]byte, 0) + } z.headers[za0001] = za0002 } case "body": @@ -267,6 +270,9 @@ func (z *item) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "headers", za0001) return } + if za0002 == nil { + za0002 = make([]byte, 0) + } z.headers[za0001] = za0002 } case "body": diff --git a/middleware/logger/logger_test.go b/middleware/logger/logger_test.go index 946db05d80..d459f22c59 100644 --- a/middleware/logger/logger_test.go +++ b/middleware/logger/logger_test.go @@ -1,3 +1,4 @@ +//nolint:depguard // Because we test logging :D package logger import ( @@ -6,15 +7,18 @@ import ( "errors" "fmt" "io" + "log" "net/http" "net/http/httptest" "os" "runtime" + "strconv" "sync" "testing" "time" "github.com/gofiber/fiber/v3" + fiberlog "github.com/gofiber/fiber/v3/log" "github.com/gofiber/fiber/v3/middleware/requestid" "github.com/stretchr/testify/require" "github.com/valyala/bytebufferpool" @@ -181,6 +185,83 @@ func Test_Logger_ErrorTimeZone(t *testing.T) { require.Equal(t, fiber.StatusNotFound, resp.StatusCode) } +// go test -run Test_Logger_Fiber_Logger +func Test_Logger_LoggerToWriter(t *testing.T) { + app := fiber.New() + + buf := bytebufferpool.Get() + t.Cleanup(func() { + bytebufferpool.Put(buf) + }) + + logger := fiberlog.DefaultLogger() + stdlogger, ok := logger.Logger().(*log.Logger) + require.True(t, ok) + + stdlogger.SetFlags(0) + logger.SetOutput(buf) + + testCases := []struct { + levelStr string + level fiberlog.Level + }{ + { + level: fiberlog.LevelTrace, + levelStr: "Trace", + }, + { + level: fiberlog.LevelDebug, + levelStr: "Debug", + }, + { + level: fiberlog.LevelInfo, + levelStr: "Info", + }, + { + level: fiberlog.LevelWarn, + levelStr: "Warn", + }, + { + level: fiberlog.LevelError, + levelStr: "Error", + }, + } + + for _, tc := range testCases { + level := strconv.Itoa(int(tc.level)) + t.Run(level, func(t *testing.T) { + buf.Reset() + + app.Use("/"+level, New(Config{ + Format: "${error}", + Output: LoggerToWriter(logger, tc. + level), + })) + + app.Get("/"+level, func(_ fiber.Ctx) error { + return errors.New("some random error") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/"+level, nil)) + require.NoError(t, err) + require.Equal(t, fiber.StatusInternalServerError, resp.StatusCode) + require.Equal(t, "["+tc.levelStr+"] some random error\n", buf.String()) + }) + + require.Panics(t, func() { + LoggerToWriter(logger, fiberlog.LevelPanic) + }) + + require.Panics(t, func() { + LoggerToWriter(logger, fiberlog.LevelFatal) + }) + + require.Panics(t, func() { + LoggerToWriter(nil, fiberlog.LevelFatal) + }) + } +} + type fakeErrorOutput int func (o *fakeErrorOutput) Write([]byte) (int, error) { @@ -733,6 +814,19 @@ func Benchmark_Logger(b *testing.B) { benchmarkSetup(bb, app, "/") }) + b.Run("DefaultFormatWithFiberLog", func(bb *testing.B) { + app := fiber.New() + logger := fiberlog.DefaultLogger() + logger.SetOutput(io.Discard) + app.Use(New(Config{ + Output: LoggerToWriter(logger, fiberlog.LevelDebug), + })) + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + benchmarkSetup(bb, app, "/") + }) + b.Run("WithTagParameter", func(bb *testing.B) { app := fiber.New() app.Use(New(Config{ @@ -876,6 +970,19 @@ func Benchmark_Logger_Parallel(b *testing.B) { benchmarkSetupParallel(bb, app, "/") }) + b.Run("DefaultFormatWithFiberLog", func(bb *testing.B) { + app := fiber.New() + logger := fiberlog.DefaultLogger() + logger.SetOutput(io.Discard) + app.Use(New(Config{ + Output: LoggerToWriter(logger, fiberlog.LevelDebug), + })) + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + benchmarkSetupParallel(bb, app, "/") + }) + b.Run("DefaultFormatDisableColors", func(bb *testing.B) { app := fiber.New() app.Use(New(Config{ diff --git a/middleware/logger/utils.go b/middleware/logger/utils.go index 5c0432718c..bab734794a 100644 --- a/middleware/logger/utils.go +++ b/middleware/logger/utils.go @@ -1,7 +1,11 @@ package logger import ( + "io" + "github.com/gofiber/fiber/v3" + fiberlog "github.com/gofiber/fiber/v3/log" + "github.com/gofiber/utils/v2" ) func methodColor(method string, colors fiber.Colors) string { @@ -37,3 +41,48 @@ func statusColor(code int, colors fiber.Colors) string { return colors.Red } } + +type customLoggerWriter struct { + loggerInstance fiberlog.AllLogger + level fiberlog.Level +} + +func (cl *customLoggerWriter) Write(p []byte) (int, error) { + switch cl.level { + case fiberlog.LevelTrace: + cl.loggerInstance.Trace(utils.UnsafeString(p)) + case fiberlog.LevelDebug: + cl.loggerInstance.Debug(utils.UnsafeString(p)) + case fiberlog.LevelInfo: + cl.loggerInstance.Info(utils.UnsafeString(p)) + case fiberlog.LevelWarn: + cl.loggerInstance.Warn(utils.UnsafeString(p)) + case fiberlog.LevelError: + cl.loggerInstance.Error(utils.UnsafeString(p)) + default: + return 0, nil + } + + return len(p), nil +} + +// LoggerToWriter is a helper function that returns an io.Writer that writes to a custom logger. +// You can integrate 3rd party loggers such as zerolog, logrus, etc. to logger middleware using this function. +// +// Valid levels: fiberlog.LevelInfo, fiberlog.LevelTrace, fiberlog.LevelWarn, fiberlog.LevelDebug, fiberlog.LevelError +func LoggerToWriter(logger fiberlog.AllLogger, level fiberlog.Level) io.Writer { + // Check if customLogger is nil + if logger == nil { + fiberlog.Panic("LoggerToWriter: customLogger must not be nil") + } + + // Check if level is valid + if level == fiberlog.LevelFatal || level == fiberlog.LevelPanic { + fiberlog.Panic("LoggerToWriter: invalid level") + } + + return &customLoggerWriter{ + level: level, + loggerInstance: logger, + } +}