mirror of
https://github.com/gin-gonic/gin.git
synced 2026-06-06 20:18:19 +08:00
fix(binding): support multipart/mixed form parsing (#2547)
This commit is contained in:
parent
5f4f964325
commit
0cf6c8742b
@ -17,6 +17,7 @@ const (
|
|||||||
MIMEPlain = "text/plain"
|
MIMEPlain = "text/plain"
|
||||||
MIMEPOSTForm = "application/x-www-form-urlencoded"
|
MIMEPOSTForm = "application/x-www-form-urlencoded"
|
||||||
MIMEMultipartPOSTForm = "multipart/form-data"
|
MIMEMultipartPOSTForm = "multipart/form-data"
|
||||||
|
MIMEMultipartMixed = "multipart/mixed"
|
||||||
MIMEPROTOBUF = "application/x-protobuf"
|
MIMEPROTOBUF = "application/x-protobuf"
|
||||||
MIMEMSGPACK = "application/x-msgpack"
|
MIMEMSGPACK = "application/x-msgpack"
|
||||||
MIMEMSGPACK2 = "application/msgpack"
|
MIMEMSGPACK2 = "application/msgpack"
|
||||||
@ -110,7 +111,7 @@ func Default(method, contentType string) Binding {
|
|||||||
return YAML
|
return YAML
|
||||||
case MIMETOML:
|
case MIMETOML:
|
||||||
return TOML
|
return TOML
|
||||||
case MIMEMultipartPOSTForm:
|
case MIMEMultipartPOSTForm, MIMEMultipartMixed:
|
||||||
return FormMultipart
|
return FormMultipart
|
||||||
case MIMEBSON:
|
case MIMEBSON:
|
||||||
return BSON
|
return BSON
|
||||||
|
|||||||
@ -17,6 +17,7 @@ const (
|
|||||||
MIMEPlain = "text/plain"
|
MIMEPlain = "text/plain"
|
||||||
MIMEPOSTForm = "application/x-www-form-urlencoded"
|
MIMEPOSTForm = "application/x-www-form-urlencoded"
|
||||||
MIMEMultipartPOSTForm = "multipart/form-data"
|
MIMEMultipartPOSTForm = "multipart/form-data"
|
||||||
|
MIMEMultipartMixed = "multipart/mixed"
|
||||||
MIMEPROTOBUF = "application/x-protobuf"
|
MIMEPROTOBUF = "application/x-protobuf"
|
||||||
MIMEYAML = "application/x-yaml"
|
MIMEYAML = "application/x-yaml"
|
||||||
MIMEYAML2 = "application/yaml"
|
MIMEYAML2 = "application/yaml"
|
||||||
@ -102,7 +103,7 @@ func Default(method, contentType string) Binding {
|
|||||||
return ProtoBuf
|
return ProtoBuf
|
||||||
case MIMEYAML, MIMEYAML2:
|
case MIMEYAML, MIMEYAML2:
|
||||||
return YAML
|
return YAML
|
||||||
case MIMEMultipartPOSTForm:
|
case MIMEMultipartPOSTForm, MIMEMultipartMixed:
|
||||||
return FormMultipart
|
return FormMultipart
|
||||||
case MIMETOML:
|
case MIMETOML:
|
||||||
return TOML
|
return TOML
|
||||||
|
|||||||
@ -162,6 +162,8 @@ func TestBindingDefault(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, FormMultipart, Default(http.MethodPost, MIMEMultipartPOSTForm))
|
assert.Equal(t, FormMultipart, Default(http.MethodPost, MIMEMultipartPOSTForm))
|
||||||
assert.Equal(t, FormMultipart, Default(http.MethodPut, MIMEMultipartPOSTForm))
|
assert.Equal(t, FormMultipart, Default(http.MethodPut, MIMEMultipartPOSTForm))
|
||||||
|
assert.Equal(t, FormMultipart, Default(http.MethodPost, MIMEMultipartMixed))
|
||||||
|
assert.Equal(t, FormMultipart, Default(http.MethodPut, MIMEMultipartMixed))
|
||||||
|
|
||||||
assert.Equal(t, ProtoBuf, Default(http.MethodPost, MIMEPROTOBUF))
|
assert.Equal(t, ProtoBuf, Default(http.MethodPost, MIMEPROTOBUF))
|
||||||
assert.Equal(t, ProtoBuf, Default(http.MethodPut, MIMEPROTOBUF))
|
assert.Equal(t, ProtoBuf, Default(http.MethodPut, MIMEPROTOBUF))
|
||||||
@ -593,6 +595,21 @@ func createFormMultipartRequest(t *testing.T) *http.Request {
|
|||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createFormMultipartMixedRequest(t *testing.T) *http.Request {
|
||||||
|
boundary := "--testboundary"
|
||||||
|
body := new(bytes.Buffer)
|
||||||
|
mw := multipart.NewWriter(body)
|
||||||
|
defer mw.Close()
|
||||||
|
|
||||||
|
require.NoError(t, mw.SetBoundary(boundary))
|
||||||
|
require.NoError(t, mw.WriteField("foo", "bar"))
|
||||||
|
require.NoError(t, mw.WriteField("bar", "foo"))
|
||||||
|
req, err := http.NewRequest(http.MethodPost, "/?foo=getfoo&bar=getbar", body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
req.Header.Set("Content-Type", MIMEMultipartMixed+"; boundary="+boundary)
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
func createFormMultipartRequestForMap(t *testing.T) *http.Request {
|
func createFormMultipartRequestForMap(t *testing.T) *http.Request {
|
||||||
boundary := "--testboundary"
|
boundary := "--testboundary"
|
||||||
body := new(bytes.Buffer)
|
body := new(bytes.Buffer)
|
||||||
@ -631,6 +648,16 @@ func TestBindingFormPost(t *testing.T) {
|
|||||||
assert.Equal(t, "foo", obj.Bar)
|
assert.Equal(t, "foo", obj.Bar)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBindingFormMultipartMixed(t *testing.T) {
|
||||||
|
req := createFormMultipartMixedRequest(t)
|
||||||
|
var obj FooBarStruct
|
||||||
|
require.NoError(t, FormMultipart.Bind(req, &obj))
|
||||||
|
|
||||||
|
assert.Equal(t, "multipart/form-data", FormMultipart.Name())
|
||||||
|
assert.Equal(t, "bar", obj.Foo)
|
||||||
|
assert.Equal(t, "foo", obj.Bar)
|
||||||
|
}
|
||||||
|
|
||||||
func TestBindingDefaultValueFormPost(t *testing.T) {
|
func TestBindingDefaultValueFormPost(t *testing.T) {
|
||||||
req := createDefaultFormPostRequest(t)
|
req := createDefaultFormPostRequest(t)
|
||||||
var obj FooDefaultBarStruct
|
var obj FooDefaultBarStruct
|
||||||
@ -932,6 +959,15 @@ func TestFormBindingMultipartFail(t *testing.T) {
|
|||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFormBindingMultipartMixed(t *testing.T) {
|
||||||
|
obj := FooBarStruct{}
|
||||||
|
req := createFormMultipartMixedRequest(t)
|
||||||
|
err := Form.Bind(req, &obj)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "getfoo", obj.Foo)
|
||||||
|
assert.Equal(t, "getbar", obj.Bar)
|
||||||
|
}
|
||||||
|
|
||||||
func TestFormPostBindingFail(t *testing.T) {
|
func TestFormPostBindingFail(t *testing.T) {
|
||||||
b := FormPost
|
b := FormPost
|
||||||
assert.Equal(t, "form-urlencoded", b.Name())
|
assert.Equal(t, "form-urlencoded", b.Name())
|
||||||
|
|||||||
@ -25,7 +25,7 @@ func (formBinding) Bind(req *http.Request, obj any) error {
|
|||||||
if err := req.ParseForm(); err != nil {
|
if err := req.ParseForm(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := req.ParseMultipartForm(defaultMemory); err != nil && !errors.Is(err, http.ErrNotMultipart) {
|
if err := parseMultipartForm(req, defaultMemory); err != nil && !errors.Is(err, http.ErrNotMultipart) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := mapForm(obj, req.Form); err != nil {
|
if err := mapForm(obj, req.Form); err != nil {
|
||||||
@ -53,7 +53,7 @@ func (formMultipartBinding) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (formMultipartBinding) Bind(req *http.Request, obj any) error {
|
func (formMultipartBinding) Bind(req *http.Request, obj any) error {
|
||||||
if err := req.ParseMultipartForm(defaultMemory); err != nil {
|
if err := parseMultipartForm(req, defaultMemory); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := mappingByPtr(obj, (*multipartRequest)(req), "form"); err != nil {
|
if err := mappingByPtr(obj, (*multipartRequest)(req), "form"); err != nil {
|
||||||
|
|||||||
56
binding/multipart_form.go
Normal file
56
binding/multipart_form.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// Copyright 2026 Gin Core Team. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package binding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseMultipartForm(req *http.Request, maxMemory int64) error {
|
||||||
|
err := req.ParseMultipartForm(maxMemory)
|
||||||
|
if err == nil || !errors.Is(err, http.ErrNotMultipart) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaType, _, parseErr := mime.ParseMediaType(req.Header.Get("Content-Type"))
|
||||||
|
if parseErr != nil || mediaType != MIMEMultipartMixed {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, readerErr := req.MultipartReader()
|
||||||
|
if readerErr != nil {
|
||||||
|
return readerErr
|
||||||
|
}
|
||||||
|
|
||||||
|
form, readErr := reader.ReadForm(maxMemory)
|
||||||
|
if readErr != nil {
|
||||||
|
return readErr
|
||||||
|
}
|
||||||
|
req.MultipartForm = form
|
||||||
|
|
||||||
|
if req.PostForm == nil {
|
||||||
|
req.PostForm = make(url.Values)
|
||||||
|
}
|
||||||
|
for key, values := range form.Value {
|
||||||
|
req.PostForm[key] = append(req.PostForm[key], values...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Form == nil {
|
||||||
|
req.Form = make(url.Values, len(req.PostForm))
|
||||||
|
for key, values := range req.PostForm {
|
||||||
|
req.Form[key] = append(req.Form[key], values...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.URL != nil {
|
||||||
|
for key, values := range req.URL.Query() {
|
||||||
|
req.Form[key] = append(req.Form[key], values...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -36,6 +36,7 @@ const (
|
|||||||
MIMEPlain = binding.MIMEPlain
|
MIMEPlain = binding.MIMEPlain
|
||||||
MIMEPOSTForm = binding.MIMEPOSTForm
|
MIMEPOSTForm = binding.MIMEPOSTForm
|
||||||
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
|
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
|
||||||
|
MIMEMultipartMixed = binding.MIMEMultipartMixed
|
||||||
MIMEYAML = binding.MIMEYAML
|
MIMEYAML = binding.MIMEYAML
|
||||||
MIMEYAML2 = binding.MIMEYAML2
|
MIMEYAML2 = binding.MIMEYAML2
|
||||||
MIMETOML = binding.MIMETOML
|
MIMETOML = binding.MIMETOML
|
||||||
@ -639,7 +640,7 @@ func (c *Context) initFormCache() {
|
|||||||
if c.formCache == nil {
|
if c.formCache == nil {
|
||||||
c.formCache = make(url.Values)
|
c.formCache = make(url.Values)
|
||||||
req := c.Request
|
req := c.Request
|
||||||
if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
|
if err := parseMultipartForm(req, c.engine.MaxMultipartMemory); err != nil {
|
||||||
if !errors.Is(err, http.ErrNotMultipart) {
|
if !errors.Is(err, http.ErrNotMultipart) {
|
||||||
debugPrint("error on parse multipart form array: %v", err)
|
debugPrint("error on parse multipart form array: %v", err)
|
||||||
}
|
}
|
||||||
@ -697,7 +698,7 @@ func getMapFromFormData(m map[string][]string, key string) (map[string]string, b
|
|||||||
// FormFile returns the first file for the provided form key.
|
// FormFile returns the first file for the provided form key.
|
||||||
func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {
|
func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {
|
||||||
if c.Request.MultipartForm == nil {
|
if c.Request.MultipartForm == nil {
|
||||||
if err := c.Request.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
|
if err := parseMultipartForm(c.Request, c.engine.MaxMultipartMemory); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -711,7 +712,7 @@ func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {
|
|||||||
|
|
||||||
// MultipartForm is the parsed multipart form, including file uploads.
|
// MultipartForm is the parsed multipart form, including file uploads.
|
||||||
func (c *Context) MultipartForm() (*multipart.Form, error) {
|
func (c *Context) MultipartForm() (*multipart.Form, error) {
|
||||||
err := c.Request.ParseMultipartForm(c.engine.MaxMultipartMemory)
|
err := parseMultipartForm(c.Request, c.engine.MaxMultipartMemory)
|
||||||
return c.Request.MultipartForm, err
|
return c.Request.MultipartForm, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -199,6 +199,58 @@ func TestContextMultipartForm(t *testing.T) {
|
|||||||
require.NoError(t, c.SaveUploadedFile(f.File["file"][0], "test"))
|
require.NoError(t, c.SaveUploadedFile(f.File["file"][0], "test"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContextFormFileMixed(t *testing.T) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
mw := multipart.NewWriter(buf)
|
||||||
|
w, err := mw.CreateFormFile("file", "mixed-test.txt")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = w.Write([]byte("mixed-content"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, mw.Close())
|
||||||
|
|
||||||
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
|
c.Request, _ = http.NewRequest(http.MethodPost, "/", buf)
|
||||||
|
c.Request.Header.Set("Content-Type", MIMEMultipartMixed+"; boundary="+mw.Boundary())
|
||||||
|
|
||||||
|
f, err := c.FormFile("file")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "mixed-test.txt", f.Filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextMultipartFormMixed(t *testing.T) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
mw := multipart.NewWriter(buf)
|
||||||
|
require.NoError(t, mw.WriteField("foo", "bar"))
|
||||||
|
w, err := mw.CreateFormFile("file", "mixed-test.txt")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = w.Write([]byte("mixed-content"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, mw.Close())
|
||||||
|
|
||||||
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
|
c.Request, _ = http.NewRequest(http.MethodPost, "/", buf)
|
||||||
|
c.Request.Header.Set("Content-Type", MIMEMultipartMixed+"; boundary="+mw.Boundary())
|
||||||
|
|
||||||
|
f, err := c.MultipartForm()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, f)
|
||||||
|
assert.Equal(t, []string{"bar"}, f.Value["foo"])
|
||||||
|
assert.Len(t, f.File["file"], 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextPostFormMixed(t *testing.T) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
mw := multipart.NewWriter(buf)
|
||||||
|
require.NoError(t, mw.WriteField("type", "image"))
|
||||||
|
require.NoError(t, mw.Close())
|
||||||
|
|
||||||
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
|
c.Request, _ = http.NewRequest(http.MethodPost, "/", buf)
|
||||||
|
c.Request.Header.Set("Content-Type", MIMEMultipartMixed+"; boundary="+mw.Boundary())
|
||||||
|
|
||||||
|
assert.Equal(t, "image", c.PostForm("type"))
|
||||||
|
}
|
||||||
|
|
||||||
func TestSaveUploadedOpenFailed(t *testing.T) {
|
func TestSaveUploadedOpenFailed(t *testing.T) {
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
mw := multipart.NewWriter(buf)
|
mw := multipart.NewWriter(buf)
|
||||||
|
|||||||
56
multipart_form.go
Normal file
56
multipart_form.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// Copyright 2026 Gin Core Team. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package gin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseMultipartForm(req *http.Request, maxMemory int64) error {
|
||||||
|
err := req.ParseMultipartForm(maxMemory)
|
||||||
|
if err == nil || !errors.Is(err, http.ErrNotMultipart) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaType, _, parseErr := mime.ParseMediaType(req.Header.Get("Content-Type"))
|
||||||
|
if parseErr != nil || mediaType != MIMEMultipartMixed {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, readerErr := req.MultipartReader()
|
||||||
|
if readerErr != nil {
|
||||||
|
return readerErr
|
||||||
|
}
|
||||||
|
|
||||||
|
form, readErr := reader.ReadForm(maxMemory)
|
||||||
|
if readErr != nil {
|
||||||
|
return readErr
|
||||||
|
}
|
||||||
|
req.MultipartForm = form
|
||||||
|
|
||||||
|
if req.PostForm == nil {
|
||||||
|
req.PostForm = make(url.Values)
|
||||||
|
}
|
||||||
|
for key, values := range form.Value {
|
||||||
|
req.PostForm[key] = append(req.PostForm[key], values...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Form == nil {
|
||||||
|
req.Form = make(url.Values, len(req.PostForm))
|
||||||
|
for key, values := range req.PostForm {
|
||||||
|
req.Form[key] = append(req.Form[key], values...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.URL != nil {
|
||||||
|
for key, values := range req.URL.Query() {
|
||||||
|
req.Form[key] = append(req.Form[key], values...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user