Compare commits

...

11 Commits

Author SHA1 Message Date
takanuva15
8a07c82f7b feat(binding): add support for encoding.UnmarshalText in uri/query binding 2025-10-08 17:43:35 -04:00
Bo-Yi Wu
5dd833f1f2
chore: bump minimum Go version to 1.24 and update workflows (#4388)
- Update minimum required Go version from 1.23 to 1.24 throughout documentation, warnings, and tests
- Remove Go 1.23 from the GitHub Actions workflow matrix
- Change single quotes to double quotes for consistency in workflow configuration

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-10-08 08:30:45 +08:00
dependabot[bot]
48a5dca087
chore(deps): bump github.com/go-playground/validator/v10 (#4385)
Bumps [github.com/go-playground/validator/v10](https://github.com/go-playground/validator) from 10.27.0 to 10.28.0.
- [Release notes](https://github.com/go-playground/validator/releases)
- [Commits](https://github.com/go-playground/validator/compare/v10.27.0...v10.28.0)

---
updated-dependencies:
- dependency-name: github.com/go-playground/validator/v10
  dependency-version: 10.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-08 08:13:29 +08:00
dependabot[bot]
0bd10a84f9
chore(deps): bump github/codeql-action from 3 to 4 in the actions group (#4387)
Bumps the actions group with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 3 to 4
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-08 08:12:52 +08:00
ljz
4dec17afdf
feat(logger): color latency (#4146)
Co-authored-by: lizhao <lizhao@qq.com>
2025-10-05 11:23:57 +08:00
goldlinker
731374fb36
docs(context): fix wrong function name in comment (#4382)
Signed-off-by: goldlinker <goldlinker@outlook.jp>
2025-10-03 21:26:47 +08:00
dependabot[bot]
8ca975441f
chore(deps): bump google.golang.org/protobuf from 1.36.9 to 1.36.10 (#4383)
Bumps google.golang.org/protobuf from 1.36.9 to 1.36.10.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-version: 1.36.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-03 21:25:35 +08:00
russcoss
39858a0859
refactor(binding): use maps.Copy for cleaner map handling (#4352)
Signed-off-by: russcoss <russcoss@outlook.com>
2025-09-27 11:03:59 +08:00
Meng Xun
ed150e7254
test(benchmarks): fix the incorrect function name (#4375)
Signed-off-by: mengxun <mengxun1122@163.com>
2025-09-26 08:15:35 +08:00
Bo-Yi Wu
234a6d4c00
fix(response): refine hijack behavior for response lifecycle (#4373)
* feat: refine hijack behavior for response lifecycle and add tests

- Clarify the error message for attempted hijack after response body data is written
- Modify hijack behavior: allow hijacking after headers are written (for better websocket compatibility), but block hijacking after any body data is sent
- Add comprehensive tests to validate allowed hijack after header write and disallowed hijack after body write

fix https://github.com/gin-gonic/gin/issues/4372

Signed-off-by: appleboy <appleboy.tw@gmail.com>

* test: use require for immediate test failure on errors

- Replace assert with require for error checks to ensure test failures immediately halt execution

Signed-off-by: appleboy <appleboy.tw@gmail.com>

* Update response_writer.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Signed-off-by: appleboy <appleboy.tw@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-26 08:13:39 +08:00
dependabot[bot]
df2753778e
chore(deps): bump github.com/quic-go/quic-go from 0.54.0 to 0.54.1 (#4379)
Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.54.0 to 0.54.1.
- [Release notes](https://github.com/quic-go/quic-go/releases)
- [Commits](https://github.com/quic-go/quic-go/compare/v0.54.0...v0.54.1)

---
updated-dependencies:
- dependency-name: github.com/quic-go/quic-go
  dependency-version: 0.54.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-26 08:10:02 +08:00
16 changed files with 708 additions and 195 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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+.
`)
}

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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