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
5 changes: 3 additions & 2 deletions bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ func BindQueryParams(c *Context, target any) error {
}

// BindBody binds request body contents to bindable object
// The Content-Type media type is matched case-insensitively.
// NB: then binding forms take note that this implementation uses standard library form parsing
// which parses form data from BOTH URL and BODY if content type is not MIMEMultipartForm
// See non-MIMEMultipartForm: https://golang.org/pkg/net/http/#Request.ParseForm
Expand All @@ -71,9 +72,9 @@ func BindBody(c *Context, target any) (err error) {
return
}

// mediatype is found like `mime.ParseMediaType()` does it
// Like mime.ParseMediaType, normalize the media type without changing its parameters.
base, _, _ := strings.Cut(req.Header.Get(HeaderContentType), ";")
mediatype := strings.TrimSpace(base)
mediatype := strings.ToLower(strings.TrimSpace(base))

switch mediatype {
case MIMEApplicationJSON:
Expand Down
43 changes: 43 additions & 0 deletions bind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,49 @@ func TestBindJSON(t *testing.T) {
testBindError(t, strings.NewReader(userJSONInvalidType), MIMEApplicationJSON, &json.UnmarshalTypeError{})
}

func TestBindBodyMediaTypeCaseInsensitive(t *testing.T) {
body := new(bytes.Buffer)
mw := multipart.NewWriter(body)
if !assert.NoError(t, mw.SetBoundary("CaseSensitiveBoundary")) {
return
}
assert.NoError(t, mw.WriteField("id", "1"))
assert.NoError(t, mw.WriteField("name", "Jon Snow"))
assert.NoError(t, mw.Close())

for _, tc := range []struct {
contentType string
body string
}{
{"Application/JSON; Charset=UTF-8", userJSON},
{"APPLICATION/XML", userXML},
{"Text/XML; charset=UTF-8", userXML},
{"Application/X-Www-Form-Urlencoded", userForm},
{"Multipart/Form-Data; boundary=CaseSensitiveBoundary", body.String()},
} {
t.Run(tc.contentType, func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tc.body))
req.Header.Set(HeaderContentType, tc.contentType)
c := New().NewContext(req, httptest.NewRecorder())
var target user

if assert.NoError(t, BindBody(c, &target)) {
assert.Equal(t, user{ID: 1, Name: "Jon Snow"}, target)
}
assert.Equal(t, tc.contentType, req.Header.Get(HeaderContentType))
})
}

t.Run("unsupported media type", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON))
req.Header.Set(HeaderContentType, "Application/JSON-Invalid")
c := New().NewContext(req, httptest.NewRecorder())
var target user

assert.Equal(t, &HTTPError{Code: http.StatusUnsupportedMediaType}, BindBody(c, &target))
})
}

func TestBindXML(t *testing.T) {
testBindOkay(t, strings.NewReader(userXML), nil, MIMEApplicationXML)
testBindOkay(t, strings.NewReader(userXML), dummyQuery, MIMEApplicationXML)
Expand Down
3 changes: 2 additions & 1 deletion context.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,8 @@ func (c *Context) FormValueOr(name, defaultValue string) string {

// FormValues returns the form field values as `url.Values`.
func (c *Context) FormValues() (url.Values, error) {
if strings.HasPrefix(c.request.Header.Get(HeaderContentType), MIMEMultipartForm) {
base, _, _ := strings.Cut(c.request.Header.Get(HeaderContentType), ";")
if strings.EqualFold(strings.TrimSpace(base), MIMEMultipartForm) {
if err := c.request.ParseMultipartForm(c.formParseMaxMemory); err != nil {
return nil, err
}
Expand Down
28 changes: 28 additions & 0 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,34 @@ func TestContextFormValue(t *testing.T) {
assert.Error(t, err)
}

func TestContextFormValuesMediaTypeCaseInsensitive(t *testing.T) {
body := new(bytes.Buffer)
mw := multipart.NewWriter(body)
if !assert.NoError(t, mw.SetBoundary("CaseSensitiveBoundary")) {
return
}
assert.NoError(t, mw.WriteField("name", "Jon Snow"))
assert.NoError(t, mw.Close())

for _, contentType := range []string{
"multipart/form-data; boundary=CaseSensitiveBoundary",
"Multipart/Form-Data; boundary=CaseSensitiveBoundary",
"MULTIPART/FORM-DATA; boundary=CaseSensitiveBoundary",
} {
t.Run(contentType, func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/?query=value", bytes.NewReader(body.Bytes()))
req.Header.Set(HeaderContentType, contentType)
c := New().NewContext(req, nil)

values, err := c.FormValues()
if assert.NoError(t, err) {
assert.Equal(t, url.Values{"name": {"Jon Snow"}, "query": {"value"}}, values)
}
assert.Equal(t, contentType, req.Header.Get(HeaderContentType))
})
}
}

func TestContext_QueryParams(t *testing.T) {
var testCases = []struct {
expect url.Values
Expand Down