Merge branch 'master' into master

This commit is contained in:
Bo-Yi Wu 2026-02-28 10:09:11 +08:00 committed by GitHub
commit bc46a09535
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 952 additions and 178 deletions

View File

@ -26,14 +26,14 @@ jobs:
- name: Setup golangci-lint - name: Setup golangci-lint
uses: golangci/golangci-lint-action@v9 uses: golangci/golangci-lint-action@v9
with: with:
version: v2.6 version: v2.9
args: --verbose args: --verbose
test: test:
needs: lint needs: lint
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest] os: [ubuntu-latest, macos-latest]
go: ["1.24", "1.25"] go: ["1.24", "1.25", "1.26"]
test-tags: test-tags:
[ [
"", "",

View File

@ -21,7 +21,7 @@ jobs:
with: with:
go-version: "^1" go-version: "^1"
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@v7
with: with:
# either 'goreleaser' (default) or 'goreleaser-pro' # either 'goreleaser' (default) or 'goreleaser-pro'
distribution: goreleaser distribution: goreleaser

View File

@ -9,7 +9,7 @@ on:
- master - master
schedule: schedule:
# Run daily at 00:00 UTC # Run daily at 00:00 UTC
- cron: '0 0 * * *' - cron: "0 0 * * *"
workflow_dispatch: # Allow manual trigger workflow_dispatch: # Allow manual trigger
permissions: permissions:
@ -27,30 +27,30 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Run Trivy vulnerability scanner (source code) - name: Run Trivy vulnerability scanner (source code)
uses: aquasecurity/trivy-action@0.33.1 uses: aquasecurity/trivy-action@0.34.1
with: with:
scan-type: 'fs' scan-type: "fs"
scan-ref: '.' scan-ref: "."
scanners: 'vuln,secret,misconfig' scanners: "vuln,secret,misconfig"
format: 'sarif' format: "sarif"
output: 'trivy-results.sarif' output: "trivy-results.sarif"
severity: 'CRITICAL,HIGH,MEDIUM' severity: "CRITICAL,HIGH,MEDIUM"
ignore-unfixed: true ignore-unfixed: true
- name: Upload Trivy results to GitHub Security tab - name: Upload Trivy results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v4 uses: github/codeql-action/upload-sarif@v4
if: always() if: always()
with: with:
sarif_file: 'trivy-results.sarif' sarif_file: "trivy-results.sarif"
- name: Run Trivy scanner (table output for logs) - name: Run Trivy scanner (table output for logs)
uses: aquasecurity/trivy-action@0.33.1 uses: aquasecurity/trivy-action@0.34.1
if: always() if: always()
with: with:
scan-type: 'fs' scan-type: "fs"
scan-ref: '.' scan-ref: "."
scanners: 'vuln,secret,misconfig' scanners: "vuln,secret,misconfig"
format: 'table' format: "table"
severity: 'CRITICAL,HIGH,MEDIUM' severity: "CRITICAL,HIGH,MEDIUM"
ignore-unfixed: true ignore-unfixed: true
exit-code: '1' exit-code: "1"

View File

@ -36,6 +36,6 @@ Please ensure your pull request meets the following requirements:
- All tests pass in available continuous integration systems (e.g., GitHub Actions). - All tests pass in available continuous integration systems (e.g., GitHub Actions).
- Add or modify tests to cover your code changes. - Add or modify tests to cover your code changes.
- If your pull request introduces a new feature, document it in [`docs/doc.md`](docs/doc.md), not in the README. - If your pull request introduces a new feature, document it in [`docs/doc.md`](docs/doc.md), not in the README.
- Follow the checklist in the [Pull Request Template](.github/PULL_REQUEST_TEMPLATE.md:1). - Follow the checklist in the [Pull Request Template](.github/PULL_REQUEST_TEMPLATE.md).
Thank you for contributing! Thank you for contributing!

View File

@ -154,7 +154,7 @@ func runRequest(B *testing.B, r *Engine, method, path string) {
w := newMockWriter() w := newMockWriter()
B.ReportAllocs() B.ReportAllocs()
B.ResetTimer() B.ResetTimer()
for i := 0; i < B.N; i++ { for B.Loop() {
r.ServeHTTP(w, req) r.ServeHTTP(w, req)
} }
} }

View File

@ -23,6 +23,7 @@ const (
MIMEYAML = "application/x-yaml" MIMEYAML = "application/x-yaml"
MIMEYAML2 = "application/yaml" MIMEYAML2 = "application/yaml"
MIMETOML = "application/toml" MIMETOML = "application/toml"
MIMEBSON = "application/bson"
) )
// Binding describes the interface which needs to be implemented for binding the // Binding describes the interface which needs to be implemented for binding the
@ -86,6 +87,7 @@ var (
Header Binding = headerBinding{} Header Binding = headerBinding{}
Plain BindingBody = plainBinding{} Plain BindingBody = plainBinding{}
TOML BindingBody = tomlBinding{} TOML BindingBody = tomlBinding{}
BSON BindingBody = bsonBinding{}
) )
// Default returns the appropriate Binding instance based on the HTTP method // Default returns the appropriate Binding instance based on the HTTP method
@ -110,6 +112,8 @@ func Default(method, contentType string) Binding {
return TOML return TOML
case MIMEMultipartPOSTForm: case MIMEMultipartPOSTForm:
return FormMultipart return FormMultipart
case MIMEBSON:
return BSON
default: // case MIMEPOSTForm: default: // case MIMEPOSTForm:
return Form return Form
} }

View File

@ -21,6 +21,7 @@ const (
MIMEYAML = "application/x-yaml" MIMEYAML = "application/x-yaml"
MIMEYAML2 = "application/yaml" MIMEYAML2 = "application/yaml"
MIMETOML = "application/toml" MIMETOML = "application/toml"
MIMEBSON = "application/bson"
) )
// Binding describes the interface which needs to be implemented for binding the // Binding describes the interface which needs to be implemented for binding the
@ -82,6 +83,7 @@ var (
Header = headerBinding{} Header = headerBinding{}
TOML = tomlBinding{} TOML = tomlBinding{}
Plain = plainBinding{} Plain = plainBinding{}
BSON BindingBody = bsonBinding{}
) )
// Default returns the appropriate Binding instance based on the HTTP method // Default returns the appropriate Binding instance based on the HTTP method
@ -104,6 +106,8 @@ func Default(method, contentType string) Binding {
return FormMultipart return FormMultipart
case MIMETOML: case MIMETOML:
return TOML return TOML
case MIMEBSON:
return BSON
default: // case MIMEPOSTForm: default: // case MIMEPOSTForm:
return Form return Form
} }

View File

