1
0
mirror of https://github.com/gofiber/fiber.git synced 2025-02-06 11:02:01 +00:00

🐛 bug: fix EnableSplittingOnParsers is not functional (#3231)

* 🐛 bug: fix EnableSplittingOnParsers is not functional

* remove wrong testcase

* add support for external xml decoders

* improve test coverage

* fix linter

* update

* add reset methods

* improve test coverage

* merge Form and MultipartForm methods

* fix linter

* split reset and putting steps

* fix linter
This commit is contained in:
M. Efe Çetin 2024-12-25 14:53:14 +03:00 committed by GitHub
parent 58677d5c86
commit 57744ebbe8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 1339 additions and 152 deletions

10
app.go
View File

@ -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
}

109
bind.go
View File

@ -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

View File

@ -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(``))

View File

@ -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)
}

28
binder/binder_test.go Normal file
View File

@ -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)
}

View File

@ -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
}

92
binder/cbor_test.go Normal file
View File

@ -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)
}

View File

@ -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
}

90
binder/cookie_test.go Normal file
View File

@ -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")
}

View File

@ -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
}

174
binder/form_test.go Normal file
View File

@ -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)
}

View File

@ -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
}

88
binder/header_test.go Normal file
View File

@ -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")
}

View File

@ -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
}

69
binder/json_test.go Normal file
View File

@ -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])
}

View File

@ -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
}
for k, v := range data {
if len(v) == 0 {
newMap[k] = ""
continue
}
// map[string]string
newMap, ok := ptr.(map[string]string)
if !ok {
return ErrMapNotConvertable
}
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 {

View File

@ -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)
})
}
}

View File

@ -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
}

87
binder/query_test.go Normal file
View File

@ -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")
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

77
binder/uri_test.go Normal file
View File

@ -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)
}

View File

@ -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
}

135
binder/xml_test.go Normal file
View File

@ -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(`
<user>
<name>john</name>
<age>42</age>
<ignore>ignore</ignore>
<posts>
<post>
<title>post1</title>
</post>
<post>
<title>post2</title>
</post>
</posts>
</user>
`), 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(`
<user>
<name>john</name>
<age>42</age>
<unknown>unknown</unknown>
</user
`), user)
require.Error(t, err)
require.Contains(t, err.Error(), "failed to unmarshal xml")
}
func Benchmark_XMLBinding_Bind(b *testing.B) {
b.ReportAllocs()
binder := &XMLBinding{
XMLDecoder: xml.Unmarshal,
}
type Posts struct {
XMLName xml.Name `xml:"post"`
Title string `xml:"title"`
}
type User struct {
Name string `xml:"name"`
Posts []Posts `xml:"posts>post"`
Age int `xml:"age"`
}
user := new(User)
data := []byte(`
<user>
<name>john</name>
<age>42</age>
<ignore>ignore</ignore>
<posts>
<post>
<title>post1</title>
</post>
<post>
<title>post2</title>
</post>
</posts>
</user>
`)
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)
}

View File

@ -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"`

View File

@ -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.

View File

@ -83,6 +83,7 @@ app := fiber.New(fiber.Config{
| <Reference id="writebuffersize">WriteBufferSize</Reference> | `int` | Per-connection buffer size for responses' writing. | `4096` |
| <Reference id="writetimeout">WriteTimeout</Reference> | `time.Duration` | The maximum duration before timing out writes of the response. The default timeout is unlimited. | `nil` |
| <Reference id="xmlencoder">XMLEncoder</Reference> | `utils.XMLMarshal` | Allowing for flexibility in using another XML library for encoding. | `xml.Marshal` |
| <Reference id="xmldecoder">XMLDecoder</Reference> | `utils.XMLUnmarshal` | Allowing for flexibility in using another XML library for decoding. | `xml.Unmarshal` |
## Server listening

View File

@ -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

View File

@ -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
}