diff --git a/app.go b/app.go
index 7f9193a1a1..5e5475b5f1 100644
--- a/app.go
+++ b/app.go
@@ -341,6 +341,13 @@ type Config struct { //nolint:govet // Aligning the struct fields is not necessa
// Default: xml.Marshal
XMLEncoder utils.XMLMarshal `json:"-"`
+ // XMLDecoder set by an external client of Fiber it will use the provided implementation of a
+ // XMLUnmarshal
+ //
+ // Allowing for flexibility in using another XML library for decoding
+ // Default: xml.Unmarshal
+ XMLDecoder utils.XMLUnmarshal `json:"-"`
+
// If you find yourself behind some sort of proxy, like a load balancer,
// then certain header information may be sent to you using special X-Forwarded-* headers or the Forwarded header.
// For example, the Host HTTP header is usually used to return the requested host.
@@ -560,6 +567,9 @@ func New(config ...Config) *App {
if app.config.XMLEncoder == nil {
app.config.XMLEncoder = xml.Marshal
}
+ if app.config.XMLDecoder == nil {
+ app.config.XMLDecoder = xml.Unmarshal
+ }
if len(app.config.RequestMethods) == 0 {
app.config.RequestMethods = DefaultMethods
}
diff --git a/bind.go b/bind.go
index 5af83743a0..13d9d3675e 100644
--- a/bind.go
+++ b/bind.go
@@ -77,7 +77,16 @@ func (b *Bind) Custom(name string, dest any) error {
// Header binds the request header strings into the struct, map[string]string and map[string][]string.
func (b *Bind) Header(out any) error {
- if err := b.returnErr(binder.HeaderBinder.Bind(b.ctx.Request(), out)); err != nil {
+ bind := binder.GetFromThePool[*binder.HeaderBinding](&binder.HeaderBinderPool)
+ bind.EnableSplitting = b.ctx.App().config.EnableSplittingOnParsers
+
+ // Reset & put binder
+ defer func() {
+ bind.Reset()
+ binder.PutToThePool(&binder.HeaderBinderPool, bind)
+ }()
+
+ if err := b.returnErr(bind.Bind(b.ctx.Request(), out)); err != nil {
return err
}
@@ -86,7 +95,16 @@ func (b *Bind) Header(out any) error {
// RespHeader binds the response header strings into the struct, map[string]string and map[string][]string.
func (b *Bind) RespHeader(out any) error {
- if err := b.returnErr(binder.RespHeaderBinder.Bind(b.ctx.Response(), out)); err != nil {
+ bind := binder.GetFromThePool[*binder.RespHeaderBinding](&binder.RespHeaderBinderPool)
+ bind.EnableSplitting = b.ctx.App().config.EnableSplittingOnParsers
+
+ // Reset & put binder
+ defer func() {
+ bind.Reset()
+ binder.PutToThePool(&binder.RespHeaderBinderPool, bind)
+ }()
+
+ if err := b.returnErr(bind.Bind(b.ctx.Response(), out)); err != nil {
return err
}
@@ -96,7 +114,16 @@ func (b *Bind) RespHeader(out any) error {
// Cookie binds the request cookie strings into the struct, map[string]string and map[string][]string.
// NOTE: If your cookie is like key=val1,val2; they'll be binded as an slice if your map is map[string][]string. Else, it'll use last element of cookie.
func (b *Bind) Cookie(out any) error {
- if err := b.returnErr(binder.CookieBinder.Bind(b.ctx.RequestCtx(), out)); err != nil {
+ bind := binder.GetFromThePool[*binder.CookieBinding](&binder.CookieBinderPool)
+ bind.EnableSplitting = b.ctx.App().config.EnableSplittingOnParsers
+
+ // Reset & put binder
+ defer func() {
+ bind.Reset()
+ binder.PutToThePool(&binder.CookieBinderPool, bind)
+ }()
+
+ if err := b.returnErr(bind.Bind(&b.ctx.RequestCtx().Request, out)); err != nil {
return err
}
@@ -105,7 +132,16 @@ func (b *Bind) Cookie(out any) error {
// Query binds the query string into the struct, map[string]string and map[string][]string.
func (b *Bind) Query(out any) error {
- if err := b.returnErr(binder.QueryBinder.Bind(b.ctx.RequestCtx(), out)); err != nil {
+ bind := binder.GetFromThePool[*binder.QueryBinding](&binder.QueryBinderPool)
+ bind.EnableSplitting = b.ctx.App().config.EnableSplittingOnParsers
+
+ // Reset & put binder
+ defer func() {
+ bind.Reset()
+ binder.PutToThePool(&binder.QueryBinderPool, bind)
+ }()
+
+ if err := b.returnErr(bind.Bind(&b.ctx.RequestCtx().Request, out)); err != nil {
return err
}
@@ -114,7 +150,16 @@ func (b *Bind) Query(out any) error {
// JSON binds the body string into the struct.
func (b *Bind) JSON(out any) error {
- if err := b.returnErr(binder.JSONBinder.Bind(b.ctx.Body(), b.ctx.App().Config().JSONDecoder, out)); err != nil {
+ bind := binder.GetFromThePool[*binder.JSONBinding](&binder.JSONBinderPool)
+ bind.JSONDecoder = b.ctx.App().Config().JSONDecoder
+
+ // Reset & put binder
+ defer func() {
+ bind.Reset()
+ binder.PutToThePool(&binder.JSONBinderPool, bind)
+ }()
+
+ if err := b.returnErr(bind.Bind(b.ctx.Body(), out)); err != nil {
return err
}
@@ -123,7 +168,16 @@ func (b *Bind) JSON(out any) error {
// 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 {
+ bind := binder.GetFromThePool[*binder.CBORBinding](&binder.CBORBinderPool)
+ bind.CBORDecoder = b.ctx.App().Config().CBORDecoder
+
+ // Reset & put binder
+ defer func() {
+ bind.Reset()
+ binder.PutToThePool(&binder.CBORBinderPool, bind)
+ }()
+
+ if err := b.returnErr(bind.Bind(b.ctx.Body(), out)); err != nil {
return err
}
return b.validateStruct(out)
@@ -131,7 +185,16 @@ func (b *Bind) CBOR(out any) error {
// 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 {
+ bind := binder.GetFromThePool[*binder.XMLBinding](&binder.XMLBinderPool)
+ bind.XMLDecoder = b.ctx.App().config.XMLDecoder
+
+ // Reset & put binder
+ defer func() {
+ bind.Reset()
+ binder.PutToThePool(&binder.XMLBinderPool, bind)
+ }()
+
+ if err := b.returnErr(bind.Bind(b.ctx.Body(), out)); err != nil {
return err
}
@@ -139,8 +202,20 @@ func (b *Bind) XML(out any) error {
}
// Form binds the form into the struct, map[string]string and map[string][]string.
+// If Content-Type is "application/x-www-form-urlencoded" or "multipart/form-data", it will bind the form values.
+//
+// Binding multipart files is not supported yet.
func (b *Bind) Form(out any) error {
- if err := b.returnErr(binder.FormBinder.Bind(b.ctx.RequestCtx(), out)); err != nil {
+ bind := binder.GetFromThePool[*binder.FormBinding](&binder.FormBinderPool)
+ bind.EnableSplitting = b.ctx.App().config.EnableSplittingOnParsers
+
+ // Reset & put binder
+ defer func() {
+ bind.Reset()
+ binder.PutToThePool(&binder.FormBinderPool, bind)
+ }()
+
+ if err := b.returnErr(bind.Bind(&b.ctx.RequestCtx().Request, out)); err != nil {
return err
}
@@ -149,16 +224,14 @@ func (b *Bind) Form(out any) error {
// URI binds the route parameters into the struct, map[string]string and map[string][]string.
func (b *Bind) URI(out any) error {
- if err := b.returnErr(binder.URIBinder.Bind(b.ctx.Route().Params, b.ctx.Params, out)); err != nil {
- return err
- }
+ bind := binder.GetFromThePool[*binder.URIBinding](&binder.URIBinderPool)
- return b.validateStruct(out)
-}
+ // Reset & put binder
+ defer func() {
+ binder.PutToThePool(&binder.URIBinderPool, bind)
+ }()
-// MultipartForm binds the multipart form into the struct, map[string]string and map[string][]string.
-func (b *Bind) MultipartForm(out any) error {
- if err := b.returnErr(binder.FormBinder.BindMultipart(b.ctx.RequestCtx(), out)); err != nil {
+ if err := b.returnErr(bind.Bind(b.ctx.Route().Params, b.ctx.Params, out)); err != nil {
return err
}
@@ -193,10 +266,8 @@ func (b *Bind) Body(out any) error {
return b.XML(out)
case MIMEApplicationCBOR:
return b.CBOR(out)
- case MIMEApplicationForm:
+ case MIMEApplicationForm, MIMEMultipartForm:
return b.Form(out)
- case MIMEMultipartForm:
- return b.MultipartForm(out)
}
// No suitable content type found
diff --git a/bind_test.go b/bind_test.go
index 55d2dd75e9..52c9004c61 100644
--- a/bind_test.go
+++ b/bind_test.go
@@ -32,7 +32,9 @@ func Test_returnErr(t *testing.T) {
// go test -run Test_Bind_Query -v
func Test_Bind_Query(t *testing.T) {
t.Parallel()
- app := New()
+ app := New(Config{
+ EnableSplittingOnParsers: true,
+ })
c := app.AcquireCtx(&fasthttp.RequestCtx{})
type Query struct {
@@ -111,7 +113,9 @@ func Test_Bind_Query(t *testing.T) {
func Test_Bind_Query_Map(t *testing.T) {
t.Parallel()
- app := New()
+ app := New(Config{
+ EnableSplittingOnParsers: true,
+ })
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().SetBody([]byte(``))
@@ -318,13 +322,13 @@ func Test_Bind_Header(t *testing.T) {
c.Request().Header.Add("Hobby", "golang,fiber")
q := new(Header)
require.NoError(t, c.Bind().Header(q))
- require.Len(t, q.Hobby, 2)
+ require.Len(t, q.Hobby, 1)
c.Request().Header.Del("hobby")
c.Request().Header.Add("Hobby", "golang,fiber,go")
q = new(Header)
require.NoError(t, c.Bind().Header(q))
- require.Len(t, q.Hobby, 3)
+ require.Len(t, q.Hobby, 1)
empty := new(Header)
c.Request().Header.Del("hobby")
@@ -357,7 +361,7 @@ func Test_Bind_Header(t *testing.T) {
require.Equal(t, "go,fiber", h2.Hobby)
require.True(t, h2.Bool)
require.Equal(t, "Jane Doe", h2.Name) // check value get overwritten
- require.Equal(t, []string{"milo", "coke", "pepsi"}, h2.FavouriteDrinks)
+ require.Equal(t, []string{"milo,coke,pepsi"}, h2.FavouriteDrinks)
var nilSlice []string
require.Equal(t, nilSlice, h2.Empty)
require.Equal(t, []string{""}, h2.Alloc)
@@ -386,13 +390,13 @@ func Test_Bind_Header_Map(t *testing.T) {
c.Request().Header.Add("Hobby", "golang,fiber")
q := make(map[string][]string, 0)
require.NoError(t, c.Bind().Header(&q))
- require.Len(t, q["Hobby"], 2)
+ require.Len(t, q["Hobby"], 1)
c.Request().Header.Del("hobby")
c.Request().Header.Add("Hobby", "golang,fiber,go")
q = make(map[string][]string, 0)
require.NoError(t, c.Bind().Header(&q))
- require.Len(t, q["Hobby"], 3)
+ require.Len(t, q["Hobby"], 1)
empty := make(map[string][]string, 0)
c.Request().Header.Del("hobby")
@@ -543,7 +547,9 @@ func Test_Bind_Header_Schema(t *testing.T) {
// go test -run Test_Bind_Resp_Header -v
func Test_Bind_RespHeader(t *testing.T) {
t.Parallel()
- app := New()
+ app := New(Config{
+ EnableSplittingOnParsers: true,
+ })
c := app.AcquireCtx(&fasthttp.RequestCtx{})
type Header struct {
@@ -627,13 +633,13 @@ func Test_Bind_RespHeader_Map(t *testing.T) {
c.Response().Header.Add("Hobby", "golang,fiber")
q := make(map[string][]string, 0)
require.NoError(t, c.Bind().RespHeader(&q))
- require.Len(t, q["Hobby"], 2)
+ require.Len(t, q["Hobby"], 1)
c.Response().Header.Del("hobby")
c.Response().Header.Add("Hobby", "golang,fiber,go")
q = make(map[string][]string, 0)
require.NoError(t, c.Bind().RespHeader(&q))
- require.Len(t, q["Hobby"], 3)
+ require.Len(t, q["Hobby"], 1)
empty := make(map[string][]string, 0)
c.Response().Header.Del("hobby")
@@ -751,7 +757,9 @@ func Benchmark_Bind_Query_WithParseParam(b *testing.B) {
func Benchmark_Bind_Query_Comma(b *testing.B) {
var err error
- app := New()
+ app := New(Config{
+ EnableSplittingOnParsers: true,
+ })
c := app.AcquireCtx(&fasthttp.RequestCtx{})
type Query struct {
@@ -1341,7 +1349,9 @@ func Benchmark_Bind_URI_Map(b *testing.B) {
func Test_Bind_Cookie(t *testing.T) {
t.Parallel()
- app := New()
+ app := New(Config{
+ EnableSplittingOnParsers: true,
+ })
c := app.AcquireCtx(&fasthttp.RequestCtx{})
type Cookie struct {
@@ -1414,7 +1424,9 @@ func Test_Bind_Cookie(t *testing.T) {
func Test_Bind_Cookie_Map(t *testing.T) {
t.Parallel()
- app := New()
+ app := New(Config{
+ EnableSplittingOnParsers: true,
+ })
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().SetBody([]byte(``))
diff --git a/binder/binder.go b/binder/binder.go
index bb3fc2b394..06c7c926a5 100644
--- a/binder/binder.go
+++ b/binder/binder.go
@@ -2,6 +2,7 @@ package binder
import (
"errors"
+ "sync"
)
// Binder errors
@@ -10,15 +11,69 @@ var (
ErrMapNotConvertable = errors.New("binder: map is not convertable to map[string]string or map[string][]string")
)
-// Init default binders for Fiber
-var (
- HeaderBinder = &headerBinding{}
- RespHeaderBinder = &respHeaderBinding{}
- CookieBinder = &cookieBinding{}
- QueryBinder = &queryBinding{}
- FormBinder = &formBinding{}
- URIBinder = &uriBinding{}
- XMLBinder = &xmlBinding{}
- JSONBinder = &jsonBinding{}
- CBORBinder = &cborBinding{}
-)
+var HeaderBinderPool = sync.Pool{
+ New: func() any {
+ return &HeaderBinding{}
+ },
+}
+
+var RespHeaderBinderPool = sync.Pool{
+ New: func() any {
+ return &RespHeaderBinding{}
+ },
+}
+
+var CookieBinderPool = sync.Pool{
+ New: func() any {
+ return &CookieBinding{}
+ },
+}
+
+var QueryBinderPool = sync.Pool{
+ New: func() any {
+ return &QueryBinding{}
+ },
+}
+
+var FormBinderPool = sync.Pool{
+ New: func() any {
+ return &FormBinding{}
+ },
+}
+
+var URIBinderPool = sync.Pool{
+ New: func() any {
+ return &URIBinding{}
+ },
+}
+
+var XMLBinderPool = sync.Pool{
+ New: func() any {
+ return &XMLBinding{}
+ },
+}
+
+var JSONBinderPool = sync.Pool{
+ New: func() any {
+ return &JSONBinding{}
+ },
+}
+
+var CBORBinderPool = sync.Pool{
+ New: func() any {
+ return &CBORBinding{}
+ },
+}
+
+func GetFromThePool[T any](pool *sync.Pool) T {
+ binder, ok := pool.Get().(T)
+ if !ok {
+ panic(errors.New("failed to type-assert to T"))
+ }
+
+ return binder
+}
+
+func PutToThePool[T any](pool *sync.Pool, binder T) {
+ pool.Put(binder)
+}
diff --git a/binder/binder_test.go b/binder/binder_test.go
new file mode 100644
index 0000000000..d078ed02c6
--- /dev/null
+++ b/binder/binder_test.go
@@ -0,0 +1,28 @@
+package binder
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func Test_GetAndPutToThePool(t *testing.T) {
+ t.Parallel()
+
+ // Panics in case we get from another pool
+ require.Panics(t, func() {
+ _ = GetFromThePool[*HeaderBinding](&CookieBinderPool)
+ })
+
+ // We get from the pool
+ binder := GetFromThePool[*HeaderBinding](&HeaderBinderPool)
+ PutToThePool(&HeaderBinderPool, binder)
+
+ _ = GetFromThePool[*RespHeaderBinding](&RespHeaderBinderPool)
+ _ = GetFromThePool[*QueryBinding](&QueryBinderPool)
+ _ = GetFromThePool[*FormBinding](&FormBinderPool)
+ _ = GetFromThePool[*URIBinding](&URIBinderPool)
+ _ = GetFromThePool[*XMLBinding](&XMLBinderPool)
+ _ = GetFromThePool[*JSONBinding](&JSONBinderPool)
+ _ = GetFromThePool[*CBORBinding](&CBORBinderPool)
+}
diff --git a/binder/cbor.go b/binder/cbor.go
index 6f47893531..8b1d0d4291 100644
--- a/binder/cbor.go
+++ b/binder/cbor.go
@@ -4,15 +4,22 @@ import (
"github.com/gofiber/utils/v2"
)
-// cborBinding is the CBOR binder for CBOR request body.
-type cborBinding struct{}
+// CBORBinding is the CBOR binder for CBOR request body.
+type CBORBinding struct {
+ CBORDecoder utils.CBORUnmarshal
+}
// Name returns the binding name.
-func (*cborBinding) Name() string {
+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)
+func (b *CBORBinding) Bind(body []byte, out any) error {
+ return b.CBORDecoder(body, out)
+}
+
+// Reset resets the CBORBinding binder.
+func (b *CBORBinding) Reset() {
+ b.CBORDecoder = nil
}
diff --git a/binder/cbor_test.go b/binder/cbor_test.go
new file mode 100644
index 0000000000..16c24cbbca
--- /dev/null
+++ b/binder/cbor_test.go
@@ -0,0 +1,92 @@
+package binder
+
+import (
+ "testing"
+
+ "github.com/fxamacker/cbor/v2"
+ "github.com/stretchr/testify/require"
+)
+
+func Test_CBORBinder_Bind(t *testing.T) {
+ t.Parallel()
+
+ b := &CBORBinding{
+ CBORDecoder: cbor.Unmarshal,
+ }
+ require.Equal(t, "cbor", b.Name())
+
+ type Post struct {
+ Title string `cbor:"title"`
+ }
+
+ type User struct {
+ Name string `cbor:"name"`
+ Posts []Post `cbor:"posts"`
+ Names []string `cbor:"names"`
+ Age int `cbor:"age"`
+ }
+ var user User
+
+ wantedUser := User{
+ Name: "john",
+ Names: []string{
+ "john",
+ "doe",
+ },
+ Age: 42,
+ Posts: []Post{
+ {Title: "post1"},
+ {Title: "post2"},
+ {Title: "post3"},
+ },
+ }
+
+ body, err := cbor.Marshal(wantedUser)
+ require.NoError(t, err)
+
+ err = b.Bind(body, &user)
+
+ require.NoError(t, err)
+ require.Equal(t, "john", user.Name)
+ require.Equal(t, 42, user.Age)
+ require.Len(t, user.Posts, 3)
+ require.Equal(t, "post1", user.Posts[0].Title)
+ require.Equal(t, "post2", user.Posts[1].Title)
+ require.Equal(t, "post3", user.Posts[2].Title)
+ require.Contains(t, user.Names, "john")
+ require.Contains(t, user.Names, "doe")
+
+ b.Reset()
+ require.Nil(t, b.CBORDecoder)
+}
+
+func Benchmark_CBORBinder_Bind(b *testing.B) {
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ binder := &CBORBinding{
+ CBORDecoder: cbor.Unmarshal,
+ }
+
+ type User struct {
+ Name string `cbor:"name"`
+ Age int `cbor:"age"`
+ }
+
+ var user User
+ wantedUser := User{
+ Name: "john",
+ Age: 42,
+ }
+
+ body, err := cbor.Marshal(wantedUser)
+ require.NoError(b, err)
+
+ for i := 0; i < b.N; i++ {
+ err = binder.Bind(body, &user)
+ }
+
+ require.NoError(b, err)
+ require.Equal(b, "john", user.Name)
+ require.Equal(b, 42, user.Age)
+}
diff --git a/binder/cookie.go b/binder/cookie.go
index 62271c8e38..230794f45a 100644
--- a/binder/cookie.go
+++ b/binder/cookie.go
@@ -8,20 +8,22 @@ import (
"github.com/valyala/fasthttp"
)
-// cookieBinding is the cookie binder for cookie request body.
-type cookieBinding struct{}
+// CookieBinding is the cookie binder for cookie request body.
+type CookieBinding struct {
+ EnableSplitting bool
+}
// Name returns the binding name.
-func (*cookieBinding) Name() string {
+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 {
+func (b *CookieBinding) Bind(req *fasthttp.Request, out any) error {
data := make(map[string][]string)
var err error
- reqCtx.Request.Header.VisitAllCookie(func(key, val []byte) {
+ req.Header.VisitAllCookie(func(key, val []byte) {
if err != nil {
return
}
@@ -29,7 +31,7 @@ func (b *cookieBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error {
k := utils.UnsafeString(key)
v := utils.UnsafeString(val)
- if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
+ if b.EnableSplitting && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
values := strings.Split(v, ",")
for i := 0; i < len(values); i++ {
data[k] = append(data[k], values[i])
@@ -45,3 +47,8 @@ func (b *cookieBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error {
return parse(b.Name(), out, data)
}
+
+// Reset resets the CookieBinding binder.
+func (b *CookieBinding) Reset() {
+ b.EnableSplitting = false
+}
diff --git a/binder/cookie_test.go b/binder/cookie_test.go
new file mode 100644
index 0000000000..bca316c9fe
--- /dev/null
+++ b/binder/cookie_test.go
@@ -0,0 +1,90 @@
+package binder
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "github.com/valyala/fasthttp"
+)
+
+func Test_CookieBinder_Bind(t *testing.T) {
+ t.Parallel()
+
+ b := &CookieBinding{
+ EnableSplitting: true,
+ }
+ require.Equal(t, "cookie", b.Name())
+
+ type Post struct {
+ Title string `form:"title"`
+ }
+
+ type User struct {
+ Name string `form:"name"`
+ Names []string `form:"names"`
+ Posts []Post `form:"posts"`
+ Age int `form:"age"`
+ }
+ var user User
+
+ req := fasthttp.AcquireRequest()
+
+ req.Header.SetCookie("name", "john")
+ req.Header.SetCookie("names", "john,doe")
+ req.Header.SetCookie("age", "42")
+
+ t.Cleanup(func() {
+ fasthttp.ReleaseRequest(req)
+ })
+
+ err := b.Bind(req, &user)
+
+ require.NoError(t, err)
+ require.Equal(t, "john", user.Name)
+ require.Equal(t, 42, user.Age)
+ require.Contains(t, user.Names, "john")
+ require.Contains(t, user.Names, "doe")
+
+ b.Reset()
+ require.False(t, b.EnableSplitting)
+}
+
+func Benchmark_CookieBinder_Bind(b *testing.B) {
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ binder := &CookieBinding{
+ EnableSplitting: true,
+ }
+
+ type User struct {
+ Name string `query:"name"`
+ Posts []string `query:"posts"`
+ Age int `query:"age"`
+ }
+ var user User
+
+ req := fasthttp.AcquireRequest()
+ b.Cleanup(func() {
+ fasthttp.ReleaseRequest(req)
+ })
+
+ req.Header.SetCookie("name", "john")
+ req.Header.SetCookie("age", "42")
+ req.Header.SetCookie("posts", "post1,post2,post3")
+
+ b.ResetTimer()
+
+ var err error
+ for i := 0; i < b.N; i++ {
+ err = binder.Bind(req, &user)
+ }
+
+ require.NoError(b, err)
+ require.Equal(b, "john", user.Name)
+ require.Equal(b, 42, user.Age)
+ require.Len(b, user.Posts, 3)
+ require.Contains(b, user.Posts, "post1")
+ require.Contains(b, user.Posts, "post2")
+ require.Contains(b, user.Posts, "post3")
+}
diff --git a/binder/form.go b/binder/form.go
index e0f1acd302..7ab0b1b258 100644
--- a/binder/form.go
+++ b/binder/form.go
@@ -8,20 +8,29 @@ import (
"github.com/valyala/fasthttp"
)
-// formBinding is the form binder for form request body.
-type formBinding struct{}
+const MIMEMultipartForm string = "multipart/form-data"
+
+// FormBinding is the form binder for form request body.
+type FormBinding struct {
+ EnableSplitting bool
+}
// Name returns the binding name.
-func (*formBinding) Name() string {
+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 {
+func (b *FormBinding) Bind(req *fasthttp.Request, out any) error {
data := make(map[string][]string)
var err error
- reqCtx.PostArgs().VisitAll(func(key, val []byte) {
+ // Handle multipart form
+ if FilterFlags(utils.UnsafeString(req.Header.ContentType())) == MIMEMultipartForm {
+ return b.bindMultipart(req, out)
+ }
+
+ req.PostArgs().VisitAll(func(key, val []byte) {
if err != nil {
return
}
@@ -33,7 +42,7 @@ func (b *formBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error {
k, err = parseParamSquareBrackets(k)
}
- if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
+ if b.EnableSplitting && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
values := strings.Split(v, ",")
for i := 0; i < len(values); i++ {
data[k] = append(data[k], values[i])
@@ -50,12 +59,17 @@ 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()
+// bindMultipart parses the request body and returns the result.
+func (b *FormBinding) bindMultipart(req *fasthttp.Request, out any) error {
+ data, err := req.MultipartForm()
if err != nil {
return err
}
return parse(b.Name(), out, data.Value)
}
+
+// Reset resets the FormBinding binder.
+func (b *FormBinding) Reset() {
+ b.EnableSplitting = false
+}
diff --git a/binder/form_test.go b/binder/form_test.go
new file mode 100644
index 0000000000..c3c52c73fd
--- /dev/null
+++ b/binder/form_test.go
@@ -0,0 +1,174 @@
+package binder
+
+import (
+ "bytes"
+ "mime/multipart"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "github.com/valyala/fasthttp"
+)
+
+func Test_FormBinder_Bind(t *testing.T) {
+ t.Parallel()
+
+ b := &FormBinding{
+ EnableSplitting: true,
+ }
+ require.Equal(t, "form", b.Name())
+
+ type Post struct {
+ Title string `form:"title"`
+ }
+
+ type User struct {
+ Name string `form:"name"`
+ Names []string `form:"names"`
+ Posts []Post `form:"posts"`
+ Age int `form:"age"`
+ }
+ var user User
+
+ req := fasthttp.AcquireRequest()
+ req.SetBodyString("name=john&names=john,doe&age=42&posts[0][title]=post1&posts[1][title]=post2&posts[2][title]=post3")
+ req.Header.SetContentType("application/x-www-form-urlencoded")
+
+ t.Cleanup(func() {
+ fasthttp.ReleaseRequest(req)
+ })
+
+ err := b.Bind(req, &user)
+
+ require.NoError(t, err)
+ require.Equal(t, "john", user.Name)
+ require.Equal(t, 42, user.Age)
+ require.Len(t, user.Posts, 3)
+ require.Equal(t, "post1", user.Posts[0].Title)
+ require.Equal(t, "post2", user.Posts[1].Title)
+ require.Equal(t, "post3", user.Posts[2].Title)
+ require.Contains(t, user.Names, "john")
+ require.Contains(t, user.Names, "doe")
+
+ b.Reset()
+ require.False(t, b.EnableSplitting)
+}
+
+func Benchmark_FormBinder_Bind(b *testing.B) {
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ binder := &QueryBinding{
+ EnableSplitting: true,
+ }
+
+ type User struct {
+ Name string `query:"name"`
+ Posts []string `query:"posts"`
+ Age int `query:"age"`
+ }
+ var user User
+
+ req := fasthttp.AcquireRequest()
+ req.URI().SetQueryString("name=john&age=42&posts=post1,post2,post3")
+ req.Header.SetContentType("application/x-www-form-urlencoded")
+
+ b.ResetTimer()
+
+ var err error
+ for i := 0; i < b.N; i++ {
+ err = binder.Bind(req, &user)
+ }
+
+ require.NoError(b, err)
+ require.Equal(b, "john", user.Name)
+ require.Equal(b, 42, user.Age)
+ require.Len(b, user.Posts, 3)
+}
+
+func Test_FormBinder_BindMultipart(t *testing.T) {
+ t.Parallel()
+
+ b := &FormBinding{
+ EnableSplitting: true,
+ }
+ require.Equal(t, "form", b.Name())
+
+ type User struct {
+ Name string `form:"name"`
+ Names []string `form:"names"`
+ Age int `form:"age"`
+ }
+ var user User
+
+ req := fasthttp.AcquireRequest()
+
+ buf := &bytes.Buffer{}
+ mw := multipart.NewWriter(buf)
+
+ require.NoError(t, mw.WriteField("name", "john"))
+ require.NoError(t, mw.WriteField("names", "john"))
+ require.NoError(t, mw.WriteField("names", "doe"))
+ require.NoError(t, mw.WriteField("age", "42"))
+ require.NoError(t, mw.Close())
+
+ req.Header.SetContentType(mw.FormDataContentType())
+ req.SetBody(buf.Bytes())
+
+ t.Cleanup(func() {
+ fasthttp.ReleaseRequest(req)
+ })
+
+ err := b.Bind(req, &user)
+
+ require.NoError(t, err)
+ require.Equal(t, "john", user.Name)
+ require.Equal(t, 42, user.Age)
+ require.Contains(t, user.Names, "john")
+ require.Contains(t, user.Names, "doe")
+}
+
+func Benchmark_FormBinder_BindMultipart(b *testing.B) {
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ binder := &FormBinding{
+ EnableSplitting: true,
+ }
+
+ type User struct {
+ Name string `form:"name"`
+ Posts []string `form:"posts"`
+ Age int `form:"age"`
+ }
+ var user User
+
+ req := fasthttp.AcquireRequest()
+ b.Cleanup(func() {
+ fasthttp.ReleaseRequest(req)
+ })
+
+ buf := &bytes.Buffer{}
+ mw := multipart.NewWriter(buf)
+
+ require.NoError(b, mw.WriteField("name", "john"))
+ require.NoError(b, mw.WriteField("age", "42"))
+ require.NoError(b, mw.WriteField("posts", "post1"))
+ require.NoError(b, mw.WriteField("posts", "post2"))
+ require.NoError(b, mw.WriteField("posts", "post3"))
+ require.NoError(b, mw.Close())
+
+ req.Header.SetContentType(mw.FormDataContentType())
+ req.SetBody(buf.Bytes())
+
+ b.ResetTimer()
+
+ var err error
+ for i := 0; i < b.N; i++ {
+ err = binder.Bind(req, &user)
+ }
+
+ require.NoError(b, err)
+ require.Equal(b, "john", user.Name)
+ require.Equal(b, 42, user.Age)
+ require.Len(b, user.Posts, 3)
+}
diff --git a/binder/header.go b/binder/header.go
index 258a0b2229..b04ce9add3 100644
--- a/binder/header.go
+++ b/binder/header.go
@@ -8,22 +8,24 @@ import (
"github.com/valyala/fasthttp"
)
-// headerBinding is the header binder for header request body.
-type headerBinding struct{}
+// v is the header binder for header request body.
+type HeaderBinding struct {
+ EnableSplitting bool
+}
// Name returns the binding name.
-func (*headerBinding) Name() string {
+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 {
+func (b *HeaderBinding) Bind(req *fasthttp.Request, out any) error {
data := make(map[string][]string)
req.Header.VisitAll(func(key, val []byte) {
k := utils.UnsafeString(key)
v := utils.UnsafeString(val)
- if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
+ if b.EnableSplitting && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
values := strings.Split(v, ",")
for i := 0; i < len(values); i++ {
data[k] = append(data[k], values[i])
@@ -35,3 +37,8 @@ func (b *headerBinding) Bind(req *fasthttp.Request, out any) error {
return parse(b.Name(), out, data)
}
+
+// Reset resets the HeaderBinding binder.
+func (b *HeaderBinding) Reset() {
+ b.EnableSplitting = false
+}
diff --git a/binder/header_test.go b/binder/header_test.go
new file mode 100644
index 0000000000..bdef8680ac
--- /dev/null
+++ b/binder/header_test.go
@@ -0,0 +1,88 @@
+package binder
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "github.com/valyala/fasthttp"
+)
+
+func Test_HeaderBinder_Bind(t *testing.T) {
+ t.Parallel()
+
+ b := &HeaderBinding{
+ EnableSplitting: true,
+ }
+ require.Equal(t, "header", b.Name())
+
+ type User struct {
+ Name string `header:"Name"`
+ Names []string `header:"Names"`
+ Posts []string `header:"Posts"`
+ Age int `header:"Age"`
+ }
+ var user User
+
+ req := fasthttp.AcquireRequest()
+ req.Header.Set("name", "john")
+ req.Header.Set("names", "john,doe")
+ req.Header.Set("age", "42")
+ req.Header.Set("posts", "post1,post2,post3")
+
+ t.Cleanup(func() {
+ fasthttp.ReleaseRequest(req)
+ })
+
+ err := b.Bind(req, &user)
+
+ require.NoError(t, err)
+ require.Equal(t, "john", user.Name)
+ require.Equal(t, 42, user.Age)
+ require.Len(t, user.Posts, 3)
+ require.Equal(t, "post1", user.Posts[0])
+ require.Equal(t, "post2", user.Posts[1])
+ require.Equal(t, "post3", user.Posts[2])
+ require.Contains(t, user.Names, "john")
+ require.Contains(t, user.Names, "doe")
+
+ b.Reset()
+ require.False(t, b.EnableSplitting)
+}
+
+func Benchmark_HeaderBinder_Bind(b *testing.B) {
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ binder := &HeaderBinding{
+ EnableSplitting: true,
+ }
+
+ type User struct {
+ Name string `header:"Name"`
+ Posts []string `header:"Posts"`
+ Age int `header:"Age"`
+ }
+ var user User
+
+ req := fasthttp.AcquireRequest()
+ b.Cleanup(func() {
+ fasthttp.ReleaseRequest(req)
+ })
+
+ req.Header.Set("name", "john")
+ req.Header.Set("age", "42")
+ req.Header.Set("posts", "post1,post2,post3")
+
+ var err error
+ for i := 0; i < b.N; i++ {
+ err = binder.Bind(req, &user)
+ }
+
+ require.NoError(b, err)
+ require.Equal(b, "john", user.Name)
+ require.Equal(b, 42, user.Age)
+ require.Len(b, user.Posts, 3)
+ require.Contains(b, user.Posts, "post1")
+ require.Contains(b, user.Posts, "post2")
+ require.Contains(b, user.Posts, "post3")
+}
diff --git a/binder/json.go b/binder/json.go
index 7889aee8a2..a6a904b550 100644
--- a/binder/json.go
+++ b/binder/json.go
@@ -4,15 +4,22 @@ import (
"github.com/gofiber/utils/v2"
)
-// jsonBinding is the JSON binder for JSON request body.
-type jsonBinding struct{}
+// JSONBinding is the JSON binder for JSON request body.
+type JSONBinding struct {
+ JSONDecoder utils.JSONUnmarshal
+}
// Name returns the binding name.
-func (*jsonBinding) Name() string {
+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)
+func (b *JSONBinding) Bind(body []byte, out any) error {
+ return b.JSONDecoder(body, out)
+}
+
+// Reset resets the JSONBinding binder.
+func (b *JSONBinding) Reset() {
+ b.JSONDecoder = nil
}
diff --git a/binder/json_test.go b/binder/json_test.go
new file mode 100644
index 0000000000..00718fdf26
--- /dev/null
+++ b/binder/json_test.go
@@ -0,0 +1,69 @@
+package binder
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func Test_JSON_Binding_Bind(t *testing.T) {
+ t.Parallel()
+
+ b := &JSONBinding{
+ JSONDecoder: json.Unmarshal,
+ }
+ require.Equal(t, "json", b.Name())
+
+ type Post struct {
+ Title string `json:"title"`
+ }
+
+ type User struct {
+ Name string `json:"name"`
+ Posts []Post `json:"posts"`
+ Age int `json:"age"`
+ }
+ var user User
+
+ err := b.Bind([]byte(`{"name":"john","age":42,"posts":[{"title":"post1"},{"title":"post2"},{"title":"post3"}]}`), &user)
+ require.NoError(t, err)
+ require.Equal(t, "john", user.Name)
+ require.Equal(t, 42, user.Age)
+ require.Len(t, user.Posts, 3)
+ require.Equal(t, "post1", user.Posts[0].Title)
+ require.Equal(t, "post2", user.Posts[1].Title)
+ require.Equal(t, "post3", user.Posts[2].Title)
+
+ b.Reset()
+ require.Nil(t, b.JSONDecoder)
+}
+
+func Benchmark_JSON_Binding_Bind(b *testing.B) {
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ binder := &JSONBinding{
+ JSONDecoder: json.Unmarshal,
+ }
+
+ type User struct {
+ Name string `json:"name"`
+ Posts []string `json:"posts"`
+ Age int `json:"age"`
+ }
+
+ var user User
+ var err error
+ for i := 0; i < b.N; i++ {
+ err = binder.Bind([]byte(`{"name":"john","age":42,"posts":["post1","post2","post3"]}`), &user)
+ }
+
+ require.NoError(b, err)
+ require.Equal(b, "john", user.Name)
+ require.Equal(b, 42, user.Age)
+ require.Len(b, user.Posts, 3)
+ require.Equal(b, "post1", user.Posts[0])
+ require.Equal(b, "post2", user.Posts[1])
+ require.Equal(b, "post3", user.Posts[2])
+}
diff --git a/binder/mapping.go b/binder/mapping.go
index 055345fe26..d8b692f7e4 100644
--- a/binder/mapping.go
+++ b/binder/mapping.go
@@ -32,7 +32,7 @@ var (
// decoderPoolMap helps to improve binders
decoderPoolMap = map[string]*sync.Pool{}
// tags is used to classify parser's pool
- tags = []string{HeaderBinder.Name(), RespHeaderBinder.Name(), CookieBinder.Name(), QueryBinder.Name(), FormBinder.Name(), URIBinder.Name()}
+ tags = []string{"header", "respHeader", "cookie", "query", "form", "uri"}
)
// SetParserDecoder allow globally change the option of form decoder, update decoderPool
@@ -107,8 +107,9 @@ func parseToStruct(aliasTag string, out any, data map[string][]string) error {
func parseToMap(ptr any, data map[string][]string) error {
elem := reflect.TypeOf(ptr).Elem()
- // map[string][]string
- if elem.Kind() == reflect.Slice {
+ //nolint:exhaustive // it's not necessary to check all types
+ switch elem.Kind() {
+ case reflect.Slice:
newMap, ok := ptr.(map[string][]string)
if !ok {
return ErrMapNotConvertable
@@ -117,18 +118,20 @@ func parseToMap(ptr any, data map[string][]string) error {
for k, v := range data {
newMap[k] = v
}
+ case reflect.String, reflect.Interface:
+ newMap, ok := ptr.(map[string]string)
+ if !ok {
+ return ErrMapNotConvertable
+ }
- return nil
- }
-
- // map[string]string
- newMap, ok := ptr.(map[string]string)
- if !ok {
- return ErrMapNotConvertable
- }
+ for k, v := range data {
+ if len(v) == 0 {
+ newMap[k] = ""
+ continue
+ }
- for k, v := range data {
- newMap[k] = v[len(v)-1]
+ newMap[k] = v[len(v)-1]
+ }
}
return nil
@@ -223,7 +226,7 @@ func equalFieldType(out any, kind reflect.Kind, key string) bool {
continue
}
// Get tag from field if exist
- inputFieldName := typeField.Tag.Get(QueryBinder.Name())
+ inputFieldName := typeField.Tag.Get("query") // Name of query binder
if inputFieldName == "" {
inputFieldName = typeField.Name
} else {
diff --git a/binder/mapping_test.go b/binder/mapping_test.go
index e6fc8146f7..75cdc78305 100644
--- a/binder/mapping_test.go
+++ b/binder/mapping_test.go
@@ -29,6 +29,21 @@ func Test_EqualFieldType(t *testing.T) {
require.True(t, equalFieldType(&user, reflect.String, "Address"))
require.True(t, equalFieldType(&user, reflect.Int, "AGE"))
require.True(t, equalFieldType(&user, reflect.Int, "age"))
+
+ var user2 struct {
+ User struct {
+ Name string
+ Address string `query:"address"`
+ Age int `query:"AGE"`
+ } `query:"user"`
+ }
+
+ require.True(t, equalFieldType(&user2, reflect.String, "user.name"))
+ require.True(t, equalFieldType(&user2, reflect.String, "user.Name"))
+ require.True(t, equalFieldType(&user2, reflect.String, "user.address"))
+ require.True(t, equalFieldType(&user2, reflect.String, "user.Address"))
+ require.True(t, equalFieldType(&user2, reflect.Int, "user.AGE"))
+ require.True(t, equalFieldType(&user2, reflect.Int, "user.age"))
}
func Test_ParseParamSquareBrackets(t *testing.T) {
@@ -97,3 +112,68 @@ func Test_ParseParamSquareBrackets(t *testing.T) {
})
}
}
+
+func Test_parseToMap(t *testing.T) {
+ inputMap := map[string][]string{
+ "key1": {"value1", "value2"},
+ "key2": {"value3"},
+ "key3": {"value4"},
+ }
+
+ // Test map[string]string
+ m := make(map[string]string)
+ err := parseToMap(m, inputMap)
+ require.NoError(t, err)
+
+ require.Equal(t, "value2", m["key1"])
+ require.Equal(t, "value3", m["key2"])
+ require.Equal(t, "value4", m["key3"])
+
+ // Test map[string][]string
+ m2 := make(map[string][]string)
+ err = parseToMap(m2, inputMap)
+ require.NoError(t, err)
+
+ require.Len(t, m2["key1"], 2)
+ require.Contains(t, m2["key1"], "value1")
+ require.Contains(t, m2["key1"], "value2")
+ require.Len(t, m2["key2"], 1)
+ require.Len(t, m2["key3"], 1)
+
+ // Test map[string]any
+ m3 := make(map[string]any)
+ err = parseToMap(m3, inputMap)
+ require.ErrorIs(t, err, ErrMapNotConvertable)
+}
+
+func Test_FilterFlags(t *testing.T) {
+ tests := []struct {
+ input string
+ expected string
+ }{
+ {
+ input: "text/javascript; charset=utf-8",
+ expected: "text/javascript",
+ },
+ {
+ input: "text/javascript",
+ expected: "text/javascript",
+ },
+
+ {
+ input: "text/javascript; charset=utf-8; foo=bar",
+ expected: "text/javascript",
+ },
+ {
+ input: "text/javascript charset=utf-8",
+ expected: "text/javascript",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.input, func(t *testing.T) {
+ result := FilterFlags(tt.input)
+ require.Equal(t, tt.expected, result)
+ })
+ }
+}
diff --git a/binder/query.go b/binder/query.go
index 8f029d30c4..9ee500ba63 100644
--- a/binder/query.go
+++ b/binder/query.go
@@ -8,20 +8,22 @@ import (
"github.com/valyala/fasthttp"
)
-// queryBinding is the query binder for query request body.
-type queryBinding struct{}
+// QueryBinding is the query binder for query request body.
+type QueryBinding struct {
+ EnableSplitting bool
+}
// Name returns the binding name.
-func (*queryBinding) Name() string {
+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 {
+func (b *QueryBinding) Bind(reqCtx *fasthttp.Request, out any) error {
data := make(map[string][]string)
var err error
- reqCtx.QueryArgs().VisitAll(func(key, val []byte) {
+ reqCtx.URI().QueryArgs().VisitAll(func(key, val []byte) {
if err != nil {
return
}
@@ -33,7 +35,7 @@ func (b *queryBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error {
k, err = parseParamSquareBrackets(k)
}
- if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
+ if b.EnableSplitting && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
values := strings.Split(v, ",")
for i := 0; i < len(values); i++ {
data[k] = append(data[k], values[i])
@@ -49,3 +51,8 @@ func (b *queryBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error {
return parse(b.Name(), out, data)
}
+
+// Reset resets the QueryBinding binder.
+func (b *QueryBinding) Reset() {
+ b.EnableSplitting = false
+}
diff --git a/binder/query_test.go b/binder/query_test.go
new file mode 100644
index 0000000000..0d457e5795
--- /dev/null
+++ b/binder/query_test.go
@@ -0,0 +1,87 @@
+package binder
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "github.com/valyala/fasthttp"
+)
+
+func Test_QueryBinder_Bind(t *testing.T) {
+ t.Parallel()
+
+ b := &QueryBinding{
+ EnableSplitting: true,
+ }
+ require.Equal(t, "query", b.Name())
+
+ type Post struct {
+ Title string `query:"title"`
+ }
+
+ type User struct {
+ Name string `query:"name"`
+ Names []string `query:"names"`
+ Posts []Post `query:"posts"`
+ Age int `query:"age"`
+ }
+ var user User
+
+ req := fasthttp.AcquireRequest()
+ req.URI().SetQueryString("name=john&names=john,doe&age=42&posts[0][title]=post1&posts[1][title]=post2&posts[2][title]=post3")
+
+ t.Cleanup(func() {
+ fasthttp.ReleaseRequest(req)
+ })
+
+ err := b.Bind(req, &user)
+
+ require.NoError(t, err)
+ require.Equal(t, "john", user.Name)
+ require.Equal(t, 42, user.Age)
+ require.Len(t, user.Posts, 3)
+ require.Equal(t, "post1", user.Posts[0].Title)
+ require.Equal(t, "post2", user.Posts[1].Title)
+ require.Equal(t, "post3", user.Posts[2].Title)
+ require.Contains(t, user.Names, "john")
+ require.Contains(t, user.Names, "doe")
+
+ b.Reset()
+ require.False(t, b.EnableSplitting)
+}
+
+func Benchmark_QueryBinder_Bind(b *testing.B) {
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ binder := &QueryBinding{
+ EnableSplitting: true,
+ }
+
+ type User struct {
+ Name string `query:"name"`
+ Posts []string `query:"posts"`
+ Age int `query:"age"`
+ }
+ var user User
+
+ req := fasthttp.AcquireRequest()
+ b.Cleanup(func() {
+ fasthttp.ReleaseRequest(req)
+ })
+
+ req.URI().SetQueryString("name=john&age=42&posts=post1,post2,post3")
+
+ var err error
+ for i := 0; i < b.N; i++ {
+ err = binder.Bind(req, &user)
+ }
+
+ require.NoError(b, err)
+ require.Equal(b, "john", user.Name)
+ require.Equal(b, 42, user.Age)
+ require.Len(b, user.Posts, 3)
+ require.Contains(b, user.Posts, "post1")
+ require.Contains(b, user.Posts, "post2")
+ require.Contains(b, user.Posts, "post3")
+}
diff --git a/binder/resp_header.go b/binder/resp_header.go
index ef14255315..fc84d01402 100644
--- a/binder/resp_header.go
+++ b/binder/resp_header.go
@@ -8,22 +8,24 @@ import (
"github.com/valyala/fasthttp"
)
-// respHeaderBinding is the respHeader binder for response header.
-type respHeaderBinding struct{}
+// RespHeaderBinding is the respHeader binder for response header.
+type RespHeaderBinding struct {
+ EnableSplitting bool
+}
// Name returns the binding name.
-func (*respHeaderBinding) Name() string {
+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 {
+func (b *RespHeaderBinding) Bind(resp *fasthttp.Response, out any) error {
data := make(map[string][]string)
resp.Header.VisitAll(func(key, val []byte) {
k := utils.UnsafeString(key)
v := utils.UnsafeString(val)
- if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
+ if b.EnableSplitting && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
values := strings.Split(v, ",")
for i := 0; i < len(values); i++ {
data[k] = append(data[k], values[i])
@@ -35,3 +37,8 @@ func (b *respHeaderBinding) Bind(resp *fasthttp.Response, out any) error {
return parse(b.Name(), out, data)
}
+
+// Reset resets the RespHeaderBinding binder.
+func (b *RespHeaderBinding) Reset() {
+ b.EnableSplitting = false
+}
diff --git a/binder/resp_header_test.go b/binder/resp_header_test.go
new file mode 100644
index 0000000000..ff3b51f604
--- /dev/null
+++ b/binder/resp_header_test.go
@@ -0,0 +1,79 @@
+package binder
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "github.com/valyala/fasthttp"
+)
+
+func Test_RespHeaderBinder_Bind(t *testing.T) {
+ t.Parallel()
+
+ b := &RespHeaderBinding{
+ EnableSplitting: true,
+ }
+ require.Equal(t, "respHeader", b.Name())
+
+ type User struct {
+ Name string `respHeader:"name"`
+ Posts []string `respHeader:"posts"`
+ Age int `respHeader:"age"`
+ }
+ var user User
+
+ resp := fasthttp.AcquireResponse()
+ resp.Header.Set("name", "john")
+ resp.Header.Set("age", "42")
+ resp.Header.Set("posts", "post1,post2,post3")
+
+ t.Cleanup(func() {
+ fasthttp.ReleaseResponse(resp)
+ })
+
+ err := b.Bind(resp, &user)
+
+ require.NoError(t, err)
+ require.Equal(t, "john", user.Name)
+ require.Equal(t, 42, user.Age)
+ require.Equal(t, []string{"post1", "post2", "post3"}, user.Posts)
+
+ b.Reset()
+ require.False(t, b.EnableSplitting)
+}
+
+func Benchmark_RespHeaderBinder_Bind(b *testing.B) {
+ b.ReportAllocs()
+
+ binder := &RespHeaderBinding{
+ EnableSplitting: true,
+ }
+
+ type User struct {
+ Name string `respHeader:"name"`
+ Posts []string `respHeader:"posts"`
+ Age int `respHeader:"age"`
+ }
+ var user User
+
+ resp := fasthttp.AcquireResponse()
+ resp.Header.Set("name", "john")
+ resp.Header.Set("age", "42")
+ resp.Header.Set("posts", "post1,post2,post3")
+
+ b.Cleanup(func() {
+ fasthttp.ReleaseResponse(resp)
+ })
+
+ b.ResetTimer()
+
+ var err error
+ for i := 0; i < b.N; i++ {
+ err = binder.Bind(resp, &user)
+ }
+
+ require.NoError(b, err)
+ require.Equal(b, "john", user.Name)
+ require.Equal(b, 42, user.Age)
+ require.Equal(b, []string{"post1", "post2", "post3"}, user.Posts)
+}
diff --git a/binder/uri.go b/binder/uri.go
index b58d9d49c4..9b358c64b8 100644
--- a/binder/uri.go
+++ b/binder/uri.go
@@ -1,15 +1,15 @@
package binder
// uriBinding is the URI binder for URI parameters.
-type uriBinding struct{}
+type URIBinding struct{}
// Name returns the binding name.
-func (*uriBinding) Name() string {
+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 {
+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 {
data[param] = append(data[param], paramsFunc(param))
@@ -17,3 +17,8 @@ func (b *uriBinding) Bind(params []string, paramsFunc func(key string, defaultVa
return parse(b.Name(), out, data)
}
+
+// Reset resets URIBinding binder.
+func (*URIBinding) Reset() {
+ // Nothing to reset
+}
diff --git a/binder/uri_test.go b/binder/uri_test.go
new file mode 100644
index 0000000000..8babdef962
--- /dev/null
+++ b/binder/uri_test.go
@@ -0,0 +1,77 @@
+package binder
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func Test_URIBinding_Bind(t *testing.T) {
+ t.Parallel()
+
+ b := &URIBinding{}
+ require.Equal(t, "uri", b.Name())
+
+ type User struct {
+ Name string `uri:"name"`
+ Posts []string `uri:"posts"`
+ Age int `uri:"age"`
+ }
+ var user User
+
+ paramsKey := []string{"name", "age", "posts"}
+ paramsVals := []string{"john", "42", "post1,post2,post3"}
+ paramsFunc := func(key string, _ ...string) string {
+ for i, k := range paramsKey {
+ if k == key {
+ return paramsVals[i]
+ }
+ }
+
+ return ""
+ }
+
+ err := b.Bind(paramsKey, paramsFunc, &user)
+ require.NoError(t, err)
+ require.Equal(t, "john", user.Name)
+ require.Equal(t, 42, user.Age)
+ require.Equal(t, []string{"post1,post2,post3"}, user.Posts)
+
+ b.Reset()
+}
+
+func Benchmark_URIBinding_Bind(b *testing.B) {
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ binder := &URIBinding{}
+
+ type User struct {
+ Name string `uri:"name"`
+ Posts []string `uri:"posts"`
+ Age int `uri:"age"`
+ }
+ var user User
+
+ paramsKey := []string{"name", "age", "posts"}
+ paramsVals := []string{"john", "42", "post1,post2,post3"}
+ paramsFunc := func(key string, _ ...string) string {
+ for i, k := range paramsKey {
+ if k == key {
+ return paramsVals[i]
+ }
+ }
+
+ return ""
+ }
+
+ var err error
+ for i := 0; i < b.N; i++ {
+ err = binder.Bind(paramsKey, paramsFunc, &user)
+ }
+
+ require.NoError(b, err)
+ require.Equal(b, "john", user.Name)
+ require.Equal(b, 42, user.Age)
+ require.Equal(b, []string{"post1,post2,post3"}, user.Posts)
+}
diff --git a/binder/xml.go b/binder/xml.go
index 58da2b9b07..0c345a4236 100644
--- a/binder/xml.go
+++ b/binder/xml.go
@@ -1,23 +1,31 @@
package binder
import (
- "encoding/xml"
"fmt"
+
+ "github.com/gofiber/utils/v2"
)
-// xmlBinding is the XML binder for XML request body.
-type xmlBinding struct{}
+// XMLBinding is the XML binder for XML request body.
+type XMLBinding struct {
+ XMLDecoder utils.XMLUnmarshal
+}
// Name returns the binding name.
-func (*xmlBinding) Name() string {
+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 {
+func (b *XMLBinding) Bind(body []byte, out any) error {
+ if err := b.XMLDecoder(body, out); err != nil {
return fmt.Errorf("failed to unmarshal xml: %w", err)
}
return nil
}
+
+// Reset resets the XMLBinding binder.
+func (b *XMLBinding) Reset() {
+ b.XMLDecoder = nil
+}
diff --git a/binder/xml_test.go b/binder/xml_test.go
new file mode 100644
index 0000000000..879ccf0b78
--- /dev/null
+++ b/binder/xml_test.go
@@ -0,0 +1,135 @@
+package binder
+
+import (
+ "encoding/xml"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func Test_XMLBinding_Bind(t *testing.T) {
+ t.Parallel()
+
+ b := &XMLBinding{
+ XMLDecoder: xml.Unmarshal,
+ }
+ require.Equal(t, "xml", b.Name())
+
+ type Posts struct {
+ XMLName xml.Name `xml:"post"`
+ Title string `xml:"title"`
+ }
+
+ type User struct {
+ Name string `xml:"name"`
+ Ignore string `xml:"-"`
+ Posts []Posts `xml:"posts>post"`
+ Age int `xml:"age"`
+ }
+
+ user := new(User)
+ err := b.Bind([]byte(`
+
+ john
+ 42
+ ignore
+
+
+ post1
+
+
+ post2
+
+
+
+ `), user)
+ require.NoError(t, err)
+ require.Equal(t, "john", user.Name)
+ require.Equal(t, 42, user.Age)
+ require.Empty(t, user.Ignore)
+
+ require.Len(t, user.Posts, 2)
+ require.Equal(t, "post1", user.Posts[0].Title)
+ require.Equal(t, "post2", user.Posts[1].Title)
+
+ b.Reset()
+ require.Nil(t, b.XMLDecoder)
+}
+
+func Test_XMLBinding_Bind_error(t *testing.T) {
+ t.Parallel()
+ b := &XMLBinding{
+ XMLDecoder: xml.Unmarshal,
+ }
+
+ type User struct {
+ Name string `xml:"name"`
+ Age int `xml:"age"`
+ }
+
+ user := new(User)
+ err := b.Bind([]byte(`
+
+ john
+ 42
+ unknown
+ post"`
+ Age int `xml:"age"`
+ }
+
+ user := new(User)
+ data := []byte(`
+
+ john
+ 42
+ ignore
+
+
+ post1
+
+
+ post2
+
+
+
+ `)
+
+ b.StartTimer()
+
+ var err error
+ for i := 0; i < b.N; i++ {
+ err = binder.Bind(data, user)
+ }
+ require.NoError(b, err)
+
+ user = new(User)
+ err = binder.Bind(data, user)
+ require.NoError(b, err)
+
+ require.Equal(b, "john", user.Name)
+ require.Equal(b, 42, user.Age)
+
+ require.Len(b, user.Posts, 2)
+ require.Equal(b, "post1", user.Posts[0].Title)
+ require.Equal(b, "post2", user.Posts[1].Title)
+}
diff --git a/ctx_test.go b/ctx_test.go
index 72c415a9ca..fe9a4a412d 100644
--- a/ctx_test.go
+++ b/ctx_test.go
@@ -1433,7 +1433,9 @@ func Benchmark_Ctx_Fresh_LastModified(b *testing.B) {
func Test_Ctx_Binders(t *testing.T) {
t.Parallel()
// setup
- app := New()
+ app := New(Config{
+ EnableSplittingOnParsers: true,
+ })
type TestEmbeddedStruct struct {
Names []string `query:"names"`
diff --git a/docs/api/bind.md b/docs/api/bind.md
index e91fed4273..d2b336310d 100644
--- a/docs/api/bind.md
+++ b/docs/api/bind.md
@@ -18,7 +18,6 @@ Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more..
- [Body](#body)
- [Form](#form)
- [JSON](#json)
- - [MultipartForm](#multipartform)
- [XML](#xml)
- [CBOR](#cbor)
- [Cookie](#cookie)
@@ -83,7 +82,7 @@ curl -X POST -F name=john -F pass=doe http://localhost:3000
### Form
-Binds the request form body to a struct.
+Binds the request or multipart form body data to a struct.
It is important to specify the correct struct tag based on the content type to be parsed. For example, if you want to parse a form body with a field called `Pass`, you would use a struct field with `form:"pass"`.
@@ -111,12 +110,16 @@ app.Post("/", func(c fiber.Ctx) error {
})
```
-Run tests with the following `curl` command:
+Run tests with the following `curl` commands for both `application/x-www-form-urlencoded` and `multipart/form-data`:
```bash
curl -X POST -H "Content-Type: application/x-www-form-urlencoded" --data "name=john&pass=doe" localhost:3000
```
+```bash
+curl -X POST -H "Content-Type: multipart/form-data" -F "name=john" -F "pass=doe" localhost:3000
+```
+
### JSON
Binds the request JSON body to a struct.
@@ -153,43 +156,6 @@ Run tests with the following `curl` command:
curl -X POST -H "Content-Type: application/json" --data "{\"name\":\"john\",\"pass\":\"doe\"}" localhost:3000
```
-### MultipartForm
-
-Binds the request multipart form body to a struct.
-
-It is important to specify the correct struct tag based on the content type to be parsed. For example, if you want to parse a multipart form body with a field called `Pass`, you would use a struct field with `form:"pass"`.
-
-```go title="Signature"
-func (b *Bind) MultipartForm(out any) error
-```
-
-```go title="Example"
-// Field names should start with an uppercase letter
-type Person struct {
- Name string `form:"name"`
- Pass string `form:"pass"`
-}
-
-app.Post("/", func(c fiber.Ctx) error {
- p := new(Person)
-
- if err := c.Bind().MultipartForm(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: multipart/form-data" -F "name=john" -F "pass=doe" localhost:3000
-```
-
### XML
Binds the request XML body to a struct.
diff --git a/docs/api/fiber.md b/docs/api/fiber.md
index 17cf3896b9..70320984da 100644
--- a/docs/api/fiber.md
+++ b/docs/api/fiber.md
@@ -83,6 +83,7 @@ app := fiber.New(fiber.Config{
| 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` |
+| XMLDecoder | `utils.XMLUnmarshal` | Allowing for flexibility in using another XML library for decoding. | `xml.Unmarshal` |
## Server listening
diff --git a/docs/whats_new.md b/docs/whats_new.md
index eadc1afa4a..321df424d6 100644
--- a/docs/whats_new.md
+++ b/docs/whats_new.md
@@ -49,6 +49,7 @@ We have made several changes to the Fiber app, including:
- `EnablePrintRoutes`
- `ListenerNetwork` (previously `Network`)
- **Trusted Proxy Configuration**: The `EnabledTrustedProxyCheck` has been moved to `app.Config.TrustProxy`, and `TrustedProxies` has been moved to `TrustProxyConfig.Proxies`.
+- **XMLDecoder Config Property**: The `XMLDecoder` property has been added to allow usage of 3rd-party XML libraries in XML binder.
### New Methods
diff --git a/redirect.go b/redirect.go
index bc79314922..483272c7b5 100644
--- a/redirect.go
+++ b/redirect.go
@@ -146,10 +146,8 @@ func (r *Redirect) WithInput() *Redirect {
oldInput := make(map[string]string)
switch ctype {
- case MIMEApplicationForm:
+ case MIMEApplicationForm, MIMEMultipartForm:
_ = r.c.Bind().Form(oldInput) //nolint:errcheck // not needed
- case MIMEMultipartForm:
- _ = r.c.Bind().MultipartForm(oldInput) //nolint:errcheck // not needed
default:
_ = r.c.Bind().Query(oldInput) //nolint:errcheck // not needed
}