Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 57 additions & 4 deletions binder/form.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,27 @@ package binder

import (
"mime/multipart"
"sync"

"github.com/gofiber/utils/v2"
"github.com/valyala/fasthttp"
)

const MIMEMultipartForm string = "multipart/form-data"

var (
formMapPool = sync.Pool{
New: func() any {
return make(map[string][]string)
},
}
formFileMapPool = sync.Pool{
New: func() any {
return make(map[string][]*multipart.FileHeader)
},
}
)

// FormBinding is the form binder for form request body.
type FormBinding struct {
EnableSplitting bool
Expand All @@ -21,13 +35,14 @@ func (*FormBinding) Name() string {

// Bind parses the request body and returns the result.
func (b *FormBinding) Bind(req *fasthttp.Request, out any) error {
data := make(map[string][]string)

// Handle multipart form
if FilterFlags(utils.UnsafeString(req.Header.ContentType())) == MIMEMultipartForm {
return b.bindMultipart(req, out)
}

data := acquireFormMap()
defer releaseFormMap(data)

for key, val := range req.PostArgs().All() {
k := utils.UnsafeString(key)
v := utils.UnsafeString(val)
Expand All @@ -46,15 +61,19 @@ func (b *FormBinding) bindMultipart(req *fasthttp.Request, out any) error {
return err
}

data := make(map[string][]string)
data := acquireFormMap()
defer releaseFormMap(data)

for key, values := range multipartForm.Value {
err = formatBindData(b.Name(), out, data, key, values, b.EnableSplitting, true)
if err != nil {
return err
}
}

files := make(map[string][]*multipart.FileHeader)
files := acquireFileHeaderMap()
defer releaseFileHeaderMap(files)

for key, values := range multipartForm.File {
err = formatBindData(b.Name(), out, files, key, values, b.EnableSplitting, true)
if err != nil {
Expand All @@ -69,3 +88,37 @@ func (b *FormBinding) bindMultipart(req *fasthttp.Request, out any) error {
func (b *FormBinding) Reset() {
b.EnableSplitting = false
}

func acquireFormMap() map[string][]string {
m, ok := formMapPool.Get().(map[string][]string)
if !ok {
m = make(map[string][]string)
}
return m
}

func releaseFormMap(m map[string][]string) {
clearFormMap(m)
formMapPool.Put(m)
}

func acquireFileHeaderMap() map[string][]*multipart.FileHeader {
m, ok := formFileMapPool.Get().(map[string][]*multipart.FileHeader)
if !ok {
m = make(map[string][]*multipart.FileHeader)
}
return m
}

func releaseFileHeaderMap(m map[string][]*multipart.FileHeader) {
clearFileHeaderMap(m)
formFileMapPool.Put(m)
}

func clearFormMap(m map[string][]string) {
clear(m)
}

func clearFileHeaderMap(m map[string][]*multipart.FileHeader) {
clear(m)
}
83 changes: 83 additions & 0 deletions binder/form_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,89 @@ func Test_FormBinder_BindMultipart_FileError(t *testing.T) {
require.Contains(t, err.Error(), "unmatched brackets")
}

func Test_FormBinder_Bind_MapClearedBetweenRequests(t *testing.T) {
t.Parallel()

b := &FormBinding{}

type payload struct {
Name string `form:"name"`
Age int `form:"age"`
}

firstReq := fasthttp.AcquireRequest()
firstReq.SetBodyString("name=john&age=21")
firstReq.Header.SetContentType("application/x-www-form-urlencoded")
t.Cleanup(func() { fasthttp.ReleaseRequest(firstReq) })

var first payload
require.NoError(t, b.Bind(firstReq, &first))
require.Equal(t, "john", first.Name)
require.Equal(t, 21, first.Age)

secondReq := fasthttp.AcquireRequest()
secondReq.SetBodyString("age=42")
secondReq.Header.SetContentType("application/x-www-form-urlencoded")
t.Cleanup(func() { fasthttp.ReleaseRequest(secondReq) })

var second payload
require.NoError(t, b.Bind(secondReq, &second))
require.Empty(t, second.Name)
require.Equal(t, 42, second.Age)
}

func Test_FormBinder_BindMultipart_MapsClearedBetweenRequests(t *testing.T) {
t.Parallel()

b := &FormBinding{}

type payload struct { // betteralign:ignore - test payload prioritizes readability over alignment
Avatar *multipart.FileHeader `form:"avatar"`
Name string `form:"name"`
Age int `form:"age"`
}

firstReq := fasthttp.AcquireRequest()
firstBuffer := &bytes.Buffer{}
firstWriter := multipart.NewWriter(firstBuffer)

require.NoError(t, firstWriter.WriteField("name", "john"))
require.NoError(t, firstWriter.WriteField("age", "21"))

firstFile, err := firstWriter.CreateFormFile("avatar", "avatar.txt")
require.NoError(t, err)
_, err = firstFile.Write([]byte("avatar-content"))
require.NoError(t, err)
require.NoError(t, firstWriter.Close())

firstReq.Header.SetContentType(firstWriter.FormDataContentType())
firstReq.SetBody(firstBuffer.Bytes())
t.Cleanup(func() { fasthttp.ReleaseRequest(firstReq) })

var first payload
require.NoError(t, b.Bind(firstReq, &first))
require.Equal(t, "john", first.Name)
require.Equal(t, 21, first.Age)
require.NotNil(t, first.Avatar)
require.Equal(t, "avatar.txt", first.Avatar.Filename)

secondReq := fasthttp.AcquireRequest()
secondBuffer := &bytes.Buffer{}
secondWriter := multipart.NewWriter(secondBuffer)
require.NoError(t, secondWriter.WriteField("age", "42"))
require.NoError(t, secondWriter.Close())

secondReq.Header.SetContentType(secondWriter.FormDataContentType())
secondReq.SetBody(secondBuffer.Bytes())
t.Cleanup(func() { fasthttp.ReleaseRequest(secondReq) })

var second payload
require.NoError(t, b.Bind(secondReq, &second))
require.Empty(t, second.Name)
require.Equal(t, 42, second.Age)
require.Nil(t, second.Avatar)
}

func Benchmark_FormBinder_BindMultipart(b *testing.B) {
b.ReportAllocs()

Expand Down
Loading