diff --git a/app.go b/app.go
index e1e607e6fa..71905f8874 100644
--- a/app.go
+++ b/app.go
@@ -23,9 +23,9 @@ import (
"sync"
"time"
+ "github.com/fxamacker/cbor/v2"
"github.com/gofiber/fiber/v3/log"
"github.com/gofiber/utils/v2"
-
"github.com/valyala/fasthttp"
)
@@ -318,6 +318,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
//
@@ -535,6 +549,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 eff595cd72..2cd6e73a69 100644
--- a/bind.go
+++ b/bind.go
@@ -120,6 +120,13 @@ func (b *Bind) JSON(out any) error {
return b.validateStruct(out)
}
+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 {
@@ -182,6 +189,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 aa00e191ca..f13238918d 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"
@@ -913,11 +914,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))
@@ -925,19 +926,26 @@ 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)
})
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) {
@@ -1091,6 +1099,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
@@ -1710,9 +1747,16 @@ func Test_Bind_RepeatParserWithSameStruct(t *testing.T) {
require.NoError(t, c.Bind().Body(r))
require.Equal(t, "body_param", r.BodyParam)
}
+ cb, err := cbor.Marshal(&Request{
+ BodyParam: "body_param",
+ })
+ if err != nil {
+ t.Error(err)
+ }
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/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..12ed13715c
--- /dev/null
+++ b/binder/cbor.go
@@ -0,0 +1,15 @@
+package binder
+
+import (
+ "github.com/gofiber/utils/v2"
+)
+
+type cborBinding struct{}
+
+func (*cborBinding) Name() string {
+ return "cbor"
+}
+
+func (*cborBinding) Bind(body []byte, cborDecoder utils.CBORUnmarshal, out any) error {
+ return cborDecoder(body, out)
+}
diff --git a/client/client.go b/client/client.go
index d9b9c84c63..3665ff21e6 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 xml marshal function in Core.
+func (c *Client) CBORMarshal() utils.CBORMarshal {
+ return c.cborMarshal
+}
+
+// SetCBORMarshal Set xml encoder.
+func (c *Client) SetCBORMarshal(f utils.CBORMarshal) *Client {
+ c.cborMarshal = f
+ return c
+}
+
+// CBORUnmarshal returns xml unmarshal function in Core.
+func (c *Client) CBORUnmarshal() utils.CBORUnmarshal {
+ return c.cborUnmarshal
+}
+
+// SetCBORUnmarshal Set xml 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 {
@@ -698,6 +723,8 @@ func New() *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 0323f70ccd..c91c1da5db 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"
@@ -202,6 +203,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().
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/request.go b/client/request.go
index 61b5798c57..724358600a 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,12 @@ func (r *Request) SetXML(v any) *Request {
return r
}
+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/response.go b/client/response.go
index a8a032b6c5..f21290f764 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/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 a2eee2754b..28f9a9d27c 100644
--- a/ctx.go
+++ b/ctx.go
@@ -883,6 +883,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 9fd434bc3e..8705073656 100644
--- a/ctx_interface_gen.go
+++ b/ctx_interface_gen.go
@@ -163,6 +163,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 af56197c58..cdab326396 100644
--- a/ctx_test.go
+++ b/ctx_test.go
@@ -12,6 +12,7 @@ import (
"context"
"crypto/tls"
"embed"
+ "encoding/hex"
"encoding/xml"
"errors"
"fmt"
@@ -3615,6 +3616,91 @@ 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")
+
+ 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/whats_new.md b/docs/whats_new.md
index 0a47dca491..1ffdcfa447 100644
--- a/docs/whats_new.md
+++ b/docs/whats_new.md
@@ -196,6 +196,8 @@ DRAFT section
- 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.
+- Introducing [CBOR](https://cbor.io/) binary encoding format for response body. CBOR is a binary data serialization format that is both compact and efficient, making it ideal for use in web applications.
+
### new methods
- AutoFormat -> ExpressJs like
@@ -208,6 +210,7 @@ DRAFT section
- SendString -> ExpressJs like
- String -> ExpressJs like
- ViewBind -> instead of Bind
+- CBOR -> for CBOR encoding
### removed methods
diff --git a/go.mod b/go.mod
index 781a260a03..559556367f 100644
--- a/go.mod
+++ b/go.mod
@@ -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/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":