Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 bug: Fix square bracket notation in Multipart FormData #3235

Merged
merged 13 commits into from
Dec 31, 2024
108 changes: 106 additions & 2 deletions bind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"encoding/json"
"errors"
"fmt"
"mime/multipart"
"net/http/httptest"
"reflect"
"testing"
Expand Down Expand Up @@ -988,6 +989,48 @@
Data []Demo `query:"data"`
}

t.Run("MultipartCollectionQueryDotNotation", func(t *testing.T) {
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().Reset()

buf := &bytes.Buffer{}
writer := multipart.NewWriter(buf)
writer.WriteField("data.0.name", "john")

Check failure on line 998 in bind_test.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `writer.WriteField` is not checked (errcheck)
writer.WriteField("data.1.name", "doe")

Check failure on line 999 in bind_test.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `writer.WriteField` is not checked (errcheck)
writer.Close()

Check failure on line 1000 in bind_test.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `writer.Close` is not checked (errcheck)
gaby marked this conversation as resolved.
Show resolved Hide resolved

c.Request().Header.SetContentType(writer.FormDataContentType())
c.Request().SetBody(buf.Bytes())
c.Request().Header.SetContentLength(len(c.Body()))

cq := new(CollectionQuery)
require.NoError(t, c.Bind().Body(cq))
require.Len(t, cq.Data, 2)
require.Equal(t, "john", cq.Data[0].Name)
require.Equal(t, "doe", cq.Data[1].Name)
})

t.Run("MultipartCollectionQuerySquareBrackets", func(t *testing.T) {
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().Reset()

buf := &bytes.Buffer{}
writer := multipart.NewWriter(buf)
writer.WriteField("data[0][name]", "john")

Check failure on line 1019 in bind_test.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `writer.WriteField` is not checked (errcheck)
writer.WriteField("data[1][name]", "doe")

Check failure on line 1020 in bind_test.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `writer.WriteField` is not checked (errcheck)
writer.Close()

Check failure on line 1021 in bind_test.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `writer.Close` is not checked (errcheck)
gaby marked this conversation as resolved.
Show resolved Hide resolved

c.Request().Header.SetContentType(writer.FormDataContentType())
c.Request().SetBody(buf.Bytes())
c.Request().Header.SetContentLength(len(c.Body()))

cq := new(CollectionQuery)
require.NoError(t, c.Bind().Body(cq))
require.Len(t, cq.Data, 2)
require.Equal(t, "john", cq.Data[0].Name)
require.Equal(t, "doe", cq.Data[1].Name)
})

t.Run("CollectionQuerySquareBrackets", func(t *testing.T) {
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().Reset()
Expand Down Expand Up @@ -1180,13 +1223,69 @@
app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})

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

type Demo struct {
Name string `form:"name"`
Persons []Person `form:"persons"`
}

buf := &bytes.Buffer{}
writer := multipart.NewWriter(buf)
writer.WriteField("name", "john")

Check failure on line 1238 in bind_test.go

View workflow job for this annotation

GitHub Actions / lint

unhandled-error: Unhandled error in call to function mime/multipart.Writer.WriteField (revive)

Check failure on line 1238 in bind_test.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `writer.WriteField` is not checked (errcheck)

writer.Close()

Check failure on line 1240 in bind_test.go

View workflow job for this annotation

GitHub Actions / lint

unhandled-error: Unhandled error in call to function mime/multipart.Writer.Close (revive)

Check failure on line 1240 in bind_test.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `writer.Close` is not checked (errcheck)
gaby marked this conversation as resolved.
Show resolved Hide resolved
body := buf.Bytes()

c.Request().SetBody(body)
c.Request().Header.SetContentType(MIMEMultipartForm + `;boundary=` + writer.Boundary())
c.Request().Header.SetContentLength(len(body))
d := new(Demo)

b.ReportAllocs()
b.ResetTimer()

for n := 0; n < b.N; n++ {
err = c.Bind().Body(d)
}

require.NoError(b, err)
require.Equal(b, "john", d.Name)
}

// go test -v -run=^$ -bench=Benchmark_Bind_Body_MultipartForm_Nested -benchmem -count=4
func Benchmark_Bind_Body_MultipartForm_Nested(b *testing.B) {
var err error

app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})

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

type Demo struct {
Name string `form:"name"`
Persons []Person `form:"persons"`
}

body := []byte("--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--")
buf := &bytes.Buffer{}
writer := multipart.NewWriter(buf)
writer.WriteField("name", "john")
writer.WriteField("persons.0.name", "john")
writer.WriteField("persons[0][age]", "10")
writer.WriteField("persons[1][name]", "doe")
writer.WriteField("persons.1.age", "20")

writer.Close()
gaby marked this conversation as resolved.
Show resolved Hide resolved
body := buf.Bytes()

c.Request().SetBody(body)
c.Request().Header.SetContentType(MIMEMultipartForm + `;boundary="b"`)
c.Request().Header.SetContentType(MIMEMultipartForm + `;boundary=` + writer.Boundary())
c.Request().Header.SetContentLength(len(body))
d := new(Demo)

Expand All @@ -1196,8 +1295,13 @@
for n := 0; n < b.N; n++ {
err = c.Bind().Body(d)
}

require.NoError(b, err)
require.Equal(b, "john", d.Name)
require.Equal(b, "john", d.Persons[0].Name)
require.Equal(b, 10, d.Persons[0].Age)
require.Equal(b, "doe", d.Persons[1].Name)
require.Equal(b, 20, d.Persons[1].Age)
}

// go test -v -run=^$ -bench=Benchmark_Bind_Body_Form_Map -benchmem -count=4
Expand Down
22 changes: 22 additions & 0 deletions binder/form.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,27 @@
return err
}

for key, values := range data.Value {
ReneWerner87 marked this conversation as resolved.
Show resolved Hide resolved
if strings.Contains(key, "[") {
k, err := parseParamSquareBrackets(key)
if err != nil {
return err
}
data.Value[k] = values
delete(data.Value, key) // Remove bracket notation and use dot instead

Check warning on line 67 in binder/form.go

View check run for this annotation

Codecov / codecov/patch

binder/form.go#L60-L67

Added lines #L60 - L67 were not covered by tests
}

for _, v := range values {
if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, key) {
delete(data.Value, key)

values := strings.Split(v, ",")
for i := 0; i < len(values); i++ {
data.Value[key] = append(data.Value[key], values[i])
}

Check warning on line 77 in binder/form.go

View check run for this annotation

Codecov / codecov/patch

binder/form.go#L70-L77

Added lines #L70 - L77 were not covered by tests
}
}
}

gaby marked this conversation as resolved.
Show resolved Hide resolved
return parse(b.Name(), out, data.Value)
}
Loading