@ -21,6 +21,7 @@ import (
"github.com/gin-gonic/gin/testdata/protoexample" "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/v2/bson"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
@ -172,6 +173,9 @@ func TestBindingDefault(t *testing.T) {
assert.Equal(t, TOML, Default(http.MethodPost, MIMETOML)) assert.Equal(t, TOML, Default(http.MethodPost, MIMETOML))
assert.Equal(t, TOML, Default(http.MethodPut, MIMETOML)) assert.Equal(t, TOML, Default(http.MethodPut, MIMETOML))
assert.Equal(t, BSON, Default(http.MethodPost, MIMEBSON))
assert.Equal(t, BSON, Default(http.MethodPut, MIMEBSON))
} }
func TestBindingJSONNilBody(t *testing.T) { func TestBindingJSONNilBody(t *testing.T) {
@ -731,6 +735,18 @@ func TestBindingProtoBufFail(t *testing.T) {
string(data), string(data[1:])) string(data), string(data[1:]))
} }
func TestBindingBSON(t *testing.T) {
var obj FooStruct
obj.Foo = "bar"
data, _ := bson.Marshal(&obj)
testBodyBinding(t,
BSON, "bson",
"/", "/",
string(data),
// note: for badbody, we remove first byte to make it invalid
string(data[1:]))
}
func TestValidationFails(t *testing.T) { func TestValidationFails(t *testing.T) {
var obj FooStruct var obj FooStruct
req := requestWithBody(http.MethodPost, "/", `{"bar": "foo"}`) req := requestWithBody(http.MethodPost, "/", `{"bar": "foo"}`)

30
binding/bson.go Normal file
View File

@ -0,0 +1,30 @@
// Copyright 2025 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"io"
"net/http"
"go.mongodb.org/mongo-driver/v2/bson"
)
type bsonBinding struct{}
func (bsonBinding) Name() string {
return "bson"
}
func (b bsonBinding) Bind(req *http.Request, obj any) error {
buf, err := io.ReadAll(req.Body)
if err == nil {
err = b.BindBody(buf, obj)
}
return err
}
func (bsonBinding) BindBody(body []byte, obj any) error {
return bson.Unmarshal(body, obj)
}

View File

@ -27,7 +27,7 @@ func (err SliceValidationError) Error() string {
} }
var b strings.Builder var b strings.Builder
for i := 0; i < len(err); i++ { for i := range len(err) {
if err[i] != nil { if err[i] != nil {
if b.Len() > 0 { if b.Len() > 0 {
b.WriteString("\n") b.WriteString("\n")
@ -58,7 +58,7 @@ func (v *defaultValidator) ValidateStruct(obj any) error {
case reflect.Slice, reflect.Array: case reflect.Slice, reflect.Array:
count := value.Len() count := value.Len()
validateRet := make(SliceValidationError, 0) validateRet := make(SliceValidationError, 0)
for i := 0; i < count; i++ { for i := range count {
if err := v.ValidateStruct(value.Index(i).Interface()); err != nil { if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
validateRet = append(validateRet, err) validateRet = append(validateRet, err)
} }

View File

@ -5,6 +5,7 @@
package binding package binding
import ( import (
"encoding"
"errors" "errors"
"fmt" "fmt"
"maps" "maps"
@ -118,7 +119,7 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
tValue := value.Type() tValue := value.Type()
var isSet bool var isSet bool
for i := 0; i < value.NumField(); i++ { for i := range value.NumField() {
sf := tValue.Field(i) sf := tValue.Field(i)
if sf.PkgPath != "" && !sf.Anonymous { // unexported if sf.PkgPath != "" && !sf.Anonymous { // unexported
continue continue
@ -137,6 +138,8 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
type setOptions struct { type setOptions struct {
isDefaultExists bool isDefaultExists bool
defaultValue string defaultValue string
// parser specifies what interface to use for reading the request & default values (e.g. `encoding.TextUnmarshaler`)
parser string
} }
func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) { func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
@ -168,6 +171,8 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
setOpt.defaultValue = strings.ReplaceAll(v, ";", ",") setOpt.defaultValue = strings.ReplaceAll(v, ";", ",")
} }
} }
} else if k, v = head(opt, "="); k == "parser" {
setOpt.parser = v
} }
} }
@ -191,6 +196,20 @@ func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
return false, nil 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) { func trySplit(vs []string, field reflect.StructField) (newVs []string, err error) {
cfTag := field.Tag.Get("collection_format") cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" { if cfTag == "" || cfTag == "multi" {
@ -208,7 +227,7 @@ func trySplit(vs []string, field reflect.StructField) (newVs []string, err error
case "pipes": case "pipes":
sep = "|" sep = "|"
default: 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 totalLength := 0
@ -244,7 +263,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 return ok, err
} }
@ -252,7 +273,7 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
return false, err return false, err
} }
return true, setSlice(vs, value, field) return true, setSlice(vs, value, field, opt)
case reflect.Array: case reflect.Array:
if len(vs) == 0 { if len(vs) == 0 {
if !opt.isDefaultExists { if !opt.isDefaultExists {
@ -267,7 +288,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 return ok, err
} }
@ -279,27 +302,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 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: default:
var val string var val string
if !ok { if !ok || len(vs) == 0 || (len(vs) > 0 && vs[0] == "") {
val = opt.defaultValue val = opt.defaultValue
} else if len(vs) > 0 {
val = vs[0]
} }
if len(vs) > 0 { if ok, err = trySetUsingParser(val, value, opt.parser); ok {
val = vs[0] return ok, err
if val == "" { } else if ok, err = trySetCustom(val, value); ok {
val = opt.defaultValue
}
}
if ok, err := trySetCustom(val, value); ok {
return ok, err 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
}
// If it is a string type, no spaces are removed, and the user data is not modified here // If it is a string type, no spaces are removed, and the user data is not modified here
if value.Kind() != reflect.String { if value.Kind() != reflect.String {
val = strings.TrimSpace(val) val = strings.TrimSpace(val)
@ -352,7 +380,7 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
if !value.Elem().IsValid() { if !value.Elem().IsValid() {
value.Set(reflect.New(value.Type().Elem())) value.Set(reflect.New(value.Type().Elem()))
} }
return setWithProperType(val, value.Elem(), field) return setWithProperType(val, value.Elem(), field, opt)
default: default:
return errUnknownType return errUnknownType
} }
@ -459,9 +487,9 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
return nil 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 { for i, s := range vals {
err := setWithProperType(s, value.Index(i), field) err := setWithProperType(s, value.Index(i), field, opt)
if err != nil { if err != nil {
return err return err
} }
@ -469,9 +497,9 @@ func setArray(vals []string, value reflect.Value, field reflect.StructField) err
return nil 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)) slice := reflect.MakeSlice(value.Type(), len(vals), len(vals))
err := setArray(vals, slice, field) err := setArray(vals, slice, field, opt)
if err != nil { if err != nil {
return err return err
} }

View File

@ -5,6 +5,7 @@
package binding package binding
import ( import (
"encoding"
"encoding/hex" "encoding/hex"
"errors" "errors"
"mime/multipart" "mime/multipart"
@ -524,6 +525,16 @@ func TestMappingCustomUnmarshalParamHexWithURITag(t *testing.T) {
assert.EqualValues(t, 245, s.Foo) assert.EqualValues(t, 245, s.Foo)
} }
func TestMappingCustomUnmarshalParamHexDefault(t *testing.T) {
var s struct {
Foo customUnmarshalParamHex `form:"foo,default=f5"`
}
err := mappingByPtr(&s, formSource{"foo": {}}, "form")
require.NoError(t, err)
assert.EqualValues(t, 0xf5, s.Foo)
}
type customUnmarshalParamType struct { type customUnmarshalParamType struct {
Protocol string Protocol string
Path string Path string
@ -624,6 +635,33 @@ func TestMappingCustomSliceForm(t *testing.T) {
assert.Equal(t, "foo", s.FileData[1]) assert.Equal(t, "foo", s.FileData[1])
} }
func TestMappingCustomSliceStopsWhenError(t *testing.T) {
var s struct {
FileData customPath `form:"path"`
}
err := mappingByPtr(&s, formSource{"path": {"invalid"}}, "form")
require.ErrorContains(t, err, "invalid format")
require.Empty(t, s.FileData)
}
func TestMappingCustomSliceOfSliceUri(t *testing.T) {
var s struct {
FileData []customPath `uri:"path" collection_format:"csv"`
}
err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "uri")
require.NoError(t, err)
assert.Equal(t, []customPath{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData)
}
func TestMappingCustomSliceOfSliceForm(t *testing.T) {
var s struct {
FileData []customPath `form:"path" collection_format:"csv"`
}
err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "form")
require.NoError(t, err)
assert.Equal(t, []customPath{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData)
}
type objectID [12]byte type objectID [12]byte
func (o *objectID) UnmarshalParam(param string) error { func (o *objectID) UnmarshalParam(param string) error {
@ -675,6 +713,358 @@ func TestMappingCustomArrayForm(t *testing.T) {
assert.Equal(t, expected, s.FileData) assert.Equal(t, expected, s.FileData)
} }
func TestMappingCustomArrayOfArrayUri(t *testing.T) {
id1, _ := convertTo(`664a062ac74a8ad104e0e80e`)
id2, _ := convertTo(`664a062ac74a8ad104e0e80f`)
var s struct {
FileData []objectID `uri:"ids" collection_format:"csv"`
}
err := mappingByPtr(&s, formSource{"ids": {`664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`}}, "uri")
require.NoError(t, err)
assert.Equal(t, []objectID{id1, id2}, s.FileData)
}
func TestMappingCustomArrayOfArrayForm(t *testing.T) {
id1, _ := convertTo(`664a062ac74a8ad104e0e80e`)
id2, _ := convertTo(`664a062ac74a8ad104e0e80f`)
var s struct {
FileData []objectID `form:"ids" collection_format:"csv"`
}
err := mappingByPtr(&s, formSource{"ids": {`664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`}}, "form")
require.NoError(t, err)
assert.Equal(t, []objectID{id1, id2}, s.FileData)
}
// ==== TextUnmarshaler tests START ====
type customUnmarshalTextHex int
func (f *customUnmarshalTextHex) UnmarshalText(text []byte) error {
v, err := strconv.ParseInt(string(text), 16, 64)
if err != nil {
return err
}
*f = customUnmarshalTextHex(v)
return nil
}
// verify type implements TextUnmarshaler
var _ encoding.TextUnmarshaler = (*customUnmarshalTextHex)(nil)
func TestMappingCustomUnmarshalTextHexUri(t *testing.T) {
var s struct {
Field customUnmarshalTextHex `uri:"field,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&s, formSource{"field": {`f5`}}, "uri")
require.NoError(t, err)
assert.EqualValues(t, 245, s.Field)
}
func TestMappingCustomUnmarshalTextHexForm(t *testing.T) {
var s struct {
Field customUnmarshalTextHex `form:"field,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&s, formSource{"field": {`f5`}}, "form")
require.NoError(t, err)
assert.EqualValues(t, 245, s.Field)
}
func TestMappingCustomUnmarshalTextHexDefault(t *testing.T) {
var s struct {
Field customUnmarshalTextHex `form:"field,default=f5,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&s, formSource{"field1": {}}, "form")
require.NoError(t, err)
assert.EqualValues(t, 0xf5, s.Field)
}
type customUnmarshalTextType struct {
Protocol string
Path string
Name string
}
func (f *customUnmarshalTextType) 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 = (*customUnmarshalTextType)(nil)
func TestMappingCustomStructTypeUnmarshalTextForm(t *testing.T) {
var s struct {
FileData customUnmarshalTextType `form:"data,parser=encoding.TextUnmarshaler"`
}
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 TestMappingCustomStructTypeUnmarshalTextUri(t *testing.T) {
var s struct {
FileData customUnmarshalTextType `uri:"data,parser=encoding.TextUnmarshaler"`
}
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 TestMappingCustomPointerStructTypeUnmarshalTextForm(t *testing.T) {
var s struct {
FileData *customUnmarshalTextType `form:"data,parser=encoding.TextUnmarshaler"`
}
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 TestMappingCustomPointerStructTypeUnmarshalTextUri(t *testing.T) {
var s struct {
FileData *customUnmarshalTextType `uri:"data,parser=encoding.TextUnmarshaler"`
}
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)
}
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 TestMappingCustomSliceUnmarshalTextUri(t *testing.T) {
var s struct {
FileData customPathUnmarshalText `uri:"path,parser=encoding.TextUnmarshaler"`
}
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 TestMappingCustomSliceUnmarshalTextForm(t *testing.T) {
var s struct {
FileData customPathUnmarshalText `form:"path,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "form")
require.NoError(t, err)
assert.Equal(t, "bar", s.FileData[0])
assert.Equal(t, "foo", s.FileData[1])
}
func TestMappingCustomSliceUnmarshalTextStopsWhenError(t *testing.T) {
var s struct {
FileData customPathUnmarshalText `form:"path,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&s, formSource{"path": {"invalid"}}, "form")
require.ErrorContains(t, err, "invalid format")
require.Empty(t, s.FileData)
}
func TestMappingCustomSliceOfSliceUnmarshalTextUri(t *testing.T) {
var s struct {
FileData []customPathUnmarshalText `uri:"path,parser=encoding.TextUnmarshaler" collection_format:"csv"`
}
err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "uri")
require.NoError(t, err)
assert.Equal(t, []customPathUnmarshalText{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData)
}
func TestMappingCustomSliceOfSliceUnmarshalTextForm(t *testing.T) {
var s struct {
FileData []customPathUnmarshalText `form:"path,parser=encoding.TextUnmarshaler" collection_format:"csv"`
}
err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "form")
require.NoError(t, err)
assert.Equal(t, []customPathUnmarshalText{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData)
}
func TestMappingCustomSliceOfSliceUnmarshalTextDefault(t *testing.T) {
var s struct {
FileData []customPathUnmarshalText `form:"path,default=bar/foo;bar/foo/spam,parser=encoding.TextUnmarshaler" collection_format:"csv"`
}
err := mappingByPtr(&s, formSource{"path": {}}, "form")
require.NoError(t, err)
assert.Equal(t, []customPathUnmarshalText{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData)
}
type objectIDUnmarshalText [12]byte
func (o *objectIDUnmarshalText) UnmarshalText(text []byte) error {
oid, err := convertToOidUnmarshalText(string(text))
if err != nil {
return err
}
*o = oid
return nil
}
func convertToOidUnmarshalText(s string) (objectIDUnmarshalText, error) {
oid, err := convertTo(s)
return objectIDUnmarshalText(oid), err
}
var _ encoding.TextUnmarshaler = (*objectIDUnmarshalText)(nil)
func TestMappingCustomArrayUnmarshalTextUri(t *testing.T) {
var s struct {
FileData objectIDUnmarshalText `uri:"id,parser=encoding.TextUnmarshaler"`
}
val := `664a062ac74a8ad104e0e80f`
err := mappingByPtr(&s, formSource{"id": {val}}, "uri")
require.NoError(t, err)
expected, _ := convertToOidUnmarshalText(val)
assert.Equal(t, expected, s.FileData)
}
func TestMappingCustomArrayUnmarshalTextForm(t *testing.T) {
var s struct {
FileData objectIDUnmarshalText `form:"id,parser=encoding.TextUnmarshaler"`
}
val := `664a062ac74a8ad104e0e80f`
err := mappingByPtr(&s, formSource{"id": {val}}, "form")
require.NoError(t, err)
expected, _ := convertToOidUnmarshalText(val)
assert.Equal(t, expected, s.FileData)
}
func TestMappingCustomArrayOfArrayUnmarshalTextUri(t *testing.T) {
id1, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80e`)
id2, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80f`)
var s struct {
FileData []objectIDUnmarshalText `uri:"ids,parser=encoding.TextUnmarshaler" collection_format:"csv"`
}
err := mappingByPtr(&s, formSource{"ids": {`664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`}}, "uri")
require.NoError(t, err)
assert.Equal(t, []objectIDUnmarshalText{id1, id2}, s.FileData)
}
func TestMappingCustomArrayOfArrayUnmarshalTextForm(t *testing.T) {
id1, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80e`)
id2, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80f`)
var s struct {
FileData []objectIDUnmarshalText `form:"ids,parser=encoding.TextUnmarshaler" collection_format:"csv"`
}
err := mappingByPtr(&s, formSource{"ids": {`664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`}}, "form")
require.NoError(t, err)
assert.Equal(t, []objectIDUnmarshalText{id1, id2}, s.FileData)
}
func TestMappingCustomArrayOfArrayUnmarshalTextDefault(t *testing.T) {
id1, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80e`)
id2, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80f`)
var s struct {
FileData []objectIDUnmarshalText `form:"ids,default=664a062ac74a8ad104e0e80e;664a062ac74a8ad104e0e80f,parser=encoding.TextUnmarshaler" collection_format:"csv"`
}
err := mappingByPtr(&s, formSource{"ids": {}}, "form")
require.NoError(t, err)
assert.Equal(t, []objectIDUnmarshalText{id1, id2}, s.FileData)
}
// 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 customUnmarshalParamHex `form:"hex"`
HexByUnmarshalText customUnmarshalParamHex `form:"hex2,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&s, formSource{
"hex": {`f5`},
"hex2": {`f5`},
}, "form")
require.NoError(t, err)
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 customUnmarshalTextHex `form:"hex"`
HexByUnmarshalText customUnmarshalTextHex `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 normal 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 ====
func TestMappingEmptyValues(t *testing.T) { func TestMappingEmptyValues(t *testing.T) {
t.Run("slice with default", func(t *testing.T) { t.Run("slice with default", func(t *testing.T) {
var s struct { var s struct {

View File

@ -40,6 +40,7 @@ const (
MIMEYAML2 = binding.MIMEYAML2 MIMEYAML2 = binding.MIMEYAML2
MIMETOML = binding.MIMETOML MIMETOML = binding.MIMETOML
MIMEPROTOBUF = binding.MIMEPROTOBUF MIMEPROTOBUF = binding.MIMEPROTOBUF
MIMEBSON = binding.MIMEBSON
) )
// BodyBytesKey indicates a default body bytes key. // BodyBytesKey indicates a default body bytes key.
@ -386,6 +387,11 @@ func (c *Context) GetDuration(key any) time.Duration {
return getTyped[time.Duration](c, key) return getTyped[time.Duration](c, key)
} }
// GetError returns the value associated with the key as an error.
func (c *Context) GetError(key any) error {
return getTyped[error](c, key)
}
// GetIntSlice returns the value associated with the key as a slice of integers. // GetIntSlice returns the value associated with the key as a slice of integers.
func (c *Context) GetIntSlice(key any) []int { func (c *Context) GetIntSlice(key any) []int {
return getTyped[[]int](c, key) return getTyped[[]int](c, key)
@ -451,6 +457,11 @@ func (c *Context) GetStringSlice(key any) []string {
return getTyped[[]string](c, key) return getTyped[[]string](c, key)
} }
// GetErrorSlice returns the value associated with the key as a slice of errors.
func (c *Context) GetErrorSlice(key any) []error {
return getTyped[[]error](c, key)
}
// GetStringMap returns the value associated with the key as a map of interfaces. // GetStringMap returns the value associated with the key as a map of interfaces.
func (c *Context) GetStringMap(key any) map[string]any { func (c *Context) GetStringMap(key any) map[string]any {
return getTyped[map[string]any](c, key) return getTyped[map[string]any](c, key)
@ -740,8 +751,8 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm
// "application/json" --> JSON binding // "application/json" --> JSON binding
// "application/xml" --> XML binding // "application/xml" --> XML binding
// //
// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. // It parses the request's body based on the Content-Type (e.g., JSON or XML).
// It decodes the json payload into the struct specified as a pointer. // It decodes the payload into the struct specified as a pointer.
// It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid. // It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid.
func (c *Context) Bind(obj any) error { func (c *Context) Bind(obj any) error {
b := binding.Default(c.Request.Method, c.ContentType()) b := binding.Default(c.Request.Method, c.ContentType())
@ -821,8 +832,8 @@ func (c *Context) MustBindWith(obj any, b binding.Binding) error {
// "application/json" --> JSON binding // "application/json" --> JSON binding
// "application/xml" --> XML binding // "application/xml" --> XML binding
// //
// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. // It parses the request's body based on the Content-Type (e.g., JSON or XML).
// It decodes the json payload into the struct specified as a pointer. // It decodes the payload into the struct specified as a pointer.
// Like c.Bind() but this method does not set the response status code to 400 or abort if input is not valid. // Like c.Bind() but this method does not set the response status code to 400 or abort if input is not valid.
func (c *Context) ShouldBind(obj any) error { func (c *Context) ShouldBind(obj any) error {
b := binding.Default(c.Request.Method, c.ContentType()) b := binding.Default(c.Request.Method, c.ContentType())
@ -978,14 +989,27 @@ func (c *Context) ClientIP() string {
} }
} }
// It also checks if the remoteIP is a trusted proxy or not. var (
// In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks trusted bool
// defined by Engine.SetTrustedProxies() remoteIP net.IP
remoteIP := net.ParseIP(c.RemoteIP()) )
if remoteIP == nil { // If gin is listening a unix socket, always trust it.
return "" localAddr, ok := c.Request.Context().Value(http.LocalAddrContextKey).(net.Addr)
if ok && strings.HasPrefix(localAddr.Network(), "unix") {
trusted = true
}
// Fallback
if !trusted {
// It also checks if the remoteIP is a trusted proxy or not.
// In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks
// defined by Engine.SetTrustedProxies()
remoteIP = net.ParseIP(c.RemoteIP())
if remoteIP == nil {
return ""
}
trusted = c.engine.isTrustedProxy(remoteIP)
} }
trusted := c.engine.isTrustedProxy(remoteIP)
if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil { if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
for _, headerName := range c.engine.RemoteIPHeaders { for _, headerName := range c.engine.RemoteIPHeaders {
@ -1034,7 +1058,7 @@ func (c *Context) requestHeader(key string) string {
// bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function. // bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function.
func bodyAllowedForStatus(status int) bool { func bodyAllowedForStatus(status int) bool {
switch { switch {
case status >= 100 && status <= 199: case status >= http.StatusContinue && status < http.StatusOK:
return false return false
case status == http.StatusNoContent: case status == http.StatusNoContent:
return false return false
@ -1223,6 +1247,11 @@ func (c *Context) ProtoBuf(code int, obj any) {
c.Render(code, render.ProtoBuf{Data: obj}) c.Render(code, render.ProtoBuf{Data: obj})
} }
// BSON serializes the given struct as BSON into the response body.
func (c *Context) BSON(code int, obj any) {
c.Render(code, render.BSON{Data: obj})
}
// String writes the given string into the response body. // String writes the given string into the response body.
func (c *Context) String(code int, format string, values ...any) { func (c *Context) String(code int, format string, values ...any) {
c.Render(code, render.String{Format: format, Data: values}) c.Render(code, render.String{Format: format, Data: values})
@ -1330,6 +1359,7 @@ type Negotiate struct {
Data any Data any
TOMLData any TOMLData any
PROTOBUFData any PROTOBUFData any
BSONData any
} }
// Negotiate calls different Render according to acceptable Accept format. // Negotiate calls different Render according to acceptable Accept format.
@ -1359,6 +1389,10 @@ func (c *Context) Negotiate(code int, config Negotiate) {
data := chooseData(config.PROTOBUFData, config.Data) data := chooseData(config.PROTOBUFData, config.Data)
c.ProtoBuf(code, data) c.ProtoBuf(code, data)
case binding.MIMEBSON:
data := chooseData(config.BSONData, config.Data)
c.BSON(code, data)
default: default:
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck
} }

View File

@ -32,6 +32,7 @@ import (
testdata "github.com/gin-gonic/gin/testdata/protoexample" testdata "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/v2/bson"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
@ -516,6 +517,14 @@ func TestContextGetDuration(t *testing.T) {
assert.Equal(t, time.Second, c.GetDuration("duration")) assert.Equal(t, time.Second, c.GetDuration("duration"))
} }
func TestContextGetError(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
key := "error"
value := errors.New("test error")
c.Set(key, value)
assert.Equal(t, value, c.GetError(key))
}
func TestContextGetIntSlice(t *testing.T) { func TestContextGetIntSlice(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
key := "int-slice" key := "int-slice"
@ -618,6 +627,14 @@ func TestContextGetStringSlice(t *testing.T) {
assert.Equal(t, []string{"foo"}, c.GetStringSlice("slice")) assert.Equal(t, []string{"foo"}, c.GetStringSlice("slice"))
} }
func TestContextGetErrorSlice(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
key := "error-slice"
value := []error{errors.New("error1"), errors.New("error2")}
c.Set(key, value)
assert.Equal(t, value, c.GetErrorSlice(key))
}
func TestContextGetStringMap(t *testing.T) { func TestContextGetStringMap(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
m := make(map[string]any) m := make(map[string]any)
@ -1722,6 +1739,23 @@ func TestContextNegotiationWithPROTOBUF(t *testing.T) {
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type")) assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
} }
func TestContextNegotiationWithBSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest(http.MethodPost, "", nil)
c.Negotiate(http.StatusOK, Negotiate{
Offered: []string{MIMEBSON, MIMEXML, MIMEJSON, MIMEYAML, MIMEYAML2},
Data: H{"foo": "bar"},
})
bData, _ := bson.Marshal(H{"foo": "bar"})
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, string(bData), w.Body.String())
assert.Equal(t, "application/bson", w.Header().Get("Content-Type"))
}
func TestContextNegotiationNotSupport(t *testing.T) { func TestContextNegotiationNotSupport(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) c, _ := CreateTestContext(w)
@ -1952,6 +1986,16 @@ func TestContextClientIP(t *testing.T) {
c.engine.trustedCIDRs, _ = c.engine.prepareTrustedCIDRs() c.engine.trustedCIDRs, _ = c.engine.prepareTrustedCIDRs()
resetContextForClientIPTests(c) resetContextForClientIPTests(c)
// unix address
addr := &net.UnixAddr{Net: "unix", Name: "@"}
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), http.LocalAddrContextKey, addr))
c.Request.RemoteAddr = addr.String()
assert.Equal(t, "20.20.20.20", c.ClientIP())
// reset
c.Request = c.Request.WithContext(context.Background())
resetContextForClientIPTests(c)
// Legacy tests (validating that the defaults don't break the // Legacy tests (validating that the defaults don't break the
// (insecure!) old behaviour) // (insecure!) old behaviour)
assert.Equal(t, "20.20.20.20", c.ClientIP()) assert.Equal(t, "20.20.20.20", c.ClientIP())
@ -3688,22 +3732,22 @@ func BenchmarkGetMapFromFormData(b *testing.B) {
// Test case 3: Large dataset with many bracket keys // Test case 3: Large dataset with many bracket keys
largeData := make(map[string][]string) largeData := make(map[string][]string)
for i := 0; i < 100; i++ { for i := range 100 {
key := fmt.Sprintf("ids[%d]", i) key := fmt.Sprintf("ids[%d]", i)
largeData[key] = []string{fmt.Sprintf("value%d", i)} largeData[key] = []string{fmt.Sprintf("value%d", i)}
} }
for i := 0; i < 50; i++ { for i := range 50 {
key := fmt.Sprintf("names[%d]", i) key := fmt.Sprintf("names[%d]", i)
largeData[key] = []string{fmt.Sprintf("name%d", i)} largeData[key] = []string{fmt.Sprintf("name%d", i)}
} }
for i := 0; i < 25; i++ { for i := range 25 {
key := fmt.Sprintf("other[key%d]", i) key := fmt.Sprintf("other[key%d]", i)
largeData[key] = []string{fmt.Sprintf("other%d", i)} largeData[key] = []string{fmt.Sprintf("other%d", i)}
} }
// Test case 4: Dataset with many non-matching keys (worst case) // Test case 4: Dataset with many non-matching keys (worst case)
worstCaseData := make(map[string][]string) worstCaseData := make(map[string][]string)
for i := 0; i < 100; i++ { for i := range 100 {
key := fmt.Sprintf("nonmatching%d", i) key := fmt.Sprintf("nonmatching%d", i)
worstCaseData[key] = []string{fmt.Sprintf("value%d", i)} worstCaseData[key] = []string{fmt.Sprintf("value%d", i)}
} }
@ -3739,7 +3783,7 @@ func BenchmarkGetMapFromFormData(b *testing.B) {
for _, bm := range benchmarks { for _, bm := range benchmarks {
b.Run(bm.name, func(b *testing.B) { b.Run(bm.name, func(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for b.Loop() {
_, _ = getMapFromFormData(bm.data, bm.key) _, _ = getMapFromFormData(bm.data, bm.key)
} }
}) })

View File

@ -22,6 +22,7 @@
- [How to write log file](#how-to-write-log-file) - [How to write log file](#how-to-write-log-file)
- [Custom Log Format](#custom-log-format) - [Custom Log Format](#custom-log-format)
- [Controlling Log output coloring](#controlling-log-output-coloring) - [Controlling Log output coloring](#controlling-log-output-coloring)
- [Avoid logging query strings](#avoid-loging-query-strings)
- [Model binding and validation](#model-binding-and-validation) - [Model binding and validation](#model-binding-and-validation)
- [Custom Validators](#custom-validators) - [Custom Validators](#custom-validators)
- [Only Bind Query String](#only-bind-query-string) - [Only Bind Query String](#only-bind-query-string)
@ -592,6 +593,20 @@ func main() {
} }
``` ```
### Avoid logging query strings
```go
func main() {
router := gin.New()
// SkipQueryString indicates that the logger should not log the query string.
// For example, /path?q=1 will be logged as /path
loggerConfig := gin.LoggerConfig{SkipQueryString: true}
router.Use(gin.LoggerWithConfig(loggerConfig))
}
```
### Model binding and validation ### Model binding and validation
To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML, TOML and standard form values (foo=bar&boo=baz). To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML, TOML and standard form values (foo=bar&boo=baz).
@ -911,7 +926,7 @@ curl -X POST http://localhost:8080/person
NOTE: For default [collection values](#collection-format-for-arrays), the following rules apply: 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 - 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" - 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 +1024,68 @@ curl -v localhost:8088/thinkerou/not-uuid
### Bind custom unmarshaler ### 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 ```go
package main package main
import ( import (
"github.com/gin-gonic/gin" "encoding"
"strings" "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 already implements `encoding.TextUnmarshaler` but you want to customize how gin binds the type differently (eg to change what error message is returned), you can implement the dedicated `BindUnmarshaler` interface provided by gin instead.
```go
package main
import (
"strings"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
) )
type Birthday string type Birthday string
@ -1024,29 +1095,37 @@ func (b *Birthday) UnmarshalParam(param string) error {
return nil return nil
} }
var _ binding.BindUnmarshaler = (*Birthday)(nil) //assert Birthday implements binding.BindUnmarshaler
func main() { func main() {
route := gin.Default() route := gin.Default()
var request struct { 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) { route.GET("/test", func(ctx *gin.Context) {
_ = ctx.BindQuery(&request) _ = ctx.BindQuery(&request)
ctx.JSON(200, request.Birthday) ctx.JSON(200, request)
}) })
route.Run(":8088") _ = route.Run(":8088")
} }
``` ```
Test it with: Test it with:
```sh ```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 Result
```sh ```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 ### Bind Header
```go ```go

View File

@ -26,8 +26,6 @@ const (
ErrorTypePublic ErrorType = 1 << 1 ErrorTypePublic ErrorType = 1 << 1
// ErrorTypeAny indicates any other error. // ErrorTypeAny indicates any other error.
ErrorTypeAny ErrorType = 1<<64 - 1 ErrorTypeAny ErrorType = 1<<64 - 1
// ErrorTypeNu indicates any other error.
ErrorTypeNu = 2
) )
// Error represents a error's specification. // Error represents a error's specification.

View File

@ -400,7 +400,7 @@ func TestConcurrentHandleContext(t *testing.T) {
var wg sync.WaitGroup var wg sync.WaitGroup
iterations := 200 iterations := 200
wg.Add(iterations) wg.Add(iterations)
for i := 0; i < iterations; i++ { for range iterations {
go func() { go func() {
req, err := http.NewRequest(http.MethodGet, "/", nil) req, err := http.NewRequest(http.MethodGet, "/", nil)
assert.NoError(t, err) assert.NoError(t, err)

33
go.mod
View File

@ -2,41 +2,46 @@ module github.com/gin-gonic/gin
go 1.24.0 go 1.24.0
toolchain go1.24.7
require ( require (
github.com/bytedance/sonic v1.14.2 github.com/bytedance/sonic v1.15.0
github.com/gin-contrib/sse v1.1.0 github.com/gin-contrib/sse v1.1.0
github.com/go-playground/validator/v10 v10.28.0 github.com/go-playground/validator/v10 v10.30.1
github.com/goccy/go-json v0.10.2 github.com/goccy/go-json v0.10.5
github.com/goccy/go-yaml v1.19.0 github.com/goccy/go-yaml v1.19.2
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-isatty v0.0.20
github.com/modern-go/reflect2 v1.0.2 github.com/modern-go/reflect2 v1.0.2
github.com/pelletier/go-toml/v2 v2.2.4 github.com/pelletier/go-toml/v2 v2.2.4
github.com/quic-go/quic-go v0.57.1 github.com/quic-go/quic-go v0.59.0
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/ugorji/go/codec v1.3.1 github.com/ugorji/go/codec v1.3.1
golang.org/x/net v0.47.0 go.mongodb.org/mongo-driver/v2 v2.5.0
golang.org/x/net v0.50.0
google.golang.org/protobuf v1.36.10 google.golang.org/protobuf v1.36.10
) )
require gopkg.in/yaml.v3 v3.0.1 // indirect
require ( require (
github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
golang.org/x/arch v0.20.0 // indirect go.uber.org/mock v0.6.0 // indirect
golang.org/x/crypto v0.45.0 // indirect golang.org/x/arch v0.22.0 // indirect
golang.org/x/sys v0.38.0 // indirect golang.org/x/crypto v0.48.0 // indirect
golang.org/x/text v0.31.0 // indirect golang.org/x/sys v0.41.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect golang.org/x/text v0.34.0 // indirect
) )

59
go.sum
View File

@ -1,17 +1,17 @@
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@ -20,12 +20,12 @@ 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/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 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@ -41,8 +41,9 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
@ -51,8 +52,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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -70,21 +71,21 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -30,7 +30,7 @@ func rawStrToBytes(s string) []byte {
func TestBytesToString(t *testing.T) { func TestBytesToString(t *testing.T) {
data := make([]byte, 1024) data := make([]byte, 1024)
for i := 0; i < 100; i++ { for range 100 {
_, err := cRand.Read(data) _, err := cRand.Read(data)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -79,7 +79,7 @@ func RandStringBytesMaskImprSrcSB(n int) string {
} }
func TestStringToBytes(t *testing.T) { func TestStringToBytes(t *testing.T) {
for i := 0; i < 100; i++ { for range 100 {
s := RandStringBytesMaskImprSrcSB(64) s := RandStringBytesMaskImprSrcSB(64)
if !bytes.Equal(rawStrToBytes(s), StringToBytes(s)) { if !bytes.Equal(rawStrToBytes(s), StringToBytes(s)) {
t.Fatal("don't match") t.Fatal("don't match")

View File

@ -48,6 +48,11 @@ type LoggerConfig struct {
// Optional. // Optional.
SkipPaths []string SkipPaths []string
// SkipQueryString indicates that query strings should not be written
// for cases such as when API keys are passed via query strings.
// Optional. Default value is false.
SkipQueryString bool
// Skip is a Skipper that indicates which logs should not be written. // Skip is a Skipper that indicates which logs should not be written.
// Optional. // Optional.
Skip Skipper Skip Skipper
@ -298,7 +303,7 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
param.BodySize = c.Writer.Size() param.BodySize = c.Writer.Size()
if raw != "" { if raw != "" && !conf.SkipQueryString {
path = path + "?" + raw path = path + "?" + raw
} }

View File

@ -471,3 +471,17 @@ func TestForceConsoleColor(t *testing.T) {
// reset console color mode. // reset console color mode.
consoleColorMode = autoColor consoleColorMode = autoColor
} }
func TestLoggerWithConfigSkipQueryString(t *testing.T) {
buffer := new(strings.Builder)
router := New()
router.Use(LoggerWithConfig(LoggerConfig{
Output: buffer,
SkipQueryString: true,
}))
router.GET("/logged", func(c *Context) { c.Status(http.StatusOK) })
PerformRequest(router, "GET", "/logged?a=21")
assert.Contains(t, buffer.String(), "200")
assert.NotContains(t, buffer.String(), "a=21")
}

View File

@ -12,12 +12,12 @@ import (
"fmt" "fmt"
"io" "io"
"log" "log"
"net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"os" "os"
"runtime" "runtime"
"strings" "strings"
"syscall"
"time" "time"
"github.com/gin-gonic/gin/internal/bytesconv" "github.com/gin-gonic/gin/internal/bytesconv"
@ -57,40 +57,33 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
} }
return func(c *Context) { return func(c *Context) {
defer func() { defer func() {
if err := recover(); err != nil { if rec := recover(); rec != nil {
// Check for a broken connection, as it is not really a // Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace. // condition that warrants a panic stack trace.
var brokenPipe bool var isBrokenPipe bool
if ne, ok := err.(*net.OpError); ok { err, ok := rec.(error)
var se *os.SyscallError if ok {
if errors.As(ne, &se) { isBrokenPipe = errors.Is(err, syscall.EPIPE) ||
seStr := strings.ToLower(se.Error()) errors.Is(err, syscall.ECONNRESET) ||
if strings.Contains(seStr, "broken pipe") || errors.Is(err, http.ErrAbortHandler)
strings.Contains(seStr, "connection reset by peer") {
brokenPipe = true
}
}
}
if e, ok := err.(error); ok && errors.Is(e, http.ErrAbortHandler) {
brokenPipe = true
} }
if logger != nil { if logger != nil {
if brokenPipe { if isBrokenPipe {
logger.Printf("%s\n%s%s", err, secureRequestDump(c.Request), reset) logger.Printf("%s\n%s%s", rec, secureRequestDump(c.Request), reset)
} else if IsDebugging() { } else if IsDebugging() {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
timeFormat(time.Now()), secureRequestDump(c.Request), err, stack(stackSkip), reset) timeFormat(time.Now()), secureRequestDump(c.Request), rec, stack(stackSkip), reset)
} else { } else {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s", logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
timeFormat(time.Now()), err, stack(stackSkip), reset) timeFormat(time.Now()), rec, stack(stackSkip), reset)
} }
} }
if brokenPipe { if isBrokenPipe {
// If the connection is dead, we can't write a status to it. // If the connection is dead, we can't write a status to it.
c.Error(err.(error)) //nolint: errcheck c.Error(err) //nolint: errcheck
c.Abort() c.Abort()
} else { } else {
handle(c, err) handle(c, rec)
} }
} }
}() }()

View File

@ -22,7 +22,7 @@ func TestPanicClean(t *testing.T) {
router.Use(RecoveryWithWriter(buffer)) router.Use(RecoveryWithWriter(buffer))
router.GET("/recovery", func(c *Context) { router.GET("/recovery", func(c *Context) {
c.AbortWithStatus(http.StatusBadRequest) c.AbortWithStatus(http.StatusBadRequest)
panic("Oupps, Houston, we have a problem") panic("Oops, Houston, we have a problem")
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/recovery", w := PerformRequest(router, http.MethodGet, "/recovery",
@ -52,14 +52,14 @@ func TestPanicInHandler(t *testing.T) {
router := New() router := New()
router.Use(RecoveryWithWriter(buffer)) router.Use(RecoveryWithWriter(buffer))
router.GET("/recovery", func(_ *Context) { router.GET("/recovery", func(_ *Context) {
panic("Oupps, Houston, we have a problem") panic("Oops, Houston, we have a problem")
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/recovery") w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, buffer.String(), "panic recovered") assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem") assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem")
assert.Contains(t, buffer.String(), t.Name()) assert.Contains(t, buffer.String(), t.Name())
assert.NotContains(t, buffer.String(), "GET /recovery") assert.NotContains(t, buffer.String(), "GET /recovery")
@ -80,7 +80,7 @@ func TestPanicWithAbort(t *testing.T) {
router.Use(RecoveryWithWriter(nil)) router.Use(RecoveryWithWriter(nil))
router.GET("/recovery", func(c *Context) { router.GET("/recovery", func(c *Context) {
c.AbortWithStatus(http.StatusBadRequest) c.AbortWithStatus(http.StatusBadRequest)
panic("Oupps, Houston, we have a problem") panic("Oops, Houston, we have a problem")
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/recovery") w := PerformRequest(router, http.MethodGet, "/recovery")
@ -98,13 +98,13 @@ func TestFunction(t *testing.T) {
func TestPanicWithBrokenPipe(t *testing.T) { func TestPanicWithBrokenPipe(t *testing.T) {
const expectCode = 204 const expectCode = 204
expectMsgs := map[syscall.Errno]string{ expectErrnos := []syscall.Errno{
syscall.EPIPE: "broken pipe", syscall.EPIPE,
syscall.ECONNRESET: "connection reset by peer", syscall.ECONNRESET,
} }
for errno, expectMsg := range expectMsgs { for _, errno := range expectErrnos {
t.Run(expectMsg, func(t *testing.T) { t.Run("Recovery from "+errno.Error(), func(t *testing.T) {
var buf strings.Builder var buf strings.Builder
router := New() router := New()
@ -122,7 +122,8 @@ func TestPanicWithBrokenPipe(t *testing.T) {
w := PerformRequest(router, http.MethodGet, "/recovery") w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST // TEST
assert.Equal(t, expectCode, w.Code) assert.Equal(t, expectCode, w.Code)
assert.Contains(t, strings.ToLower(buf.String()), expectMsg) assert.Contains(t, strings.ToLower(buf.String()), errno.Error())
assert.NotContains(t, strings.ToLower(buf.String()), "[Recovery]")
}) })
} }
} }
@ -161,14 +162,14 @@ func TestCustomRecoveryWithWriter(t *testing.T) {
} }
router.Use(CustomRecoveryWithWriter(buffer, handleRecovery)) router.Use(CustomRecoveryWithWriter(buffer, handleRecovery))
router.GET("/recovery", func(_ *Context) { router.GET("/recovery", func(_ *Context) {
panic("Oupps, Houston, we have a problem") panic("Oops, Houston, we have a problem")
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/recovery") w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered") assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem") assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem")
assert.Contains(t, buffer.String(), t.Name()) assert.Contains(t, buffer.String(), t.Name())
assert.NotContains(t, buffer.String(), "GET /recovery") assert.NotContains(t, buffer.String(), "GET /recovery")
@ -180,7 +181,7 @@ func TestCustomRecoveryWithWriter(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery") assert.Contains(t, buffer.String(), "GET /recovery")
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String()) assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String())
SetMode(TestMode) SetMode(TestMode)
} }
@ -196,14 +197,14 @@ func TestCustomRecovery(t *testing.T) {
} }
router.Use(CustomRecovery(handleRecovery)) router.Use(CustomRecovery(handleRecovery))
router.GET("/recovery", func(_ *Context) { router.GET("/recovery", func(_ *Context) {
panic("Oupps, Houston, we have a problem") panic("Oops, Houston, we have a problem")
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/recovery") w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered") assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem") assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem")
assert.Contains(t, buffer.String(), t.Name()) assert.Contains(t, buffer.String(), t.Name())
assert.NotContains(t, buffer.String(), "GET /recovery") assert.NotContains(t, buffer.String(), "GET /recovery")
@ -215,7 +216,7 @@ func TestCustomRecovery(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery") assert.Contains(t, buffer.String(), "GET /recovery")
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String()) assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String())
SetMode(TestMode) SetMode(TestMode)
} }
@ -231,14 +232,14 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
} }
router.Use(RecoveryWithWriter(DefaultErrorWriter, handleRecovery)) router.Use(RecoveryWithWriter(DefaultErrorWriter, handleRecovery))
router.GET("/recovery", func(_ *Context) { router.GET("/recovery", func(_ *Context) {
panic("Oupps, Houston, we have a problem") panic("Oops, Houston, we have a problem")
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/recovery") w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered") assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem") assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem")
assert.Contains(t, buffer.String(), t.Name()) assert.Contains(t, buffer.String(), t.Name())
assert.NotContains(t, buffer.String(), "GET /recovery") assert.NotContains(t, buffer.String(), "GET /recovery")
@ -250,7 +251,7 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery") assert.Contains(t, buffer.String(), "GET /recovery")
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String()) assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String())
SetMode(TestMode) SetMode(TestMode)
} }

34
render/bson.go Normal file
View File

@ -0,0 +1,34 @@
// Copyright 2025 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package render
import (
"net/http"
"go.mongodb.org/mongo-driver/v2/bson"
)
// BSON contains the given interface object.
type BSON struct {
Data any
}
var bsonContentType = []string{"application/bson"}
// Render (BSON) marshals the given interface object and writes data with custom ContentType.
func (r BSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
bytes, err := bson.Marshal(&r.Data)
if err == nil {
_, err = w.Write(bytes)
}
return err
}
// WriteContentType (BSONBuf) writes BSONBuf ContentType.
func (r BSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, bsonContentType)
}

View File

@ -4,7 +4,10 @@
package render package render
import "net/http" import (
"net/http"
"strconv"
)
// Data contains ContentType and bytes data. // Data contains ContentType and bytes data.
type Data struct { type Data struct {
@ -15,6 +18,9 @@ type Data struct {
// Render (Data) writes data with custom ContentType. // Render (Data) writes data with custom ContentType.
func (r Data) Render(w http.ResponseWriter) (err error) { func (r Data) Render(w http.ResponseWriter) (err error) {
r.WriteContentType(w) r.WriteContentType(w)
if len(r.Data) > 0 {
w.Header().Set("Content-Length", strconv.Itoa(len(r.Data)))
}
_, err = w.Write(r.Data) _, err = w.Write(r.Data)
return return
} }

View File

@ -7,7 +7,7 @@
package render package render
import ( import (
"bytes" "errors"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
@ -16,9 +16,6 @@ import (
"github.com/ugorji/go/codec" "github.com/ugorji/go/codec"
) )
// TODO unit tests
// test errors
func TestRenderMsgPack(t *testing.T) { func TestRenderMsgPack(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
data := map[string]any{ data := map[string]any{
@ -32,13 +29,52 @@ func TestRenderMsgPack(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
h := new(codec.MsgpackHandle) var decoded map[string]any
assert.NotNil(t, h) var mh codec.MsgpackHandle
buf := bytes.NewBuffer([]byte{}) mh.RawToString = true
assert.NotNil(t, buf) err = codec.NewDecoderBytes(w.Body.Bytes(), &mh).Decode(&decoded)
err = codec.NewEncoder(buf, h).Encode(data)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, w.Body.String(), buf.String()) assert.Equal(t, data, decoded)
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
} }
func TestWriteMsgPack(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]any{
"foo": "bar",
"num": 42,
}
err := WriteMsgPack(w, data)
require.NoError(t, err)
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
var decoded map[string]any
var mh codec.MsgpackHandle
mh.RawToString = true
err = codec.NewDecoderBytes(w.Body.Bytes(), &mh).Decode(&decoded)
require.NoError(t, err)
assert.Len(t, decoded, 2)
assert.Equal(t, "bar", decoded["foo"])
assert.EqualValues(t, 42, decoded["num"])
}
type failWriter struct {
*httptest.ResponseRecorder
}
func (w *failWriter) Write(data []byte) (int, error) {
return 0, errors.New("write error")
}
func TestRenderMsgPackError(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]any{
"foo": "bar",
}
err := (MsgPack{data}).Render(&failWriter{w})
require.Error(t, err)
assert.Contains(t, err.Error(), "write error")
}

View File

@ -8,6 +8,7 @@ import (
"encoding/xml" "encoding/xml"
"errors" "errors"
"html/template" "html/template"
"io"
"net" "net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -19,6 +20,7 @@ import (
testdata "github.com/gin-gonic/gin/testdata/protoexample" testdata "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/v2/bson"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
@ -359,6 +361,31 @@ func TestRenderProtoBufFail(t *testing.T) {
require.Error(t, err) require.Error(t, err)
} }
func TestRenderBSON(t *testing.T) {
w := httptest.NewRecorder()
reps := []int64{int64(1), int64(2)}
type mystruct struct {
Label string
Reps []int64
}
data := &mystruct{
Label: "test",
Reps: reps,
}
(BSON{data}).WriteContentType(w)
bsonData, err := bson.Marshal(data)
require.NoError(t, err)
assert.Equal(t, "application/bson", w.Header().Get("Content-Type"))
err = (BSON{data}).Render(w)
require.NoError(t, err)
assert.Equal(t, bsonData, w.Body.Bytes())
assert.Equal(t, "application/bson", w.Header().Get("Content-Type"))
}
func TestRenderXML(t *testing.T) { func TestRenderXML(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
data := xmlmap{ data := xmlmap{
@ -427,6 +454,36 @@ func TestRenderData(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "#!PNG some raw data", w.Body.String()) assert.Equal(t, "#!PNG some raw data", w.Body.String())
assert.Equal(t, "image/png", w.Header().Get("Content-Type")) assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
assert.Equal(t, "19", w.Header().Get("Content-Length"))
}
func TestRenderDataContentLength(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
size, err := strconv.Atoi(r.URL.Query().Get("size"))
assert.NoError(t, err)
data := Data{
ContentType: "application/octet-stream",
Data: make([]byte, size),
}
assert.NoError(t, data.Render(w))
}))
t.Cleanup(srv.Close)
for _, size := range []int{0, 1, 100, 100_000} {
t.Run(strconv.Itoa(size), func(t *testing.T) {
resp, err := http.Get(srv.URL + "?size=" + strconv.Itoa(size))
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, "application/octet-stream", resp.Header.Get("Content-Type"))
assert.Equal(t, strconv.Itoa(size), resp.Header.Get("Content-Length"))
actual, err := io.Copy(io.Discard, resp.Body)
require.NoError(t, err)
assert.EqualValues(t, size, actual)
})
}
} }
func TestRenderString(t *testing.T) { func TestRenderString(t *testing.T) {

View File

@ -169,7 +169,7 @@ func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes {
}) })
} }
// StaticFileFS works just like `StaticFile` but a custom `http.FileSystem` can be used instead.. // StaticFileFS works just like `StaticFile` but a custom `http.FileSystem` can be used instead.
// router.StaticFileFS("favicon.ico", "./resources/favicon.ico", Dir{".", false}) // router.StaticFileFS("favicon.ico", "./resources/favicon.ico", Dir{".", false})
// Gin by default uses: gin.Dir() // Gin by default uses: gin.Dir()
func (group *RouterGroup) StaticFileFS(relativePath, filepath string, fs http.FileSystem) IRoutes { func (group *RouterGroup) StaticFileFS(relativePath, filepath string, fs http.FileSystem) IRoutes {

View File

@ -671,12 +671,7 @@ walk: // Outer loop for walking the tree
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]byte, bool) { func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]byte, bool) {
const stackBufSize = 128 const stackBufSize = 128
// Use a static sized buffer on the stack in the common case. buf := make([]byte, 0, max(stackBufSize, len(path)+1))
// If the path is too long, allocate a buffer on the heap instead.
buf := make([]byte, 0, stackBufSize)
if length := len(path) + 1; length > stackBufSize {
buf = make([]byte, 0, length)
}
ciPath := n.findCaseInsensitivePathRec( ciPath := n.findCaseInsensitivePathRec(
path, path,

View File

@ -162,7 +162,7 @@ func resolveAddress(addr []string) string {
// https://stackoverflow.com/questions/53069040/checking-a-string-contains-only-ascii-characters // https://stackoverflow.com/questions/53069040/checking-a-string-contains-only-ascii-characters
func isASCII(s string) bool { func isASCII(s string) bool {
for i := 0; i < len(s); i++ { for i := range len(s) {
if s[i] > unicode.MaxASCII { if s[i] > unicode.MaxASCII {
return false return false
} }