Merge 0cf6c8742be087ec36d181e4fe96d59ac0bcd01f into 8d0468f72897652485933b845253386f9147a8bf

This commit is contained in:
ljluestc 2026-06-02 21:51:40 +08:00 committed by GitHub
commit c182026d64
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 210 additions and 7 deletions

View File

@ -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

View File

@ -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

View File

@ -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())

View File

@ -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
View 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
}

View File

@ -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
} }

View File

@ -200,6 +200,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
View 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
}