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"
|
||||
MIMEPOSTForm = "application/x-www-form-urlencoded"
|
||||
MIMEMultipartPOSTForm = "multipart/form-data"
|
||||
MIMEMultipartMixed = "multipart/mixed"
|
||||
MIMEPROTOBUF = "application/x-protobuf"
|
||||
MIMEMSGPACK = "application/x-msgpack"
|
||||
MIMEMSGPACK2 = "application/msgpack"
|
||||
@ -110,7 +111,7 @@ func Default(method, contentType string) Binding {
|
||||
return YAML
|
||||
case MIMETOML:
|
||||
return TOML
|
||||
case MIMEMultipartPOSTForm:
|
||||
case MIMEMultipartPOSTForm, MIMEMultipartMixed:
|
||||
return FormMultipart
|
||||
case MIMEBSON:
|
||||
return BSON
|
||||
|
||||
@ -17,6 +17,7 @@ const (
|
||||
MIMEPlain = "text/plain"
|
||||
MIMEPOSTForm = "application/x-www-form-urlencoded"
|
||||
MIMEMultipartPOSTForm = "multipart/form-data"
|
||||
MIMEMultipartMixed = "multipart/mixed"
|
||||
MIMEPROTOBUF = "application/x-protobuf"
|
||||
MIMEYAML = "application/x-yaml"
|
||||
MIMEYAML2 = "application/yaml"
|
||||
@ -102,7 +103,7 @@ func Default(method, contentType string) Binding {
|
||||
return ProtoBuf
|
||||
case MIMEYAML, MIMEYAML2:
|
||||
return YAML
|
||||
case MIMEMultipartPOSTForm:
|
||||
case MIMEMultipartPOSTForm, MIMEMultipartMixed:
|
||||
return FormMultipart
|
||||
case MIMETOML:
|
||||
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.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.MethodPut, MIMEPROTOBUF))
|
||||
@ -593,6 +595,21 @@ func createFormMultipartRequest(t *testing.T) *http.Request {
|
||||
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 {
|
||||
boundary := "--testboundary"
|
||||
body := new(bytes.Buffer)
|
||||
@ -631,6 +648,16 @@ func TestBindingFormPost(t *testing.T) {
|
||||
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) {
|
||||
req := createDefaultFormPostRequest(t)
|
||||
var obj FooDefaultBarStruct
|
||||
@ -932,6 +959,15 @@ func TestFormBindingMultipartFail(t *testing.T) {
|
||||
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) {
|
||||
b := FormPost
|
||||
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 {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
if err := req.ParseMultipartForm(defaultMemory); err != nil {
|
||||
if err := parseMultipartForm(req, defaultMemory); err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
MIMEPOSTForm = binding.MIMEPOSTForm
|
||||
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
|
||||
MIMEMultipartMixed = binding.MIMEMultipartMixed
|
||||
MIMEYAML = binding.MIMEYAML
|
||||
MIMEYAML2 = binding.MIMEYAML2
|
||||
MIMETOML = binding.MIMETOML
|
||||
@ -639,7 +640,7 @@ func (c *Context) initFormCache() {
|
||||
if c.formCache == nil {
|
||||
c.formCache = make(url.Values)
|
||||
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) {
|
||||
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.
|
||||
func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -711,7 +712,7 @@ func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {
|
||||
|
||||
// MultipartForm is the parsed multipart form, including file uploads.
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@ -199,6 +199,58 @@ func TestContextMultipartForm(t *testing.T) {
|
||||
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) {
|
||||
buf := new(bytes.Buffer)
|
||||
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