mirror of
https://github.com/gin-gonic/gin.git
synced 2026-07-08 03:31:07 +08:00
Compare commits
11 Commits
56fca4b81f
...
8a07c82f7b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a07c82f7b | ||
|
|
5dd833f1f2 | ||
|
|
48a5dca087 | ||
|
|
0bd10a84f9 | ||
|
|
4dec17afdf | ||
|
|
731374fb36 | ||
|
|
8ca975441f | ||
|
|
39858a0859 | ||
|
|
ed150e7254 | ||
|
|
234a6d4c00 | ||
|
|
df2753778e |
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@ -37,7 +37,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@ -46,4 +46,4 @@ jobs:
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
|
||||
10
.github/workflows/gin.yml
vendored
10
.github/workflows/gin.yml
vendored
@ -33,7 +33,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
go: ["1.23", "1.24", "1.25"]
|
||||
go: ["1.24", "1.25"]
|
||||
test-tags:
|
||||
[
|
||||
"",
|
||||
@ -92,8 +92,8 @@ jobs:
|
||||
- name: Run Trivy vulnerability scanner in repo mode
|
||||
uses: aquasecurity/trivy-action@0.33.1
|
||||
with:
|
||||
scan-type: 'fs'
|
||||
scan-type: "fs"
|
||||
ignore-unfixed: true
|
||||
format: 'table'
|
||||
exit-code: '1'
|
||||
severity: 'CRITICAL,HIGH,MEDIUM'
|
||||
format: "table"
|
||||
exit-code: "1"
|
||||
severity: "CRITICAL,HIGH,MEDIUM"
|
||||
|
||||
@ -43,7 +43,7 @@ Gin combines the simplicity of Express.js-style routing with Go's performance ch
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Go version**: Gin requires [Go](https://go.dev/) version [1.23](https://go.dev/doc/devel/release#go1.23.0) or above
|
||||
- **Go version**: Gin requires [Go](https://go.dev/) version [1.24](https://go.dev/doc/devel/release#go1.24.0) or above
|
||||
- **Basic Go knowledge**: Familiarity with Go syntax and package management is helpful
|
||||
|
||||
### Installation
|
||||
|
||||
@ -87,7 +87,7 @@ func BenchmarkOneRouteString(B *testing.B) {
|
||||
runRequest(B, router, http.MethodGet, "/text")
|
||||
}
|
||||
|
||||
func BenchmarkManyRoutesFist(B *testing.B) {
|
||||
func BenchmarkManyRoutesFirst(B *testing.B) {
|
||||
router := New()
|
||||
router.Any("/ping", func(c *Context) {})
|
||||
runRequest(B, router, http.MethodGet, "/ping")
|
||||
|
||||
@ -5,8 +5,10 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"mime/multipart"
|
||||
"reflect"
|
||||
"strconv"
|
||||
@ -136,6 +138,7 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
|
||||
type setOptions struct {
|
||||
isDefaultExists bool
|
||||
defaultValue string
|
||||
parser string
|
||||
}
|
||||
|
||||
func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
|
||||
@ -167,6 +170,8 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
|
||||
setOpt.defaultValue = strings.ReplaceAll(v, ";", ",")
|
||||
}
|
||||
}
|
||||
} else if k, v = head(opt, "="); k == "parser" {
|
||||
setOpt.parser = v
|
||||
}
|
||||
}
|
||||
|
||||
@ -190,6 +195,20 @@ func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// trySetUsingParser tries to set a custom type value based on the presence of the "parser" tag on the field.
|
||||
// If the parser tag does not exist or does not match any of the supported parsers, gin will skip over this.
|
||||
func trySetUsingParser(val string, value reflect.Value, parser string) (isSet bool, err error) {
|
||||
switch parser {
|
||||
case "encoding.TextUnmarshaler":
|
||||
v, ok := value.Addr().Interface().(encoding.TextUnmarshaler)
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
return true, v.UnmarshalText([]byte(val))
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func trySplit(vs []string, field reflect.StructField) (newVs []string, err error) {
|
||||
cfTag := field.Tag.Get("collection_format")
|
||||
if cfTag == "" || cfTag == "multi" {
|
||||
@ -207,7 +226,7 @@ func trySplit(vs []string, field reflect.StructField) (newVs []string, err error
|
||||
case "pipes":
|
||||
sep = "|"
|
||||
default:
|
||||
return vs, fmt.Errorf("%s is not supported in the collection_format. (csv, ssv, pipes)", cfTag)
|
||||
return vs, fmt.Errorf("%s is not supported in the collection_format. (multi, csv, ssv, tsv, pipes)", cfTag)
|
||||
}
|
||||
|
||||
totalLength := 0
|
||||
@ -230,7 +249,7 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
||||
|
||||
switch value.Kind() {
|
||||
case reflect.Slice:
|
||||
if !ok {
|
||||
if !ok || len(vs) == 0 || (len(vs) > 0 && vs[0] == "") {
|
||||
vs = []string{opt.defaultValue}
|
||||
|
||||
// pre-process the default value for multi if present
|
||||
@ -240,7 +259,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
||||
}
|
||||
}
|
||||
|
||||
if ok, err = trySetCustom(vs[0], value); ok {
|
||||
if ok, err = trySetUsingParser(vs[0], value, opt.parser); ok {
|
||||
return ok, err
|
||||
} else if ok, err = trySetCustom(vs[0], value); ok {
|
||||
return ok, err
|
||||
}
|
||||
|
||||
@ -248,9 +269,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, setSlice(vs, value, field)
|
||||
return true, setSlice(vs, value, field, opt)
|
||||
case reflect.Array:
|
||||
if !ok {
|
||||
if !ok || len(vs) == 0 || (len(vs) > 0 && vs[0] == "") {
|
||||
vs = []string{opt.defaultValue}
|
||||
|
||||
// pre-process the default value for multi if present
|
||||
@ -260,7 +281,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
||||
}
|
||||
}
|
||||
|
||||
if ok, err = trySetCustom(vs[0], value); ok {
|
||||
if ok, err = trySetUsingParser(vs[0], value, opt.parser); ok {
|
||||
return ok, err
|
||||
} else if ok, err = trySetCustom(vs[0], value); ok {
|
||||
return ok, err
|
||||
}
|
||||
|
||||
@ -272,27 +295,32 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
||||
return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
|
||||
}
|
||||
|
||||
return true, setArray(vs, value, field)
|
||||
return true, setArray(vs, value, field, opt)
|
||||
default:
|
||||
var val string
|
||||
if !ok {
|
||||
if !ok || len(vs) == 0 || (len(vs) > 0 && vs[0] == "") {
|
||||
val = opt.defaultValue
|
||||
} else if len(vs) > 0 {
|
||||
val = vs[0]
|
||||
}
|
||||
|
||||
if len(vs) > 0 {
|
||||
val = vs[0]
|
||||
if val == "" {
|
||||
val = opt.defaultValue
|
||||
}
|
||||
}
|
||||
if ok, err := trySetCustom(val, value); ok {
|
||||
if ok, err = trySetUsingParser(val, value, opt.parser); ok {
|
||||
return ok, err
|
||||
} else if ok, err = trySetCustom(val, value); ok {
|
||||
return ok, err
|
||||
}
|
||||
return true, setWithProperType(val, value, field)
|
||||
return true, setWithProperType(val, value, field, opt)
|
||||
}
|
||||
}
|
||||
|
||||
func setWithProperType(val string, value reflect.Value, field reflect.StructField) error {
|
||||
func setWithProperType(val string, value reflect.Value, field reflect.StructField, opt setOptions) error {
|
||||
// this if-check is required for parsing nested types like []MyId, where MyId is [12]byte
|
||||
if ok, err := trySetUsingParser(val, value, opt.parser); ok {
|
||||
return err
|
||||
} else if ok, err = trySetCustom(val, value); ok {
|
||||
return err
|
||||
}
|
||||
|
||||
switch value.Kind() {
|
||||
case reflect.Int:
|
||||
return setIntField(val, 0, value)
|
||||
@ -340,7 +368,7 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
|
||||
if !value.Elem().IsValid() {
|
||||
value.Set(reflect.New(value.Type().Elem()))
|
||||
}
|
||||
return setWithProperType(val, value.Elem(), field)
|
||||
return setWithProperType(val, value.Elem(), field, opt)
|
||||
default:
|
||||
return errUnknownType
|
||||
}
|
||||
@ -447,9 +475,9 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
|
||||
return nil
|
||||
}
|
||||
|
||||
func setArray(vals []string, value reflect.Value, field reflect.StructField) error {
|
||||
func setArray(vals []string, value reflect.Value, field reflect.StructField, opt setOptions) error {
|
||||
for i, s := range vals {
|
||||
err := setWithProperType(s, value.Index(i), field)
|
||||
err := setWithProperType(s, value.Index(i), field, opt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -457,9 +485,9 @@ func setArray(vals []string, value reflect.Value, field reflect.StructField) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func setSlice(vals []string, value reflect.Value, field reflect.StructField) error {
|
||||
func setSlice(vals []string, value reflect.Value, field reflect.StructField, opt setOptions) error {
|
||||
slice := reflect.MakeSlice(value.Type(), len(vals), len(vals))
|
||||
err := setArray(vals, slice, field)
|
||||
err := setArray(vals, slice, field, opt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -489,9 +517,7 @@ func setFormMap(ptr any, form map[string][]string) error {
|
||||
if !ok {
|
||||
return ErrConvertMapStringSlice
|
||||
}
|
||||
for k, v := range form {
|
||||
ptrMap[k] = v
|
||||
}
|
||||
maps.Copy(ptrMap, form)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"mime/multipart"
|
||||
@ -454,44 +455,44 @@ func TestMappingIgnoredCircularRef(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
type customUnmarshalParamHex int
|
||||
// ==== BindUmarshaler tests START ====
|
||||
|
||||
func (f *customUnmarshalParamHex) UnmarshalParam(param string) error {
|
||||
type customHexUnmarshalParam int
|
||||
|
||||
func (f *customHexUnmarshalParam) UnmarshalParam(param string) error {
|
||||
v, err := strconv.ParseInt(param, 16, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*f = customUnmarshalParamHex(v)
|
||||
*f = customHexUnmarshalParam(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestMappingCustomUnmarshalParamHexWithFormTag(t *testing.T) {
|
||||
var s struct {
|
||||
Foo customUnmarshalParamHex `form:"foo"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "form")
|
||||
require.NoError(t, err)
|
||||
func TestMappingCustomHexUnmarshalParam(t *testing.T) {
|
||||
RunMappingUsingUriAndFormTagAndAssertForUnmarshalParam[customHexUnmarshalParam](
|
||||
t,
|
||||
`f5`,
|
||||
func(hex customHexUnmarshalParam, t *testing.T) {
|
||||
assert.EqualValues(t, 245, hex)
|
||||
},
|
||||
)
|
||||
|
||||
assert.EqualValues(t, 245, s.Foo)
|
||||
// verify default binding works with UnmarshalParam
|
||||
var sDefaultValue struct {
|
||||
Field1 customHexUnmarshalParam `form:"field1,default=f5"`
|
||||
}
|
||||
err := mappingByPtr(&sDefaultValue, formSource{"field1": {}}, "form")
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 0xf5, sDefaultValue.Field1)
|
||||
}
|
||||
|
||||
func TestMappingCustomUnmarshalParamHexWithURITag(t *testing.T) {
|
||||
var s struct {
|
||||
Foo customUnmarshalParamHex `uri:"foo"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "uri")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, 245, s.Foo)
|
||||
}
|
||||
|
||||
type customUnmarshalParamType struct {
|
||||
type customTypeUnmarshalParam struct {
|
||||
Protocol string
|
||||
Path string
|
||||
Name string
|
||||
}
|
||||
|
||||
func (f *customUnmarshalParamType) UnmarshalParam(param string) error {
|
||||
func (f *customTypeUnmarshalParam) UnmarshalParam(param string) error {
|
||||
parts := strings.Split(param, ":")
|
||||
if len(parts) != 3 {
|
||||
return errors.New("invalid format")
|
||||
@ -502,52 +503,28 @@ func (f *customUnmarshalParamType) UnmarshalParam(param string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestMappingCustomStructTypeWithFormTag(t *testing.T) {
|
||||
var s struct {
|
||||
FileData customUnmarshalParamType `form:"data"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "file", s.FileData.Protocol)
|
||||
assert.Equal(t, "/foo", s.FileData.Path)
|
||||
assert.Equal(t, "happiness", s.FileData.Name)
|
||||
func TestMappingCustomStructType(t *testing.T) {
|
||||
RunMappingUsingUriAndFormTagAndAssertForUnmarshalParam[customTypeUnmarshalParam](
|
||||
t,
|
||||
`file:/foo:happiness`,
|
||||
func(data customTypeUnmarshalParam, t *testing.T) {
|
||||
assert.Equal(t, "file", data.Protocol)
|
||||
assert.Equal(t, "/foo", data.Path)
|
||||
assert.Equal(t, "happiness", data.Name)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestMappingCustomStructTypeWithURITag(t *testing.T) {
|
||||
var s struct {
|
||||
FileData customUnmarshalParamType `uri:"data"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "file", s.FileData.Protocol)
|
||||
assert.Equal(t, "/foo", s.FileData.Path)
|
||||
assert.Equal(t, "happiness", s.FileData.Name)
|
||||
}
|
||||
|
||||
func TestMappingCustomPointerStructTypeWithFormTag(t *testing.T) {
|
||||
var s struct {
|
||||
FileData *customUnmarshalParamType `form:"data"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "file", s.FileData.Protocol)
|
||||
assert.Equal(t, "/foo", s.FileData.Path)
|
||||
assert.Equal(t, "happiness", s.FileData.Name)
|
||||
}
|
||||
|
||||
func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) {
|
||||
var s struct {
|
||||
FileData *customUnmarshalParamType `uri:"data"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "file", s.FileData.Protocol)
|
||||
assert.Equal(t, "/foo", s.FileData.Path)
|
||||
assert.Equal(t, "happiness", s.FileData.Name)
|
||||
func TestMappingCustomPointerStructType(t *testing.T) {
|
||||
RunMappingUsingUriAndFormTagAndAssertForUnmarshalParam[*customTypeUnmarshalParam](
|
||||
t,
|
||||
`file:/foo:happiness`,
|
||||
func(data *customTypeUnmarshalParam, t *testing.T) {
|
||||
assert.Equal(t, "file", data.Protocol)
|
||||
assert.Equal(t, "/foo", data.Path)
|
||||
assert.Equal(t, "happiness", data.Name)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
type customPath []string
|
||||
@ -563,32 +540,49 @@ func (p *customPath) UnmarshalParam(param string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestMappingCustomSliceUri(t *testing.T) {
|
||||
var s struct {
|
||||
FileData customPath `uri:"path"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "uri")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "bar", s.FileData[0])
|
||||
assert.Equal(t, "foo", s.FileData[1])
|
||||
func TestMappingCustomSlice(t *testing.T) {
|
||||
RunMappingUsingUriAndFormTagAndAssertForUnmarshalParam[customPath](
|
||||
t,
|
||||
`bar/foo`,
|
||||
func(path customPath, t *testing.T) {
|
||||
assert.Equal(t, "bar", path[0])
|
||||
assert.Equal(t, "foo", path[1])
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestMappingCustomSliceForm(t *testing.T) {
|
||||
var s struct {
|
||||
FileData customPath `form:"path"`
|
||||
func TestMappingCustomSliceStopsWhenError(t *testing.T) {
|
||||
var sForm struct {
|
||||
Field1 customPath `form:"field1"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "form")
|
||||
require.NoError(t, err)
|
||||
err := mappingByPtr(&sForm, formSource{"field1": {"invalid"}}, "form")
|
||||
require.ErrorContains(t, err, "invalid format")
|
||||
require.Empty(t, sForm.Field1)
|
||||
}
|
||||
|
||||
assert.Equal(t, "bar", s.FileData[0])
|
||||
assert.Equal(t, "foo", s.FileData[1])
|
||||
func TestMappingCustomSliceOfSlice(t *testing.T) {
|
||||
val := `bar/foo,bar/foo/spam`
|
||||
expected := []customPath{{"bar", "foo"}, {"bar", "foo", "spam"}}
|
||||
|
||||
var sUri struct {
|
||||
Field1 []customPath `uri:"field1" collection_format:"csv"`
|
||||
}
|
||||
err := mappingByPtr(&sUri, formSource{"field1": {val}}, "uri")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected, sUri.Field1)
|
||||
|
||||
var sForm struct {
|
||||
Field1 []customPath `form:"field1" collection_format:"csv"`
|
||||
}
|
||||
err = mappingByPtr(&sForm, formSource{"field1": {val}}, "form")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected, sForm.Field1)
|
||||
}
|
||||
|
||||
type objectID [12]byte
|
||||
|
||||
func (o *objectID) UnmarshalParam(param string) error {
|
||||
oid, err := convertTo(param)
|
||||
oid, err := convertTo[objectID](param)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -597,8 +591,8 @@ func (o *objectID) UnmarshalParam(param string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertTo(s string) (objectID, error) {
|
||||
var nilObjectID objectID
|
||||
func convertTo[T ~[12]byte](s string) (T, error) {
|
||||
var nilObjectID T
|
||||
if len(s) != 24 {
|
||||
return nilObjectID, errors.New("invalid format")
|
||||
}
|
||||
@ -612,26 +606,350 @@ func convertTo(s string) (objectID, error) {
|
||||
return oid, nil
|
||||
}
|
||||
|
||||
func TestMappingCustomArrayUri(t *testing.T) {
|
||||
var s struct {
|
||||
FileData objectID `uri:"id"`
|
||||
}
|
||||
val := `664a062ac74a8ad104e0e80f`
|
||||
err := mappingByPtr(&s, formSource{"id": {val}}, "uri")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected, _ := convertTo(val)
|
||||
assert.Equal(t, expected, s.FileData)
|
||||
func TestMappingCustomArray(t *testing.T) {
|
||||
RunMappingUsingUriAndFormTagAndAssertForUnmarshalParam[objectID](
|
||||
t,
|
||||
`664a062ac74a8ad104e0e80f`,
|
||||
func(oid objectID, t *testing.T) {
|
||||
expected, _ := convertTo[objectID](`664a062ac74a8ad104e0e80f`)
|
||||
assert.Equal(t, expected, oid)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestMappingCustomArrayForm(t *testing.T) {
|
||||
var s struct {
|
||||
FileData objectID `form:"id"`
|
||||
func TestMappingCustomArrayOfArray(t *testing.T) {
|
||||
val := `664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`
|
||||
expected1, _ := convertTo[objectID](`664a062ac74a8ad104e0e80e`)
|
||||
expected2, _ := convertTo[objectID](`664a062ac74a8ad104e0e80f`)
|
||||
expected := []objectID{expected1, expected2}
|
||||
|
||||
var sUri struct {
|
||||
Field1 []objectID `uri:"field1" collection_format:"csv"`
|
||||
}
|
||||
val := `664a062ac74a8ad104e0e80f`
|
||||
err := mappingByPtr(&s, formSource{"id": {val}}, "form")
|
||||
err := mappingByPtr(&sUri, formSource{"field1": {val}}, "uri")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected, sUri.Field1)
|
||||
|
||||
var sForm struct {
|
||||
Field1 []objectID `form:"field1" collection_format:"csv"`
|
||||
}
|
||||
err = mappingByPtr(&sForm, formSource{"field1": {val}}, "form")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected, sForm.Field1)
|
||||
|
||||
var sDefaultValue struct {
|
||||
Field1 []objectID `form:"field1,default=664a062ac74a8ad104e0e80e;664a062ac74a8ad104e0e80f" collection_format:"csv"`
|
||||
}
|
||||
err = mappingByPtr(&sDefaultValue, formSource{"field1": {}}, "form")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected, sDefaultValue.Field1)
|
||||
}
|
||||
|
||||
// RunMappingUsingUriAndFormTagAndAssertForUnmarshalParam declares a struct with a field of the given generic type T
|
||||
// and runs a mapping test using the given value for both the uri and form tag. Any asserts that should be done on the
|
||||
// result are passed as a function in the last parameter.
|
||||
//
|
||||
// This method eliminates the need for writing duplicate tests to verify both form+uri tags for BindUnmarshaler tests
|
||||
func RunMappingUsingUriAndFormTagAndAssertForUnmarshalParam[T any](
|
||||
t *testing.T,
|
||||
valueToBind string,
|
||||
assertsToRunAfterBind func(T, *testing.T),
|
||||
) {
|
||||
var sUri struct {
|
||||
Field1 T `uri:"field1"`
|
||||
}
|
||||
err := mappingByPtr(&sUri, formSource{"field1": {valueToBind}}, "uri")
|
||||
require.NoError(t, err)
|
||||
assertsToRunAfterBind(sUri.Field1, t)
|
||||
|
||||
var sForm struct {
|
||||
Field1 T `form:"field1"`
|
||||
}
|
||||
err = mappingByPtr(&sForm, formSource{"field1": {valueToBind}}, "form")
|
||||
require.NoError(t, err)
|
||||
assertsToRunAfterBind(sForm.Field1, t)
|
||||
}
|
||||
|
||||
// ==== BindUmarshaler tests END ====
|
||||
|
||||
// ==== TextUnmarshaler tests START ====
|
||||
|
||||
type customHexUnmarshalText int
|
||||
|
||||
func (f *customHexUnmarshalText) UnmarshalText(text []byte) error {
|
||||
v, err := strconv.ParseInt(string(text), 16, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*f = customHexUnmarshalText(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
// verify type implements TextUnmarshaler
|
||||
var _ encoding.TextUnmarshaler = (*customHexUnmarshalText)(nil)
|
||||
|
||||
func TestMappingCustomHexUnmarshalText(t *testing.T) {
|
||||
RunMappingUsingUriAndFormTagAndAssertForUnmarshalText[customHexUnmarshalText](
|
||||
t,
|
||||
`f5`,
|
||||
func(hex customHexUnmarshalText, t *testing.T) {
|
||||
assert.EqualValues(t, 245, hex)
|
||||
},
|
||||
)
|
||||
|
||||
// verify default binding works with UnmarshalText
|
||||
var sDefaultValue struct {
|
||||
Field1 customHexUnmarshalText `form:"field1,default=f5,parser=encoding.TextUnmarshaler"`
|
||||
}
|
||||
err := mappingByPtr(&sDefaultValue, formSource{"field1": {}}, "form")
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 0xf5, sDefaultValue.Field1)
|
||||
}
|
||||
|
||||
type customTypeUnmarshalText struct {
|
||||
Protocol string
|
||||
Path string
|
||||
Name string
|
||||
}
|
||||
|
||||
func (f *customTypeUnmarshalText) UnmarshalText(text []byte) error {
|
||||
parts := strings.Split(string(text), ":")
|
||||
if len(parts) != 3 {
|
||||
return errors.New("invalid format")
|
||||
}
|
||||
f.Protocol = parts[0]
|
||||
f.Path = parts[1]
|
||||
f.Name = parts[2]
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ encoding.TextUnmarshaler = (*customTypeUnmarshalText)(nil)
|
||||
|
||||
func TestMappingCustomStructTypeUnmarshalText(t *testing.T) {
|
||||
RunMappingUsingUriAndFormTagAndAssertForUnmarshalText[customTypeUnmarshalText](
|
||||
t,
|
||||
`file:/foo:happiness`,
|
||||
func(data customTypeUnmarshalText, t *testing.T) {
|
||||
assert.Equal(t, "file", data.Protocol)
|
||||
assert.Equal(t, "/foo", data.Path)
|
||||
assert.Equal(t, "happiness", data.Name)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestMappingCustomPointerStructTypeUnmarshalText(t *testing.T) {
|
||||
RunMappingUsingUriAndFormTagAndAssertForUnmarshalText[*customTypeUnmarshalText](
|
||||
t,
|
||||
`file:/foo:happiness`,
|
||||
func(data *customTypeUnmarshalText, t *testing.T) {
|
||||
assert.Equal(t, "file", data.Protocol)
|
||||
assert.Equal(t, "/foo", data.Path)
|
||||
assert.Equal(t, "happiness", data.Name)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
type customPathUnmarshalText []string
|
||||
|
||||
func (p *customPathUnmarshalText) UnmarshalText(text []byte) error {
|
||||
elems := strings.Split(string(text), "/")
|
||||
n := len(elems)
|
||||
if n < 2 {
|
||||
return errors.New("invalid format")
|
||||
}
|
||||
|
||||
*p = elems
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ encoding.TextUnmarshaler = (*customPathUnmarshalText)(nil)
|
||||
|
||||
func TestMappingCustomSliceUnmarshalText(t *testing.T) {
|
||||
RunMappingUsingUriAndFormTagAndAssertForUnmarshalText[customPathUnmarshalText](
|
||||
t,
|
||||
`bar/foo`,
|
||||
func(path customPathUnmarshalText, t *testing.T) {
|
||||
assert.Equal(t, "bar", path[0])
|
||||
assert.Equal(t, "foo", path[1])
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestMappingCustomSliceUnmarshalTextStopsWhenError(t *testing.T) {
|
||||
var sForm struct {
|
||||
Field1 customPathUnmarshalText `form:"field1,parser=encoding.TextUnmarshaler"`
|
||||
}
|
||||
err := mappingByPtr(&sForm, formSource{"field1": {"invalid"}}, "form")
|
||||
require.ErrorContains(t, err, "invalid format")
|
||||
require.Empty(t, sForm.Field1)
|
||||
}
|
||||
|
||||
func TestMappingCustomSliceOfSliceUnmarshalText(t *testing.T) {
|
||||
val := `bar/foo,bar/foo/spam`
|
||||
expected := []customPathUnmarshalText{{"bar", "foo"}, {"bar", "foo", "spam"}}
|
||||
|
||||
var sUri struct {
|
||||
Field1 []customPathUnmarshalText `uri:"field1,parser=encoding.TextUnmarshaler" collection_format:"csv"`
|
||||
}
|
||||
err := mappingByPtr(&sUri, formSource{"field1": {val}}, "uri")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected, sUri.Field1)
|
||||
|
||||
var sForm struct {
|
||||
Field1 []customPathUnmarshalText `form:"field1,parser=encoding.TextUnmarshaler" collection_format:"csv"`
|
||||
}
|
||||
err = mappingByPtr(&sForm, formSource{"field1": {val}}, "form")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected, sForm.Field1)
|
||||
|
||||
var sDefaultValue struct {
|
||||
Field1 []customPathUnmarshalText `form:"field1,default=bar/foo;bar/foo/spam,parser=encoding.TextUnmarshaler" collection_format:"csv"`
|
||||
}
|
||||
err = mappingByPtr(&sDefaultValue, formSource{"field1": {}}, "form")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected, sDefaultValue.Field1)
|
||||
}
|
||||
|
||||
type objectIDUnmarshalText [12]byte
|
||||
|
||||
func (o *objectIDUnmarshalText) UnmarshalText(text []byte) error {
|
||||
oid, err := convertTo[objectIDUnmarshalText](string(text))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*o = oid
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ encoding.TextUnmarshaler = (*objectIDUnmarshalText)(nil)
|
||||
|
||||
func TestMappingCustomArrayUnmarshalText(t *testing.T) {
|
||||
RunMappingUsingUriAndFormTagAndAssertForUnmarshalText[objectIDUnmarshalText](
|
||||
t,
|
||||
`664a062ac74a8ad104e0e80f`,
|
||||
func(oid objectIDUnmarshalText, t *testing.T) {
|
||||
expected, _ := convertTo[objectIDUnmarshalText](`664a062ac74a8ad104e0e80f`)
|
||||
assert.Equal(t, expected, oid)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestMappingCustomArrayOfArrayUnmarshalText(t *testing.T) {
|
||||
val := `664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`
|
||||
expected1, _ := convertTo[objectIDUnmarshalText](`664a062ac74a8ad104e0e80e`)
|
||||
expected2, _ := convertTo[objectIDUnmarshalText](`664a062ac74a8ad104e0e80f`)
|
||||
expected := []objectIDUnmarshalText{expected1, expected2}
|
||||
|
||||
var sUri struct {
|
||||
Field1 []objectIDUnmarshalText `uri:"field1,parser=encoding.TextUnmarshaler" collection_format:"csv"`
|
||||
}
|
||||
err := mappingByPtr(&sUri, formSource{"field1": {val}}, "uri")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected, sUri.Field1)
|
||||
|
||||
var sForm struct {
|
||||
Field1 []objectIDUnmarshalText `form:"field1,parser=encoding.TextUnmarshaler" collection_format:"csv"`
|
||||
}
|
||||
err = mappingByPtr(&sForm, formSource{"field1": {val}}, "form")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected, sForm.Field1)
|
||||
|
||||
var sDefaultValue struct {
|
||||
Field1 []objectIDUnmarshalText `form:"field1,default=664a062ac74a8ad104e0e80e;664a062ac74a8ad104e0e80f,parser=encoding.TextUnmarshaler" collection_format:"csv"`
|
||||
}
|
||||
err = mappingByPtr(&sDefaultValue, formSource{"field1": {}}, "form")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected, sDefaultValue.Field1)
|
||||
}
|
||||
|
||||
// RunMappingUsingUriAndFormTagAndAssertForUnmarshalText declares a struct with a field of the given generic type T
|
||||
// and runs a mapping test using the given value for both the uri and form tag. Any asserts that should be done on the
|
||||
// result are passed as a function in the last parameter.
|
||||
//
|
||||
// This method eliminates the need for writing duplicate tests to verify both form+uri tags for TextUnmarshaler tests
|
||||
func RunMappingUsingUriAndFormTagAndAssertForUnmarshalText[T any](
|
||||
t *testing.T,
|
||||
valueToBind string,
|
||||
assertsToRunAfterBind func(T, *testing.T),
|
||||
) {
|
||||
var sUri struct {
|
||||
Field1 T `uri:"field1,parser=encoding.TextUnmarshaler"`
|
||||
}
|
||||
err := mappingByPtr(&sUri, formSource{"field1": {valueToBind}}, "uri")
|
||||
require.NoError(t, err)
|
||||
assertsToRunAfterBind(sUri.Field1, t)
|
||||
|
||||
var sForm struct {
|
||||
Field1 T `form:"field1,parser=encoding.TextUnmarshaler"`
|
||||
}
|
||||
err = mappingByPtr(&sForm, formSource{"field1": {valueToBind}}, "form")
|
||||
require.NoError(t, err)
|
||||
assertsToRunAfterBind(sForm.Field1, t)
|
||||
}
|
||||
|
||||
// If someone specifies parser=TextUnmarshaler and it's not defined for the type, gin should revert to using its default
|
||||
// binding logic.
|
||||
func TestMappingUsingBindUnmarshalerAndTextUnmarshalerWhenOnlyBindUnmarshalerDefined(t *testing.T) {
|
||||
var s struct {
|
||||
Hex customHexUnmarshalParam `form:"hex"`
|
||||
HexByUnmarshalText customHexUnmarshalParam `form:"hex2,parser=encoding.TextUnmarshaler"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{
|
||||
"hex": {`f5`},
|
||||
"hex2": {`f5`},
|
||||
}, "form")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected, _ := convertTo(val)
|
||||
assert.Equal(t, expected, s.FileData)
|
||||
assert.EqualValues(t, 0xf5, s.Hex)
|
||||
assert.EqualValues(t, 0xf5, s.HexByUnmarshalText) // reverts to BindUnmarshaler binding
|
||||
}
|
||||
|
||||
// If someone does not specify parser=TextUnmarshaler even when it's defined for the type, gin should ignore the
|
||||
// UnmarshalText logic and continue using its default binding logic. (This ensures gin does not break backwards
|
||||
// compatibility)
|
||||
func TestMappingUsingBindUnmarshalerAndTextUnmarshalerWhenOnlyTextUnmarshalerDefined(t *testing.T) {
|
||||
var s struct {
|
||||
Hex customHexUnmarshalText `form:"hex"`
|
||||
HexByUnmarshalText customHexUnmarshalText `form:"hex2,parser=encoding.TextUnmarshaler"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{
|
||||
"hex": {`11`},
|
||||
"hex2": {`11`},
|
||||
}, "form")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, 11, s.Hex) // this is using default int binding, not our custom hex binding. 0x11 should be 17 in decimal
|
||||
assert.EqualValues(t, 0x11, s.HexByUnmarshalText) // correct expected value for hex binding
|
||||
}
|
||||
|
||||
type customHexUnmarshalParamAndUnmarshalText int
|
||||
|
||||
func (f *customHexUnmarshalParamAndUnmarshalText) UnmarshalParam(param string) error {
|
||||
return errors.New("should not be called in unit test if parser tag present")
|
||||
}
|
||||
|
||||
func (f *customHexUnmarshalParamAndUnmarshalText) UnmarshalText(text []byte) error {
|
||||
v, err := strconv.ParseInt(string(text), 16, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*f = customHexUnmarshalParamAndUnmarshalText(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If a type has both UnmarshalParam and UnmarshalText methods defined, but the parser tag is set to TextUnmarshaler,
|
||||
// then only the UnmarshalText method should be invoked.
|
||||
func TestMappingUsingTextUnmarshalerWhenBindUnmarshalerAlsoDefined(t *testing.T) {
|
||||
var s struct {
|
||||
Hex customHexUnmarshalParamAndUnmarshalText `form:"hex,parser=encoding.TextUnmarshaler"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{
|
||||
"hex": {`f5`},
|
||||
}, "form")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, 0xf5, s.Hex)
|
||||
}
|
||||
|
||||
// ==== TextUnmarshaler tests END ====
|
||||
|
||||
@ -1233,7 +1233,7 @@ func TestContextRenderNoContentHTML(t *testing.T) {
|
||||
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
// TestContextXML tests that the response is serialized as XML
|
||||
// TestContextRenderXML tests that the response is serialized as XML
|
||||
// and Content-Type is set to application/xml
|
||||
func TestContextRenderXML(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
@ -3320,7 +3320,7 @@ func TestContextSetCookieData(t *testing.T) {
|
||||
assert.Contains(t, setCookie, "Max-Age=1")
|
||||
assert.Contains(t, setCookie, "HttpOnly")
|
||||
assert.Contains(t, setCookie, "Secure")
|
||||
// SameSite=Lax might be omitted in Go 1.23+ as it's the default
|
||||
// SameSite=Lax might be omitted in Go 1.24+ as it's the default
|
||||
// assert.Contains(t, setCookie, "SameSite=Lax")
|
||||
|
||||
// Test that when Path is empty, "/" is automatically set
|
||||
@ -3341,7 +3341,7 @@ func TestContextSetCookieData(t *testing.T) {
|
||||
assert.Contains(t, setCookie, "Max-Age=1")
|
||||
assert.Contains(t, setCookie, "HttpOnly")
|
||||
assert.Contains(t, setCookie, "Secure")
|
||||
// SameSite=Lax might be omitted in Go 1.23+ as it's the default
|
||||
// SameSite=Lax might be omitted in Go 1.24+ as it's the default
|
||||
// assert.Contains(t, setCookie, "SameSite=Lax")
|
||||
|
||||
// Test additional cookie attributes (Expires)
|
||||
@ -3364,7 +3364,7 @@ func TestContextSetCookieData(t *testing.T) {
|
||||
assert.Contains(t, setCookie, "Domain=localhost")
|
||||
assert.Contains(t, setCookie, "HttpOnly")
|
||||
assert.Contains(t, setCookie, "Secure")
|
||||
// SameSite=Lax might be omitted in Go 1.23+ as it's the default
|
||||
// SameSite=Lax might be omitted in Go 1.24+ as it's the default
|
||||
// assert.Contains(t, setCookie, "SameSite=Lax")
|
||||
|
||||
// Test for Partitioned attribute (Go 1.18+)
|
||||
@ -3384,7 +3384,7 @@ func TestContextSetCookieData(t *testing.T) {
|
||||
assert.Contains(t, setCookie, "Domain=localhost")
|
||||
assert.Contains(t, setCookie, "HttpOnly")
|
||||
assert.Contains(t, setCookie, "Secure")
|
||||
// SameSite=Lax might be omitted in Go 1.23+ as it's the default
|
||||
// SameSite=Lax might be omitted in Go 1.24+ as it's the default
|
||||
// assert.Contains(t, setCookie, "SameSite=Lax")
|
||||
// Not testing for Partitioned attribute as it may not be supported in all Go versions
|
||||
|
||||
|
||||
2
debug.go
2
debug.go
@ -78,7 +78,7 @@ func getMinVer(v string) (uint64, error) {
|
||||
|
||||
func debugPrintWARNINGDefault() {
|
||||
if v, e := getMinVer(runtime.Version()); e == nil && v < ginSupportMinGoVer {
|
||||
debugPrint(`[WARNING] Now Gin requires Go 1.23+.
|
||||
debugPrint(`[WARNING] Now Gin requires Go 1.24+.
|
||||
|
||||
`)
|
||||
}
|
||||
|
||||
@ -106,7 +106,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
|
||||
})
|
||||
m, e := getMinVer(runtime.Version())
|
||||
if e == nil && m < ginSupportMinGoVer {
|
||||
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.23+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
|
||||
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.24+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
|
||||
} else {
|
||||
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
|
||||
}
|
||||
|
||||
78
docs/doc.md
78
docs/doc.md
@ -911,7 +911,7 @@ curl -X POST http://localhost:8080/person
|
||||
|
||||
NOTE: For default [collection values](#collection-format-for-arrays), the following rules apply:
|
||||
- Since commas are used to delimit tag options, they are not supported within a default value and will result in undefined behavior
|
||||
- For the collection formats "multi" and "csv", a semicolon should be used in place of a comma to delimited default values
|
||||
- For the collection formats "multi" and "csv", a semicolon should be used in place of a comma to delimit default values
|
||||
- Since semicolons are used to delimit default values for "multi" and "csv", they are not supported within a default value for "multi" and "csv"
|
||||
|
||||
|
||||
@ -1009,12 +1009,68 @@ curl -v localhost:8088/thinkerou/not-uuid
|
||||
|
||||
### Bind custom unmarshaler
|
||||
|
||||
To override gin's default binding logic, define a function on your type that satisfies the `encoding.TextUnmarshaler` interface from the Golang standard library. Then specify `parser=encoding.TextUnmarshaler` in the `uri`/`form` tag of the field being bound.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"encoding"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Birthday string
|
||||
|
||||
func (b *Birthday) UnmarshalText(text []byte) error {
|
||||
*b = Birthday(strings.Replace(string(text), "-", "/", -1))
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ encoding.TextUnmarshaler = (*Birthday)(nil) //assert Birthday implements encoding.TextUnmarshaler
|
||||
|
||||
func main() {
|
||||
route := gin.Default()
|
||||
var request struct {
|
||||
Birthday Birthday `form:"birthday,parser=encoding.TextUnmarshaler"`
|
||||
Birthdays []Birthday `form:"birthdays,parser=encoding.TextUnmarshaler" collection_format:"csv"`
|
||||
BirthdaysDefault []Birthday `form:"birthdaysDef,default=2020-09-01;2020-09-02,parser=encoding.TextUnmarshaler" collection_format:"csv"`
|
||||
}
|
||||
route.GET("/test", func(ctx *gin.Context) {
|
||||
_ = ctx.BindQuery(&request)
|
||||
ctx.JSON(200, request)
|
||||
})
|
||||
_ = route.Run(":8088")
|
||||
}
|
||||
```
|
||||
|
||||
Test it with:
|
||||
|
||||
```sh
|
||||
curl 'localhost:8088/test?birthday=2000-01-01&birthdays=2000-01-01,2000-01-02'
|
||||
```
|
||||
Result
|
||||
```sh
|
||||
{"Birthday":"2000/01/01","Birthdays":["2000/01/01","2000/01/02"],"BirthdaysDefault":["2020/09/01","2020/09/02"]}
|
||||
```
|
||||
|
||||
Note:
|
||||
- If `parser=encoding.TextUnmarshaler` is specified for a type that does **not** implement `encoding.TextUnmarshaler`, gin will ignore it and proceed with its default binding logic.
|
||||
- If `parser=encoding.TextUnmarshaler` is specified for a type and that type's implementation of `encoding.TextUnmarshaler` returns an error, gin will stop binding and return the error to the client.
|
||||
|
||||
---
|
||||
|
||||
If a type implements `encoding.TextUnmarshaler` but you still want to customize how gin binds the type separately (eg to change what error message is returned), you can implement the dedicated `BindUnmarshaler` interface provided by gin.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
)
|
||||
|
||||
type Birthday string
|
||||
@ -1024,29 +1080,37 @@ func (b *Birthday) UnmarshalParam(param string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ binding.BindUnmarshaler = (*Birthday)(nil) //assert Birthday implements binding.BindUnmarshaler
|
||||
|
||||
func main() {
|
||||
route := gin.Default()
|
||||
var request struct {
|
||||
Birthday Birthday `form:"birthday"`
|
||||
Birthday Birthday `form:"birthday"`
|
||||
Birthdays []Birthday `form:"birthdays" collection_format:"csv"`
|
||||
BirthdaysDefault []Birthday `form:"birthdaysDef,default=2020-09-01;2020-09-02" collection_format:"csv"`
|
||||
}
|
||||
route.GET("/test", func(ctx *gin.Context) {
|
||||
_ = ctx.BindQuery(&request)
|
||||
ctx.JSON(200, request.Birthday)
|
||||
ctx.JSON(200, request)
|
||||
})
|
||||
route.Run(":8088")
|
||||
_ = route.Run(":8088")
|
||||
}
|
||||
```
|
||||
|
||||
Test it with:
|
||||
|
||||
```sh
|
||||
curl 'localhost:8088/test?birthday=2000-01-01'
|
||||
curl 'localhost:8088/test?birthday=2000-01-01&birthdays=2000-01-01,2000-01-02'
|
||||
```
|
||||
Result
|
||||
```sh
|
||||
"2000/01/01"
|
||||
{"Birthday":"2000/01/01","Birthdays":["2000/01/01","2000/01/02"],"BirthdaysDefault":["2020/09/01","2020/09/02"]}
|
||||
```
|
||||
|
||||
Note:
|
||||
- If a type implements both `encoding.TextUnmarshaler` and `BindUnmarshaler`, gin will use `BindUnmarshaler` by default unless you specify `parser=encoding.TextUnmarshaler` in the binding tag.
|
||||
- If a type returns an error from its implementation of `BindUnmarshaler`, gin will stop binding and return the error to the client.
|
||||
|
||||
### Bind Header
|
||||
|
||||
```go
|
||||
|
||||
24
go.mod
24
go.mod
@ -1,29 +1,29 @@
|
||||
module github.com/gin-gonic/gin
|
||||
|
||||
go 1.23.0
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.14.0
|
||||
github.com/gin-contrib/sse v1.1.0
|
||||
github.com/go-playground/validator/v10 v10.27.0
|
||||
github.com/go-playground/validator/v10 v10.28.0
|
||||
github.com/goccy/go-json v0.10.2
|
||||
github.com/goccy/go-yaml v1.18.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/modern-go/reflect2 v1.0.2
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/quic-go/quic-go v0.54.0
|
||||
github.com/quic-go/quic-go v0.54.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/ugorji/go/codec v1.3.0
|
||||
golang.org/x/net v0.42.0
|
||||
google.golang.org/protobuf v1.36.9
|
||||
golang.org/x/net v0.43.0
|
||||
google.golang.org/protobuf v1.36.10
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
@ -34,11 +34,11 @@ require (
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
44
go.sum
44
go.sum
@ -7,8 +7,8 @@ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gE
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
@ -17,8 +17,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
@ -44,8 +44,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/quic-go/quic-go v0.54.1 h1:4ZAWm0AhCb6+hE+l5Q1NAL0iRn/ZrMwqHRGQiFwj2eg=
|
||||
github.com/quic-go/quic-go v0.54.1/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@ -63,23 +63,23 @@ go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
38
logger.go
38
logger.go
@ -103,6 +103,27 @@ func (p *LogFormatterParams) StatusCodeColor() string {
|
||||
}
|
||||
}
|
||||
|
||||
// LatencyColor is the ANSI color for latency
|
||||
func (p *LogFormatterParams) LatencyColor() string {
|
||||
latency := p.Latency
|
||||
switch {
|
||||
case latency < time.Millisecond*100:
|
||||
return white
|
||||
case latency < time.Millisecond*200:
|
||||
return green
|
||||
case latency < time.Millisecond*300:
|
||||
return cyan
|
||||
case latency < time.Millisecond*500:
|
||||
return blue
|
||||
case latency < time.Second:
|
||||
return yellow
|
||||
case latency < time.Second*2:
|
||||
return magenta
|
||||
default:
|
||||
return red
|
||||
}
|
||||
}
|
||||
|
||||
// MethodColor is the ANSI color for appropriately logging http method to a terminal.
|
||||
func (p *LogFormatterParams) MethodColor() string {
|
||||
method := p.Method
|
||||
@ -139,20 +160,27 @@ func (p *LogFormatterParams) IsOutputColor() bool {
|
||||
|
||||
// defaultLogFormatter is the default log format function Logger middleware uses.
|
||||
var defaultLogFormatter = func(param LogFormatterParams) string {
|
||||
var statusColor, methodColor, resetColor string
|
||||
var statusColor, methodColor, resetColor, latencyColor string
|
||||
if param.IsOutputColor() {
|
||||
statusColor = param.StatusCodeColor()
|
||||
methodColor = param.MethodColor()
|
||||
resetColor = param.ResetColor()
|
||||
latencyColor = param.LatencyColor()
|
||||
}
|
||||
|
||||
if param.Latency > time.Minute {
|
||||
param.Latency = param.Latency.Truncate(time.Second)
|
||||
switch {
|
||||
case param.Latency > time.Minute:
|
||||
param.Latency = param.Latency.Truncate(time.Second * 10)
|
||||
case param.Latency > time.Second:
|
||||
param.Latency = param.Latency.Truncate(time.Millisecond * 10)
|
||||
case param.Latency > time.Millisecond:
|
||||
param.Latency = param.Latency.Truncate(time.Microsecond * 10)
|
||||
}
|
||||
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %#v\n%s",
|
||||
|
||||
return fmt.Sprintf("[GIN] %v |%s %3d %s|%s %8v %s| %15s |%s %-7s %s %#v\n%s",
|
||||
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
|
||||
statusColor, param.StatusCode, resetColor,
|
||||
param.Latency,
|
||||
latencyColor, param.Latency, resetColor,
|
||||
param.ClientIP,
|
||||
methodColor, param.Method, resetColor,
|
||||
param.Path,
|
||||
|
||||
@ -277,11 +277,11 @@ func TestDefaultLogFormatter(t *testing.T) {
|
||||
isTerm: false,
|
||||
}
|
||||
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseParam))
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 2743h29m3s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseLongDurationParam))
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseParam))
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 2743h29m0s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseLongDurationParam))
|
||||
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueParam))
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 2743h29m3s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueLongDurationParam))
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m|\x1b[97;41m 5s \x1b[0m| 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueParam))
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m|\x1b[97;41m 2743h29m0s \x1b[0m| 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueLongDurationParam))
|
||||
}
|
||||
|
||||
func TestColorForMethod(t *testing.T) {
|
||||
@ -317,6 +317,23 @@ func TestColorForStatus(t *testing.T) {
|
||||
assert.Equal(t, red, colorForStatus(2), "other things should be red")
|
||||
}
|
||||
|
||||
func TestColorForLatency(t *testing.T) {
|
||||
colorForLantency := func(latency time.Duration) string {
|
||||
p := LogFormatterParams{
|
||||
Latency: latency,
|
||||
}
|
||||
return p.LatencyColor()
|
||||
}
|
||||
|
||||
assert.Equal(t, white, colorForLantency(time.Duration(0)), "0 should be white")
|
||||
assert.Equal(t, white, colorForLantency(time.Millisecond*20), "20ms should be white")
|
||||
assert.Equal(t, green, colorForLantency(time.Millisecond*150), "150ms should be green")
|
||||
assert.Equal(t, cyan, colorForLantency(time.Millisecond*250), "250ms should be cyan")
|
||||
assert.Equal(t, yellow, colorForLantency(time.Millisecond*600), "600ms should be yellow")
|
||||
assert.Equal(t, magenta, colorForLantency(time.Millisecond*1500), "1.5s should be magenta")
|
||||
assert.Equal(t, red, colorForLantency(time.Second*3), "other things should be red")
|
||||
}
|
||||
|
||||
func TestResetColor(t *testing.T) {
|
||||
p := LogFormatterParams{}
|
||||
assert.Equal(t, string([]byte{27, 91, 48, 109}), p.ResetColor())
|
||||
|
||||
@ -17,7 +17,7 @@ const (
|
||||
defaultStatus = http.StatusOK
|
||||
)
|
||||
|
||||
var errHijackAlreadyWritten = errors.New("gin: response already written")
|
||||
var errHijackAlreadyWritten = errors.New("gin: response body already written")
|
||||
|
||||
// ResponseWriter ...
|
||||
type ResponseWriter interface {
|
||||
@ -109,7 +109,9 @@ func (w *responseWriter) Written() bool {
|
||||
|
||||
// Hijack implements the http.Hijacker interface.
|
||||
func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if w.Written() {
|
||||
// Allow hijacking before any data is written (size == -1) or after headers are written (size == 0),
|
||||
// but not after body data is written (size > 0). For compatibility with websocket libraries (e.g., github.com/coder/websocket)
|
||||
if w.size > 0 {
|
||||
return nil, nil, errHijackAlreadyWritten
|
||||
}
|
||||
if w.size < 0 {
|
||||
|
||||
@ -194,6 +194,64 @@ func TestResponseWriterHijackAfterWrite(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test: WebSocket compatibility - allow hijack after WriteHeaderNow(), but block after body data.
|
||||
func TestResponseWriterHijackAfterWriteHeaderNow(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
action func(w ResponseWriter) error
|
||||
expectWrittenBeforeHijack bool
|
||||
expectHijackSuccess bool
|
||||
expectWrittenAfterHijack bool
|
||||
expectError error
|
||||
}{
|
||||
{
|
||||
name: "hijack after WriteHeaderNow only should succeed (websocket pattern)",
|
||||
action: func(w ResponseWriter) error {
|
||||
w.WriteHeaderNow() // Simulate websocket.Accept() behavior
|
||||
return nil
|
||||
},
|
||||
expectWrittenBeforeHijack: true,
|
||||
expectHijackSuccess: true, // NEW BEHAVIOR: allow hijack after just header write
|
||||
expectWrittenAfterHijack: true,
|
||||
expectError: nil,
|
||||
},
|
||||
{
|
||||
name: "hijack after WriteHeaderNow + Write should fail",
|
||||
action: func(w ResponseWriter) error {
|
||||
w.WriteHeaderNow()
|
||||
_, err := w.Write([]byte("test"))
|
||||
return err
|
||||
},
|
||||
expectWrittenBeforeHijack: true,
|
||||
expectHijackSuccess: false,
|
||||
expectWrittenAfterHijack: true,
|
||||
expectError: errHijackAlreadyWritten,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
hijacker := &mockHijacker{ResponseRecorder: httptest.NewRecorder()}
|
||||
writer := &responseWriter{}
|
||||
writer.reset(hijacker)
|
||||
w := ResponseWriter(writer)
|
||||
|
||||
require.NoError(t, tc.action(w), "unexpected error during pre-hijack action")
|
||||
|
||||
assert.Equal(t, tc.expectWrittenBeforeHijack, w.Written(), "unexpected w.Written() state before hijack")
|
||||
|
||||
_, _, hijackErr := w.Hijack()
|
||||
|
||||
if tc.expectError == nil {
|
||||
require.NoError(t, hijackErr, "expected hijack to succeed")
|
||||
} else {
|
||||
require.ErrorIs(t, hijackErr, tc.expectError, "unexpected error from Hijack()")
|
||||
}
|
||||
assert.Equal(t, tc.expectHijackSuccess, hijacker.hijacked, "unexpected hijacker.hijacked state")
|
||||
assert.Equal(t, tc.expectWrittenAfterHijack, w.Written(), "unexpected w.Written() state after hijack")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseWriterFlush(t *testing.T) {
|
||||
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
writer := &responseWriter{}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user