diff --git a/bind.go b/bind.go index b0331a88a..31644821d 100644 --- a/bind.go +++ b/bind.go @@ -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 @@ -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: diff --git a/bind_test.go b/bind_test.go index 6a40985e9..0510cb060 100644 --- a/bind_test.go +++ b/bind_test.go @@ -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) diff --git a/context.go b/context.go index a8ee4b51d..8aaaa3de6 100644 --- a/context.go +++ b/context.go @@ -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 } diff --git a/context_test.go b/context_test.go index 83040dc76..07e31e344 100644 --- a/context_test.go +++ b/context_test.go @@ -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