mirror of
https://github.com/gin-gonic/gin.git
synced 2026-06-10 06:28:24 +08:00
Compare commits
30 Commits
d3af8d8d43
...
bfadd1aa6b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfadd1aa6b | ||
|
|
d7776de7d4 | ||
|
|
e3118cc378 | ||
|
|
cad29c5e3f | ||
|
|
d9e5cdf9c6 | ||
|
|
53410d2e07 | ||
|
|
ac95fa6bbc | ||
|
|
192ac89eef | ||
|
|
b2b489dbf4 | ||
|
|
3ab698dc51 | ||
|
|
9914178584 | ||
|
|
915e4c90d2 | ||
|
|
26c3a62865 | ||
|
|
22c274c84b | ||
|
|
d1a15347b1 | ||
|
|
64a6ed9a41 | ||
|
|
19b877fa50 | ||
|
|
2a794cd0b0 | ||
|
|
b917b14ff9 | ||
|
|
fad706f121 | ||
|
|
f416d1e594 | ||
|
|
583db590ec | ||
|
|
af6e8b70b8 | ||
|
|
63dd3e60ca | ||
|
|
c358d5656d | ||
|
|
771dcc6476 | ||
|
|
52ecf029bd | ||
|
|
440eb14ab8 | ||
|
|
ecb3f7b5e2 | ||
|
|
c4e2267954 |
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
6
.github/workflows/gin.yml
vendored
6
.github/workflows/gin.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
@ -61,11 +61,11 @@ jobs:
|
||||
cache: false
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
${{ matrix.go-build }}
|
||||
|
||||
2
.github/workflows/goreleaser.yml
vendored
2
.github/workflows/goreleaser.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
|
||||
2
.github/workflows/trivy-scan.yml
vendored
2
.github/workflows/trivy-scan.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@ -154,7 +154,7 @@ func runRequest(B *testing.B, r *Engine, method, path string) {
|
||||
w := newMockWriter()
|
||||
B.ReportAllocs()
|
||||
B.ResetTimer()
|
||||
for i := 0; i < B.N; i++ {
|
||||
for B.Loop() {
|
||||
r.ServeHTTP(w, req)
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ const (
|
||||
MIMEYAML = "application/x-yaml"
|
||||
MIMEYAML2 = "application/yaml"
|
||||
MIMETOML = "application/toml"
|
||||
MIMEBSON = "application/bson"
|
||||
)
|
||||
|
||||
// Binding describes the interface which needs to be implemented for binding the
|
||||
@ -86,6 +87,7 @@ var (
|
||||
Header Binding = headerBinding{}
|
||||
Plain BindingBody = plainBinding{}
|
||||
TOML BindingBody = tomlBinding{}
|
||||
BSON BindingBody = bsonBinding{}
|
||||
)
|
||||
|
||||
// Default returns the appropriate Binding instance based on the HTTP method
|
||||
@ -110,6 +112,8 @@ func Default(method, contentType string) Binding {
|
||||
return TOML
|
||||
case MIMEMultipartPOSTForm:
|
||||
return FormMultipart
|
||||
case MIMEBSON:
|
||||
return BSON
|
||||
default: // case MIMEPOSTForm:
|
||||
return Form
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ const (
|
||||
MIMEYAML = "application/x-yaml"
|
||||
MIMEYAML2 = "application/yaml"
|
||||
MIMETOML = "application/toml"
|
||||
MIMEBSON = "application/bson"
|
||||
)
|
||||
|
||||
// Binding describes the interface which needs to be implemented for binding the
|
||||
@ -82,6 +83,7 @@ var (
|
||||
Header = headerBinding{}
|
||||
TOML = tomlBinding{}
|
||||
Plain = plainBinding{}
|
||||
BSON BindingBody = bsonBinding{}
|
||||
)
|
||||
|
||||
// Default returns the appropriate Binding instance based on the HTTP method
|
||||
@ -104,6 +106,8 @@ func Default(method, contentType string) Binding {
|
||||
return FormMultipart
|
||||
case MIMETOML:
|
||||
return TOML
|
||||
case MIMEBSON:
|
||||
return BSON
|
||||
default: // case MIMEPOSTForm:
|
||||
return Form
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"github.com/gin-gonic/gin/testdata/protoexample"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"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.MethodPut, MIMETOML))
|
||||
|
||||
assert.Equal(t, BSON, Default(http.MethodPost, MIMEBSON))
|
||||
assert.Equal(t, BSON, Default(http.MethodPut, MIMEBSON))
|
||||
}
|
||||
|
||||
func TestBindingJSONNilBody(t *testing.T) {
|
||||
@ -731,6 +735,18 @@ func TestBindingProtoBufFail(t *testing.T) {
|
||||
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) {
|
||||
var obj FooStruct
|
||||
req := requestWithBody(http.MethodPost, "/", `{"bar": "foo"}`)
|
||||
|
||||
30
binding/bson.go
Normal file
30
binding/bson.go
Normal 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/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)
|
||||
}
|
||||
@ -27,7 +27,7 @@ func (err SliceValidationError) Error() string {
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
for i := 0; i < len(err); i++ {
|
||||
for i := range len(err) {
|
||||
if err[i] != nil {
|
||||
if b.Len() > 0 {
|
||||
b.WriteString("\n")
|
||||
@ -58,7 +58,7 @@ func (v *defaultValidator) ValidateStruct(obj any) error {
|
||||
case reflect.Slice, reflect.Array:
|
||||
count := value.Len()
|
||||
validateRet := make(SliceValidationError, 0)
|
||||
for i := 0; i < count; i++ {
|
||||
for i := range count {
|
||||
if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
|
||||
validateRet = append(validateRet, err)
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
@ -118,7 +119,7 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
|
||||
tValue := value.Type()
|
||||
|
||||
var isSet bool
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
for i := range value.NumField() {
|
||||
sf := tValue.Field(i)
|
||||
if sf.PkgPath != "" && !sf.Anonymous { // unexported
|
||||
continue
|
||||
@ -137,6 +138,8 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
|
||||
type setOptions struct {
|
||||
isDefaultExists bool
|
||||
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) {
|
||||
@ -168,6 +171,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
|
||||
}
|
||||
}
|
||||
|
||||
@ -191,6 +196,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" {
|
||||
@ -208,7 +227,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
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -252,7 +273,7 @@ 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 len(vs) == 0 {
|
||||
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
|
||||
}
|
||||
|
||||
@ -279,27 +302,37 @@ 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
|
||||
}
|
||||
|
||||
// If it is a string type, no spaces are removed, and the user data is not modified here
|
||||
if value.Kind() != reflect.String {
|
||||
val = strings.TrimSpace(val)
|
||||
}
|
||||
|
||||
switch value.Kind() {
|
||||
case reflect.Int:
|
||||
return setIntField(val, 0, value)
|
||||
@ -347,7 +380,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
|
||||
}
|
||||
@ -404,6 +437,11 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
|
||||
timeFormat = time.RFC3339
|
||||
}
|
||||
|
||||
if val == "" {
|
||||
value.Set(reflect.ValueOf(time.Time{}))
|
||||
return nil
|
||||
}
|
||||
|
||||
switch tf := strings.ToLower(timeFormat); tf {
|
||||
case "unix", "unixmilli", "unixmicro", "unixnano":
|
||||
tv, err := strconv.ParseInt(val, 10, 64)
|
||||
@ -427,11 +465,6 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
|
||||
return nil
|
||||
}
|
||||
|
||||
if val == "" {
|
||||
value.Set(reflect.ValueOf(time.Time{}))
|
||||
return nil
|
||||
}
|
||||
|
||||
l := time.Local
|
||||
if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC {
|
||||
l = time.UTC
|
||||
@ -454,9 +487,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
|
||||
}
|
||||
@ -464,9 +497,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
|
||||
}
|
||||
@ -475,6 +508,10 @@ func setSlice(vals []string, value reflect.Value, field reflect.StructField) err
|
||||
}
|
||||
|
||||
func setTimeDuration(val string, value reflect.Value) error {
|
||||
if val == "" {
|
||||
val = "0"
|
||||
}
|
||||
|
||||
d, err := time.ParseDuration(val)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"mime/multipart"
|
||||
@ -226,7 +227,35 @@ func TestMappingTime(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
type bindTestData struct {
|
||||
need any
|
||||
got any
|
||||
in map[string][]string
|
||||
}
|
||||
|
||||
func TestMappingTimeUnixNano(t *testing.T) {
|
||||
type needFixUnixNanoEmpty struct {
|
||||
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
|
||||
}
|
||||
|
||||
// ok
|
||||
tests := []bindTestData{
|
||||
{need: &needFixUnixNanoEmpty{}, got: &needFixUnixNanoEmpty{}, in: formSource{"createTime": []string{" "}}},
|
||||
{need: &needFixUnixNanoEmpty{}, got: &needFixUnixNanoEmpty{}, in: formSource{"createTime": []string{}}},
|
||||
}
|
||||
|
||||
for _, v := range tests {
|
||||
err := mapForm(v.got, v.in)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, v.need, v.got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMappingTimeDuration(t *testing.T) {
|
||||
type needFixDurationEmpty struct {
|
||||
Duration time.Duration `form:"duration"`
|
||||
}
|
||||
|
||||
var s struct {
|
||||
D time.Duration
|
||||
}
|
||||
@ -236,6 +265,17 @@ func TestMappingTimeDuration(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 5*time.Second, s.D)
|
||||
|
||||
// ok
|
||||
tests := []bindTestData{
|
||||
{need: &needFixDurationEmpty{}, got: &needFixDurationEmpty{}, in: formSource{"duration": []string{" "}}},
|
||||
{need: &needFixDurationEmpty{}, got: &needFixDurationEmpty{}, in: formSource{"duration": []string{}}},
|
||||
}
|
||||
|
||||
for _, v := range tests {
|
||||
err := mapForm(v.got, v.in)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, v.need, v.got)
|
||||
}
|
||||
// error
|
||||
err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form")
|
||||
require.Error(t, err)
|
||||
@ -485,6 +525,16 @@ func TestMappingCustomUnmarshalParamHexWithURITag(t *testing.T) {
|
||||
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 {
|
||||
Protocol string
|
||||
Path string
|
||||
@ -585,6 +635,33 @@ func TestMappingCustomSliceForm(t *testing.T) {
|
||||
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
|
||||
|
||||
func (o *objectID) UnmarshalParam(param string) error {
|
||||
@ -636,6 +713,358 @@ func TestMappingCustomArrayForm(t *testing.T) {
|
||||
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) {
|
||||
t.Run("slice with default", func(t *testing.T) {
|
||||
var s struct {
|
||||
|
||||
59
context.go
59
context.go
@ -40,6 +40,7 @@ const (
|
||||
MIMEYAML2 = binding.MIMEYAML2
|
||||
MIMETOML = binding.MIMETOML
|
||||
MIMEPROTOBUF = binding.MIMEPROTOBUF
|
||||
MIMEBSON = binding.MIMEBSON
|
||||
)
|
||||
|
||||
// BodyBytesKey indicates a default body bytes key.
|
||||
@ -55,14 +56,6 @@ const ContextRequestKey ContextKeyType = 0
|
||||
// abortIndex represents a typical value used in abort functions.
|
||||
const abortIndex int8 = math.MaxInt8 >> 1
|
||||
|
||||
// safeInt8 converts int to int8 safely, capping at math.MaxInt8
|
||||
func safeInt8(n int) int8 {
|
||||
if n > math.MaxInt8 {
|
||||
return math.MaxInt8
|
||||
}
|
||||
return int8(n)
|
||||
}
|
||||
|
||||
// Context is the most important part of gin. It allows us to pass variables between middleware,
|
||||
// manage the flow, validate the JSON of a request and render a JSON response for example.
|
||||
type Context struct {
|
||||
@ -394,6 +387,11 @@ func (c *Context) GetDuration(key any) time.Duration {
|
||||
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.
|
||||
func (c *Context) GetIntSlice(key any) []int {
|
||||
return getTyped[[]int](c, key)
|
||||
@ -459,6 +457,11 @@ func (c *Context) GetStringSlice(key any) []string {
|
||||
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.
|
||||
func (c *Context) GetStringMap(key any) map[string]any {
|
||||
return getTyped[map[string]any](c, key)
|
||||
@ -986,18 +989,32 @@ func (c *Context) ClientIP() string {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 ""
|
||||
var (
|
||||
trusted bool
|
||||
remoteIP net.IP
|
||||
)
|
||||
// If gin is listening a unix socket, always trust it.
|
||||
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 {
|
||||
for _, headerName := range c.engine.RemoteIPHeaders {
|
||||
ip, valid := c.engine.validateHeader(c.requestHeader(headerName))
|
||||
headerValue := strings.Join(c.Request.Header.Values(headerName), ",")
|
||||
ip, valid := c.engine.validateHeader(headerValue)
|
||||
if valid {
|
||||
return ip
|
||||
}
|
||||
@ -1221,6 +1238,11 @@ func (c *Context) ProtoBuf(code int, obj any) {
|
||||
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.
|
||||
func (c *Context) String(code int, format string, values ...any) {
|
||||
c.Render(code, render.String{Format: format, Data: values})
|
||||
@ -1328,6 +1350,7 @@ type Negotiate struct {
|
||||
Data any
|
||||
TOMLData any
|
||||
PROTOBUFData any
|
||||
BSONData any
|
||||
}
|
||||
|
||||
// Negotiate calls different Render according to acceptable Accept format.
|
||||
@ -1357,6 +1380,10 @@ func (c *Context) Negotiate(code int, config Negotiate) {
|
||||
data := chooseData(config.PROTOBUFData, config.Data)
|
||||
c.ProtoBuf(code, data)
|
||||
|
||||
case binding.MIMEBSON:
|
||||
data := chooseData(config.BSONData, config.Data)
|
||||
c.BSON(code, data)
|
||||
|
||||
default:
|
||||
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck
|
||||
}
|
||||
|
||||
@ -32,6 +32,7 @@ import (
|
||||
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
@ -516,6 +517,14 @@ func TestContextGetDuration(t *testing.T) {
|
||||
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) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
key := "int-slice"
|
||||
@ -618,6 +627,14 @@ func TestContextGetStringSlice(t *testing.T) {
|
||||
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) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
m := make(map[string]any)
|
||||
@ -1143,6 +1160,37 @@ func TestContextRenderNoContentIndentedJSON(t *testing.T) {
|
||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
func TestContextClientIPWithMultipleHeaders(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
c.Request, _ = http.NewRequest(http.MethodGet, "/test", nil)
|
||||
|
||||
// Multiple X-Forwarded-For headers
|
||||
c.Request.Header.Add("X-Forwarded-For", "1.2.3.4, "+localhostIP)
|
||||
c.Request.Header.Add("X-Forwarded-For", "5.6.7.8")
|
||||
c.Request.RemoteAddr = localhostIP + ":1234"
|
||||
|
||||
c.engine.ForwardedByClientIP = true
|
||||
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
|
||||
_ = c.engine.SetTrustedProxies([]string{localhostIP})
|
||||
|
||||
// Should return 5.6.7.8 (last non-trusted IP)
|
||||
assert.Equal(t, "5.6.7.8", c.ClientIP())
|
||||
}
|
||||
|
||||
func TestContextClientIPWithSingleHeader(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
c.Request, _ = http.NewRequest(http.MethodGet, "/test", nil)
|
||||
c.Request.Header.Set("X-Forwarded-For", "1.2.3.4, "+localhostIP)
|
||||
c.Request.RemoteAddr = localhostIP + ":1234"
|
||||
|
||||
c.engine.ForwardedByClientIP = true
|
||||
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
|
||||
_ = c.engine.SetTrustedProxies([]string{localhostIP})
|
||||
|
||||
// Should return 1.2.3.4
|
||||
assert.Equal(t, "1.2.3.4", c.ClientIP())
|
||||
}
|
||||
|
||||
// Tests that the response is serialized as Secure JSON
|
||||
// and Content-Type is set to application/json
|
||||
func TestContextRenderSecureJSON(t *testing.T) {
|
||||
@ -1654,6 +1702,23 @@ func TestContextNegotiationWithPROTOBUF(t *testing.T) {
|
||||
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) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
@ -1884,6 +1949,16 @@ func TestContextClientIP(t *testing.T) {
|
||||
c.engine.trustedCIDRs, _ = c.engine.prepareTrustedCIDRs()
|
||||
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
|
||||
// (insecure!) old behaviour)
|
||||
assert.Equal(t, "20.20.20.20", c.ClientIP())
|
||||
@ -1910,7 +1985,7 @@ func TestContextClientIP(t *testing.T) {
|
||||
resetContextForClientIPTests(c)
|
||||
|
||||
// IPv6 support
|
||||
c.Request.RemoteAddr = "[::1]:12345"
|
||||
c.Request.RemoteAddr = fmt.Sprintf("[%s]:12345", localhostIPv6)
|
||||
assert.Equal(t, "20.20.20.20", c.ClientIP())
|
||||
|
||||
resetContextForClientIPTests(c)
|
||||
@ -3212,7 +3287,7 @@ func TestContextCopyShouldNotCancel(t *testing.T) {
|
||||
}()
|
||||
|
||||
addr := strings.Split(l.Addr().String(), ":")
|
||||
res, err := http.Get(fmt.Sprintf("http://127.0.0.1:%s/", addr[len(addr)-1]))
|
||||
res, err := http.Get(fmt.Sprintf("http://%s:%s/", localhostIP, addr[len(addr)-1]))
|
||||
if err != nil {
|
||||
t.Error(fmt.Errorf("request error: %w", err))
|
||||
return
|
||||
@ -3620,22 +3695,22 @@ func BenchmarkGetMapFromFormData(b *testing.B) {
|
||||
|
||||
// Test case 3: Large dataset with many bracket keys
|
||||
largeData := make(map[string][]string)
|
||||
for i := 0; i < 100; i++ {
|
||||
for i := range 100 {
|
||||
key := fmt.Sprintf("ids[%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)
|
||||
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)
|
||||
largeData[key] = []string{fmt.Sprintf("other%d", i)}
|
||||
}
|
||||
|
||||
// Test case 4: Dataset with many non-matching keys (worst case)
|
||||
worstCaseData := make(map[string][]string)
|
||||
for i := 0; i < 100; i++ {
|
||||
for i := range 100 {
|
||||
key := fmt.Sprintf("nonmatching%d", i)
|
||||
worstCaseData[key] = []string{fmt.Sprintf("value%d", i)}
|
||||
}
|
||||
@ -3671,7 +3746,7 @@ func BenchmarkGetMapFromFormData(b *testing.B) {
|
||||
for _, bm := range benchmarks {
|
||||
b.Run(bm.name, func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
_, _ = getMapFromFormData(bm.data, bm.key)
|
||||
}
|
||||
})
|
||||
|
||||
6
debug.go
6
debug.go
@ -13,7 +13,9 @@ import (
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
const ginSupportMinGoVer = 23
|
||||
const ginSupportMinGoVer = 24
|
||||
|
||||
var runtimeVersion = runtime.Version()
|
||||
|
||||
// IsDebugging returns true if the framework is running in debug mode.
|
||||
// Use SetMode(gin.ReleaseMode) to disable debug mode.
|
||||
@ -77,7 +79,7 @@ func getMinVer(v string) (uint64, error) {
|
||||
}
|
||||
|
||||
func debugPrintWARNINGDefault() {
|
||||
if v, e := getMinVer(runtime.Version()); e == nil && v < ginSupportMinGoVer {
|
||||
if v, e := getMinVer(runtimeVersion); e == nil && v < ginSupportMinGoVer {
|
||||
debugPrint(`[WARNING] Now Gin requires Go 1.24+.
|
||||
|
||||
`)
|
||||
|
||||
@ -12,7 +12,6 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@ -21,10 +20,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TODO
|
||||
// func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) {
|
||||
// func debugPrint(format string, values ...any) {
|
||||
|
||||
func TestIsDebugging(t *testing.T) {
|
||||
SetMode(DebugMode)
|
||||
assert.True(t, IsDebugging())
|
||||
@ -48,6 +43,18 @@ func TestDebugPrint(t *testing.T) {
|
||||
assert.Equal(t, "[GIN-debug] these are 2 error messages\n", re)
|
||||
}
|
||||
|
||||
func TestDebugPrintFunc(t *testing.T) {
|
||||
DebugPrintFunc = func(format string, values ...any) {
|
||||
fmt.Fprintf(DefaultWriter, "[GIN-debug] "+format, values...)
|
||||
}
|
||||
re := captureOutput(t, func() {
|
||||
SetMode(DebugMode)
|
||||
debugPrint("debug print func test: %d", 123)
|
||||
SetMode(TestMode)
|
||||
})
|
||||
assert.Regexp(t, `^\[GIN-debug\] debug print func test: 123`, re)
|
||||
}
|
||||
|
||||
func TestDebugPrintError(t *testing.T) {
|
||||
re := captureOutput(t, func() {
|
||||
SetMode(DebugMode)
|
||||
@ -104,12 +111,17 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
|
||||
debugPrintWARNINGDefault()
|
||||
SetMode(TestMode)
|
||||
})
|
||||
m, e := getMinVer(runtime.Version())
|
||||
if e == nil && m < ginSupportMinGoVer {
|
||||
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)
|
||||
}
|
||||
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
|
||||
}
|
||||
|
||||
func TestDebugPrintWARNINGDefaultWithUnsupportedVersion(t *testing.T) {
|
||||
runtimeVersion = "go1.23.12"
|
||||
re := captureOutput(t, func() {
|
||||
SetMode(DebugMode)
|
||||
debugPrintWARNINGDefault()
|
||||
SetMode(TestMode)
|
||||
})
|
||||
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)
|
||||
}
|
||||
|
||||
func TestDebugPrintWARNINGNew(t *testing.T) {
|
||||
|
||||
78
docs/doc.md
78
docs/doc.md
@ -911,7 +911,7 @@ curl -X POST http://localhost:8080/person
|
||||
|
||||
NOTE: For default [collection values](#collection-format-for-arrays), the following rules apply:
|
||||
- Since commas are used to delimit tag options, they are not supported within a default value and will result in undefined behavior
|
||||
- For the collection formats "multi" and "csv", a semicolon should be used in place of a comma to delimited default values
|
||||
- For the collection formats "multi" and "csv", a semicolon should be used in place of a comma to delimit default values
|
||||
- Since semicolons are used to delimit default values for "multi" and "csv", they are not supported within a default value for "multi" and "csv"
|
||||
|
||||
|
||||
@ -1009,12 +1009,68 @@ curl -v localhost:8088/thinkerou/not-uuid
|
||||
|
||||
### Bind custom unmarshaler
|
||||
|
||||
To override gin's default binding logic, define a function on your type that satisfies the `encoding.TextUnmarshaler` interface from the Golang standard library. Then specify `parser=encoding.TextUnmarshaler` in the `uri`/`form` tag of the field being bound.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"encoding"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Birthday string
|
||||
|
||||
func (b *Birthday) UnmarshalText(text []byte) error {
|
||||
*b = Birthday(strings.Replace(string(text), "-", "/", -1))
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ encoding.TextUnmarshaler = (*Birthday)(nil) //assert Birthday implements encoding.TextUnmarshaler
|
||||
|
||||
func main() {
|
||||
route := gin.Default()
|
||||
var request struct {
|
||||
Birthday Birthday `form:"birthday,parser=encoding.TextUnmarshaler"`
|
||||
Birthdays []Birthday `form:"birthdays,parser=encoding.TextUnmarshaler" collection_format:"csv"`
|
||||
BirthdaysDefault []Birthday `form:"birthdaysDef,default=2020-09-01;2020-09-02,parser=encoding.TextUnmarshaler" collection_format:"csv"`
|
||||
}
|
||||
route.GET("/test", func(ctx *gin.Context) {
|
||||
_ = ctx.BindQuery(&request)
|
||||
ctx.JSON(200, request)
|
||||
})
|
||||
_ = route.Run(":8088")
|
||||
}
|
||||
```
|
||||
|
||||
Test it with:
|
||||
|
||||
```sh
|
||||
curl 'localhost:8088/test?birthday=2000-01-01&birthdays=2000-01-01,2000-01-02'
|
||||
```
|
||||
Result
|
||||
```sh
|
||||
{"Birthday":"2000/01/01","Birthdays":["2000/01/01","2000/01/02"],"BirthdaysDefault":["2020/09/01","2020/09/02"]}
|
||||
```
|
||||
|
||||
Note:
|
||||
- If `parser=encoding.TextUnmarshaler` is specified for a type that does **not** implement `encoding.TextUnmarshaler`, gin will ignore it and proceed with its default binding logic.
|
||||
- If `parser=encoding.TextUnmarshaler` is specified for a type and that type's implementation of `encoding.TextUnmarshaler` returns an error, gin will stop binding and return the error to the client.
|
||||
|
||||
---
|
||||
|
||||
If a type 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
|
||||
@ -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
|
||||
|
||||
@ -81,7 +81,7 @@ func (msg *Error) MarshalJSON() ([]byte, error) {
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (msg Error) Error() string {
|
||||
func (msg *Error) Error() string {
|
||||
return msg.Err.Error()
|
||||
}
|
||||
|
||||
|
||||
37
gin.go
37
gin.go
@ -11,7 +11,6 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@ -48,11 +47,6 @@ var defaultTrustedCIDRs = []*net.IPNet{
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+")
|
||||
regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
|
||||
)
|
||||
|
||||
// HandlerFunc defines the handler used by gin middleware as return value.
|
||||
type HandlerFunc func(*Context)
|
||||
|
||||
@ -141,10 +135,16 @@ type Engine struct {
|
||||
AppEngine bool
|
||||
|
||||
// UseRawPath if enabled, the url.RawPath will be used to find parameters.
|
||||
// The RawPath is only a hint, EscapedPath() should be use instead. (https://pkg.go.dev/net/url@master#URL)
|
||||
// Only use RawPath if you know what you are doing.
|
||||
UseRawPath bool
|
||||
|
||||
// UseEscapedPath if enable, the url.EscapedPath() will be used to find parameters
|
||||
// It overrides UseRawPath
|
||||
UseEscapedPath bool
|
||||
|
||||
// UnescapePathValues if true, the path value will be unescaped.
|
||||
// If UseRawPath is false (by default), the UnescapePathValues effectively is true,
|
||||
// If UseRawPath and UseEscapedPath are false (by default), the UnescapePathValues effectively is true,
|
||||
// as url.Path gonna be used, which is already unescaped.
|
||||
UnescapePathValues bool
|
||||
|
||||
@ -197,6 +197,7 @@ var _ IRouter = (*Engine)(nil)
|
||||
// - HandleMethodNotAllowed: false
|
||||
// - ForwardedByClientIP: true
|
||||
// - UseRawPath: false
|
||||
// - UseEscapedPath: false
|
||||
// - UnescapePathValues: true
|
||||
func New(opts ...OptionFunc) *Engine {
|
||||
debugPrintWARNINGNew()
|
||||
@ -214,6 +215,7 @@ func New(opts ...OptionFunc) *Engine {
|
||||
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
|
||||
TrustedPlatform: defaultPlatform,
|
||||
UseRawPath: false,
|
||||
UseEscapedPath: false,
|
||||
RemoveExtraSlash: false,
|
||||
UnescapePathValues: true,
|
||||
MaxMultipartMemory: defaultMultipartMemory,
|
||||
@ -689,7 +691,11 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
|
||||
httpMethod := c.Request.Method
|
||||
rPath := c.Request.URL.Path
|
||||
unescape := false
|
||||
if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
|
||||
|
||||
if engine.UseEscapedPath {
|
||||
rPath = c.Request.URL.EscapedPath()
|
||||
unescape = engine.UnescapePathValues
|
||||
} else if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
|
||||
rPath = c.Request.URL.RawPath
|
||||
unescape = engine.UnescapePathValues
|
||||
}
|
||||
@ -776,8 +782,8 @@ func redirectTrailingSlash(c *Context) {
|
||||
req := c.Request
|
||||
p := req.URL.Path
|
||||
if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
|
||||
prefix = regSafePrefix.ReplaceAllString(prefix, "")
|
||||
prefix = regRemoveRepeatedChar.ReplaceAllString(prefix, "/")
|
||||
prefix = sanitizePathChars(prefix)
|
||||
prefix = removeRepeatedChar(prefix, '/')
|
||||
|
||||
p = prefix + "/" + req.URL.Path
|
||||
}
|
||||
@ -788,6 +794,17 @@ func redirectTrailingSlash(c *Context) {
|
||||
redirectRequest(c)
|
||||
}
|
||||
|
||||
// sanitizePathChars removes unsafe characters from path strings,
|
||||
// keeping only ASCII letters, ASCII numbers, forward slashes, and hyphens.
|
||||
func sanitizePathChars(s string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '/' || r == '-' {
|
||||
return r
|
||||
}
|
||||
return -1
|
||||
}, s)
|
||||
}
|
||||
|
||||
func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
|
||||
req := c.Request
|
||||
rPath := req.URL.Path
|
||||
|
||||
246
ginS/gins_test.go
Normal file
246
ginS/gins_test.go
Normal file
@ -0,0 +1,246 @@
|
||||
// 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 ginS
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gin.SetMode(gin.TestMode)
|
||||
}
|
||||
|
||||
func TestGET(t *testing.T) {
|
||||
GET("/test", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "test")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
engine().ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "test", w.Body.String())
|
||||
}
|
||||
|
||||
func TestPOST(t *testing.T) {
|
||||
POST("/post", func(c *gin.Context) {
|
||||
c.String(http.StatusCreated, "created")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/post", nil)
|
||||
w := httptest.NewRecorder()
|
||||
engine().ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
assert.Equal(t, "created", w.Body.String())
|
||||
}
|
||||
|
||||
func TestPUT(t *testing.T) {
|
||||
PUT("/put", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "updated")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodPut, "/put", nil)
|
||||
w := httptest.NewRecorder()
|
||||
engine().ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "updated", w.Body.String())
|
||||
}
|
||||
|
||||
func TestDELETE(t *testing.T) {
|
||||
DELETE("/delete", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "deleted")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodDelete, "/delete", nil)
|
||||
w := httptest.NewRecorder()
|
||||
engine().ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "deleted", w.Body.String())
|
||||
}
|
||||
|
||||
func TestPATCH(t *testing.T) {
|
||||
PATCH("/patch", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "patched")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodPatch, "/patch", nil)
|
||||
w := httptest.NewRecorder()
|
||||
engine().ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "patched", w.Body.String())
|
||||
}
|
||||
|
||||
func TestOPTIONS(t *testing.T) {
|
||||
OPTIONS("/options", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "options")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodOptions, "/options", nil)
|
||||
w := httptest.NewRecorder()
|
||||
engine().ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "options", w.Body.String())
|
||||
}
|
||||
|
||||
func TestHEAD(t *testing.T) {
|
||||
HEAD("/head", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "head")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodHead, "/head", nil)
|
||||
w := httptest.NewRecorder()
|
||||
engine().ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
func TestAny(t *testing.T) {
|
||||
Any("/any", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "any")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/any", nil)
|
||||
w := httptest.NewRecorder()
|
||||
engine().ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "any", w.Body.String())
|
||||
}
|
||||
|
||||
func TestHandle(t *testing.T) {
|
||||
Handle(http.MethodGet, "/handle", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "handle")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/handle", nil)
|
||||
w := httptest.NewRecorder()
|
||||
engine().ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "handle", w.Body.String())
|
||||
}
|
||||
|
||||
func TestGroup(t *testing.T) {
|
||||
group := Group("/group")
|
||||
group.GET("/test", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "group test")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/group/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
engine().ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "group test", w.Body.String())
|
||||
}
|
||||
|
||||
func TestUse(t *testing.T) {
|
||||
var middlewareExecuted bool
|
||||
Use(func(c *gin.Context) {
|
||||
middlewareExecuted = true
|
||||
c.Next()
|
||||
})
|
||||
|
||||
GET("/middleware-test", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/middleware-test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
engine().ServeHTTP(w, req)
|
||||
|
||||
assert.True(t, middlewareExecuted)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
func TestNoRoute(t *testing.T) {
|
||||
NoRoute(func(c *gin.Context) {
|
||||
c.String(http.StatusNotFound, "custom 404")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/nonexistent", nil)
|
||||
w := httptest.NewRecorder()
|
||||
engine().ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
assert.Equal(t, "custom 404", w.Body.String())
|
||||
}
|
||||
|
||||
func TestNoMethod(t *testing.T) {
|
||||
NoMethod(func(c *gin.Context) {
|
||||
c.String(http.StatusMethodNotAllowed, "method not allowed")
|
||||
})
|
||||
|
||||
// This just verifies that NoMethod is callable
|
||||
// Testing the actual behavior would require a separate engine instance
|
||||
assert.NotNil(t, engine())
|
||||
}
|
||||
|
||||
func TestRoutes(t *testing.T) {
|
||||
GET("/routes-test", func(c *gin.Context) {})
|
||||
|
||||
routes := Routes()
|
||||
assert.NotEmpty(t, routes)
|
||||
|
||||
found := false
|
||||
for _, route := range routes {
|
||||
if route.Path == "/routes-test" && route.Method == http.MethodGet {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found)
|
||||
}
|
||||
|
||||
func TestSetHTMLTemplate(t *testing.T) {
|
||||
tmpl := template.Must(template.New("test").Parse("Hello {{.}}"))
|
||||
SetHTMLTemplate(tmpl)
|
||||
|
||||
// Verify engine has template set
|
||||
assert.NotNil(t, engine())
|
||||
}
|
||||
|
||||
func TestStaticFile(t *testing.T) {
|
||||
StaticFile("/static-file", "../testdata/test_file.txt")
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/static-file", nil)
|
||||
w := httptest.NewRecorder()
|
||||
engine().ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
func TestStatic(t *testing.T) {
|
||||
Static("/static-dir", "../testdata")
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/static-dir/test_file.txt", nil)
|
||||
w := httptest.NewRecorder()
|
||||
engine().ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
func TestStaticFS(t *testing.T) {
|
||||
fs := http.Dir("../testdata")
|
||||
StaticFS("/static-fs", fs)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/static-fs/test_file.txt", nil)
|
||||
w := httptest.NewRecorder()
|
||||
engine().ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
}
|
||||
@ -70,9 +70,10 @@ func TestRunEmpty(t *testing.T) {
|
||||
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||
assert.NoError(t, router.Run())
|
||||
}()
|
||||
// have to wait for the goroutine to start and run the server
|
||||
// otherwise the main thread will complete
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
|
||||
// Wait for server to be ready with exponential backoff
|
||||
err := waitForServerReady("http://localhost:8080/example", 10)
|
||||
require.NoError(t, err, "server should start successfully")
|
||||
|
||||
require.Error(t, router.Run(":8080"))
|
||||
testRequest(t, "http://localhost:8080/example")
|
||||
@ -213,9 +214,10 @@ func TestRunEmptyWithEnv(t *testing.T) {
|
||||
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||
assert.NoError(t, router.Run())
|
||||
}()
|
||||
// have to wait for the goroutine to start and run the server
|
||||
// otherwise the main thread will complete
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
|
||||
// Wait for server to be ready with exponential backoff
|
||||
err := waitForServerReady("http://localhost:3123/example", 10)
|
||||
require.NoError(t, err, "server should start successfully")
|
||||
|
||||
require.Error(t, router.Run(":3123"))
|
||||
testRequest(t, "http://localhost:3123/example")
|
||||
@ -234,9 +236,10 @@ func TestRunWithPort(t *testing.T) {
|
||||
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||
assert.NoError(t, router.Run(":5150"))
|
||||
}()
|
||||
// have to wait for the goroutine to start and run the server
|
||||
// otherwise the main thread will complete
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
|
||||
// Wait for server to be ready with exponential backoff
|
||||
err := waitForServerReady("http://localhost:5150/example", 10)
|
||||
require.NoError(t, err, "server should start successfully")
|
||||
|
||||
require.Error(t, router.Run(":5150"))
|
||||
testRequest(t, "http://localhost:5150/example")
|
||||
@ -397,7 +400,7 @@ func TestConcurrentHandleContext(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
iterations := 200
|
||||
wg.Add(iterations)
|
||||
for i := 0; i < iterations; i++ {
|
||||
for range iterations {
|
||||
go func() {
|
||||
req, err := http.NewRequest(http.MethodGet, "/", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
74
gin_test.go
74
gin_test.go
@ -83,7 +83,7 @@ func TestLoadHTMLGlobDebugMode(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestH2c(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
ln, err := net.Listen("tcp", localhostIP+":0")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@ -545,6 +545,29 @@ func TestNoMethodWithoutGlobalHandlers(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRebuild404Handlers(t *testing.T) {
|
||||
var middleware0 HandlerFunc = func(c *Context) {}
|
||||
var middleware1 HandlerFunc = func(c *Context) {}
|
||||
|
||||
router := New()
|
||||
|
||||
// Initially, allNoRoute should be nil
|
||||
assert.Nil(t, router.allNoRoute)
|
||||
|
||||
// Set NoRoute handlers
|
||||
router.NoRoute(middleware0)
|
||||
assert.Len(t, router.allNoRoute, 1)
|
||||
assert.Len(t, router.noRoute, 1)
|
||||
compareFunc(t, router.allNoRoute[0], middleware0)
|
||||
|
||||
// Add Use middleware should trigger rebuild404Handlers
|
||||
router.Use(middleware1)
|
||||
assert.Len(t, router.allNoRoute, 2)
|
||||
assert.Len(t, router.Handlers, 1)
|
||||
assert.Len(t, router.noRoute, 1)
|
||||
|
||||
// Global middleware should come first
|
||||
compareFunc(t, router.allNoRoute[0], middleware1)
|
||||
compareFunc(t, router.allNoRoute[1], middleware0)
|
||||
}
|
||||
|
||||
func TestNoMethodWithGlobalHandlers(t *testing.T) {
|
||||
@ -720,6 +743,55 @@ func TestEngineHandleContextPreventsMiddlewareReEntry(t *testing.T) {
|
||||
assert.Equal(t, int64(1), handlerCounterV2)
|
||||
}
|
||||
|
||||
func TestEngineHandleContextUseEscapedPathPercentEncoded(t *testing.T) {
|
||||
r := New()
|
||||
r.UseEscapedPath = true
|
||||
r.UnescapePathValues = false
|
||||
|
||||
r.GET("/v1/:path", func(c *Context) {
|
||||
// Path is Escaped, the %25 is not interpreted as %
|
||||
assert.Equal(t, "foo%252Fbar", c.Param("path"))
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/foo%252Fbar", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
func TestEngineHandleContextUseRawPathPercentEncoded(t *testing.T) {
|
||||
r := New()
|
||||
r.UseRawPath = true
|
||||
r.UnescapePathValues = false
|
||||
|
||||
r.GET("/v1/:path", func(c *Context) {
|
||||
// Path is used, the %25 is interpreted as %
|
||||
assert.Equal(t, "foo%2Fbar", c.Param("path"))
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/foo%252Fbar", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
func TestEngineHandleContextUseEscapedPathOverride(t *testing.T) {
|
||||
r := New()
|
||||
r.UseEscapedPath = true
|
||||
r.UseRawPath = true
|
||||
r.UnescapePathValues = false
|
||||
|
||||
r.GET("/v1/:path", func(c *Context) {
|
||||
assert.Equal(t, "foo%25bar", c.Param("path"))
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
w := PerformRequest(r, http.MethodGet, "/v1/foo%25bar")
|
||||
assert.Equal(t, 200, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPrepareTrustedCIRDsWith(t *testing.T) {
|
||||
r := New()
|
||||
|
||||
|
||||
21
go.mod
21
go.mod
@ -2,23 +2,28 @@ module github.com/gin-gonic/gin
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.7
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.14.2
|
||||
github.com/gin-contrib/sse v1.1.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/goccy/go-json v0.10.5
|
||||
github.com/goccy/go-yaml v1.19.1
|
||||
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.56.0
|
||||
github.com/quic-go/quic-go v0.57.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/ugorji/go/codec v1.3.1
|
||||
go.mongodb.org/mongo-driver v1.17.7
|
||||
golang.org/x/net v0.47.0
|
||||
google.golang.org/protobuf v1.36.10
|
||||
)
|
||||
|
||||
require gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
require (
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
||||
@ -30,13 +35,13 @@ require (
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kr/text v0.2.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/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/crypto v0.44.0 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
golang.org/x/arch v0.22.0 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
33
go.sum
33
go.sum
@ -22,10 +22,10 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||
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.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=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
|
||||
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@ -41,18 +41,19 @@ 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/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/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-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/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/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
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.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY=
|
||||
github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c=
|
||||
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/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
|
||||
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
||||
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@ -70,12 +71,14 @@ 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/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
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.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
go.mongodb.org/mongo-driver v1.17.7 h1:a9w+U3Vt67eYzcfq3k/OAv284/uUUkL0uP75VE5rCOU=
|
||||
go.mongodb.org/mongo-driver v1.17.7/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
||||
@ -30,7 +30,7 @@ func rawStrToBytes(s string) []byte {
|
||||
|
||||
func TestBytesToString(t *testing.T) {
|
||||
data := make([]byte, 1024)
|
||||
for i := 0; i < 100; i++ {
|
||||
for range 100 {
|
||||
_, err := cRand.Read(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@ -41,6 +41,15 @@ func TestBytesToString(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBytesToStringEmpty(t *testing.T) {
|
||||
if got := BytesToString([]byte{}); got != "" {
|
||||
t.Fatalf("BytesToString([]byte{}) = %q; want empty string", got)
|
||||
}
|
||||
if got := BytesToString(nil); got != "" {
|
||||
t.Fatalf("BytesToString(nil) = %q; want empty string", got)
|
||||
}
|
||||
}
|
||||
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
const (
|
||||
letterIdxBits = 6 // 6 bits to represent a letter index
|
||||
@ -70,7 +79,7 @@ func RandStringBytesMaskImprSrcSB(n int) string {
|
||||
}
|
||||
|
||||
func TestStringToBytes(t *testing.T) {
|
||||
for i := 0; i < 100; i++ {
|
||||
for range 100 {
|
||||
s := RandStringBytesMaskImprSrcSB(64)
|
||||
if !bytes.Equal(rawStrToBytes(s), StringToBytes(s)) {
|
||||
t.Fatal("don't match")
|
||||
@ -78,6 +87,16 @@ func TestStringToBytes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringToBytesEmpty(t *testing.T) {
|
||||
b := StringToBytes("")
|
||||
if len(b) != 0 {
|
||||
t.Fatalf(`StringToBytes("") length = %d; want 0`, len(b))
|
||||
}
|
||||
if !bytes.Equal(b, []byte("")) {
|
||||
t.Fatalf(`StringToBytes("") = %v; want []byte("")`, b)
|
||||
}
|
||||
}
|
||||
|
||||
// go test -v -run=none -bench=^BenchmarkBytesConv -benchmem=true
|
||||
|
||||
func BenchmarkBytesConvBytesToStrRaw(b *testing.B) {
|
||||
|
||||
55
path.go
55
path.go
@ -5,6 +5,8 @@
|
||||
|
||||
package gin
|
||||
|
||||
const stackBufSize = 128
|
||||
|
||||
// cleanPath is the URL version of path.Clean, it returns a canonical URL path
|
||||
// for p, eliminating . and .. elements.
|
||||
//
|
||||
@ -19,7 +21,6 @@ package gin
|
||||
//
|
||||
// If the result of this process is an empty string, "/" is returned.
|
||||
func cleanPath(p string) string {
|
||||
const stackBufSize = 128
|
||||
// Turn empty string into "/"
|
||||
if p == "" {
|
||||
return "/"
|
||||
@ -148,3 +149,55 @@ func bufApp(buf *[]byte, s string, w int, c byte) {
|
||||
}
|
||||
b[w] = c
|
||||
}
|
||||
|
||||
// removeRepeatedChar removes multiple consecutive 'char's from a string.
|
||||
// if s == "/a//b///c////" && char == '/', it returns "/a/b/c/"
|
||||
func removeRepeatedChar(s string, char byte) string {
|
||||
// Check if there are any consecutive chars
|
||||
hasRepeatedChar := false
|
||||
for i := 1; i < len(s); i++ {
|
||||
if s[i] == char && s[i-1] == char {
|
||||
hasRepeatedChar = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasRepeatedChar {
|
||||
return s
|
||||
}
|
||||
|
||||
// Reasonably sized buffer on stack to avoid allocations in the common case.
|
||||
buf := make([]byte, 0, stackBufSize)
|
||||
|
||||
// Invariants:
|
||||
// reading from s; r is index of next byte to process.
|
||||
// writing to buf; w is index of next byte to write.
|
||||
r := 0
|
||||
w := 0
|
||||
|
||||
for n := len(s); r < n; {
|
||||
if s[r] == char {
|
||||
// Write the first char
|
||||
bufApp(&buf, s, w, char)
|
||||
w++
|
||||
r++
|
||||
|
||||
// Skip all consecutive chars
|
||||
for r < n && s[r] == char {
|
||||
r++
|
||||
}
|
||||
} else {
|
||||
// Copy non-char character
|
||||
bufApp(&buf, s, w, s[r])
|
||||
w++
|
||||
r++
|
||||
}
|
||||
}
|
||||
|
||||
// If the original string was not modified (or only shortened at the end),
|
||||
// return the respective substring of the original string.
|
||||
// Otherwise, return a new string from the buffer.
|
||||
if len(buf) == 0 {
|
||||
return s[:w]
|
||||
}
|
||||
return string(buf[:w])
|
||||
}
|
||||
|
||||
47
path_test.go
47
path_test.go
@ -143,3 +143,50 @@ func BenchmarkPathCleanLong(b *testing.B) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveRepeatedChar(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
str string
|
||||
char byte
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
str: "",
|
||||
char: 'a',
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "noSlash",
|
||||
str: "abc",
|
||||
char: ',',
|
||||
want: "abc",
|
||||
},
|
||||
{
|
||||
name: "withSlash",
|
||||
str: "/a/b/c/",
|
||||
char: '/',
|
||||
want: "/a/b/c/",
|
||||
},
|
||||
{
|
||||
name: "withRepeatedSlashes",
|
||||
str: "/a//b///c////",
|
||||
char: '/',
|
||||
want: "/a/b/c/",
|
||||
},
|
||||
{
|
||||
name: "threeSlashes",
|
||||
str: "///",
|
||||
char: '/',
|
||||
want: "/",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
res := removeRepeatedChar(tc.str, tc.char)
|
||||
assert.Equal(t, tc.want, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
88
recovery.go
88
recovery.go
@ -5,25 +5,28 @@
|
||||
package gin
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"cmp"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||
)
|
||||
|
||||
const dunno = "???"
|
||||
|
||||
var dunnoBytes = []byte(dunno)
|
||||
const (
|
||||
dunno = "???"
|
||||
stackSkip = 3
|
||||
)
|
||||
|
||||
// RecoveryFunc defines the function passable to CustomRecovery.
|
||||
type RecoveryFunc func(c *Context, err any)
|
||||
@ -54,38 +57,33 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
|
||||
}
|
||||
return func(c *Context) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
if rec := recover(); rec != nil {
|
||||
// Check for a broken connection, as it is not really a
|
||||
// condition that warrants a panic stack trace.
|
||||
var brokenPipe bool
|
||||
if ne, ok := err.(*net.OpError); ok {
|
||||
var se *os.SyscallError
|
||||
if errors.As(ne, &se) {
|
||||
seStr := strings.ToLower(se.Error())
|
||||
if strings.Contains(seStr, "broken pipe") ||
|
||||
strings.Contains(seStr, "connection reset by peer") {
|
||||
brokenPipe = true
|
||||
}
|
||||
}
|
||||
var isBrokenPipe bool
|
||||
err, ok := rec.(error)
|
||||
if ok {
|
||||
isBrokenPipe = errors.Is(err, syscall.EPIPE) ||
|
||||
errors.Is(err, syscall.ECONNRESET) ||
|
||||
errors.Is(err, http.ErrAbortHandler)
|
||||
}
|
||||
if logger != nil {
|
||||
const stackSkip = 3
|
||||
if brokenPipe {
|
||||
logger.Printf("%s\n%s%s", err, secureRequestDump(c.Request), reset)
|
||||
if isBrokenPipe {
|
||||
logger.Printf("%s\n%s%s", rec, secureRequestDump(c.Request), reset)
|
||||
} else if IsDebugging() {
|
||||
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 {
|
||||
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.
|
||||
c.Error(err.(error)) //nolint: errcheck
|
||||
c.Error(err) //nolint: errcheck
|
||||
c.Abort()
|
||||
} else {
|
||||
handle(c, err)
|
||||
handle(c, rec)
|
||||
}
|
||||
}
|
||||
}()
|
||||
@ -117,8 +115,11 @@ func stack(skip int) []byte {
|
||||
buf := new(bytes.Buffer) // the returned data
|
||||
// As we loop, we open files and read them. These variables record the currently
|
||||
// loaded file.
|
||||
var lines [][]byte
|
||||
var lastFile string
|
||||
var (
|
||||
nLine string
|
||||
lastFile string
|
||||
err error
|
||||
)
|
||||
for i := skip; ; i++ { // Skip the expected number of frames
|
||||
pc, file, line, ok := runtime.Caller(i)
|
||||
if !ok {
|
||||
@ -127,25 +128,44 @@ func stack(skip int) []byte {
|
||||
// Print this much at least. If we can't find the source, it won't show.
|
||||
fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
|
||||
if file != lastFile {
|
||||
data, err := os.ReadFile(file)
|
||||
nLine, err = readNthLine(file, line-1)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
lines = bytes.Split(data, []byte{'\n'})
|
||||
lastFile = file
|
||||
}
|
||||
fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
|
||||
fmt.Fprintf(buf, "\t%s: %s\n", function(pc), cmp.Or(nLine, dunno))
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// source returns a space-trimmed slice of the n'th line.
|
||||
func source(lines [][]byte, n int) []byte {
|
||||
n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
|
||||
if n < 0 || n >= len(lines) {
|
||||
return dunnoBytes
|
||||
// readNthLine reads the nth line from the file.
|
||||
// It returns the trimmed content of the line if found,
|
||||
// or an empty string if the line doesn't exist.
|
||||
// If there's an error opening the file, it returns the error.
|
||||
func readNthLine(file string, n int) (string, error) {
|
||||
if n < 0 {
|
||||
return "", nil
|
||||
}
|
||||
return bytes.TrimSpace(lines[n])
|
||||
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for i := 0; i < n; i++ {
|
||||
if !scanner.Scan() {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
if scanner.Scan() {
|
||||
return strings.TrimSpace(scanner.Text()), nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// function returns, if possible, the name of the function containing the PC.
|
||||
|
||||
102
recovery_test.go
102
recovery_test.go
@ -88,21 +88,6 @@ func TestPanicWithAbort(t *testing.T) {
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func TestSource(t *testing.T) {
|
||||
bs := source(nil, 0)
|
||||
assert.Equal(t, dunnoBytes, bs)
|
||||
|
||||
in := [][]byte{
|
||||
[]byte("Hello world."),
|
||||
[]byte("Hi, gin.."),
|
||||
}
|
||||
bs = source(in, 10)
|
||||
assert.Equal(t, dunnoBytes, bs)
|
||||
|
||||
bs = source(in, 1)
|
||||
assert.Equal(t, []byte("Hello world."), bs)
|
||||
}
|
||||
|
||||
func TestFunction(t *testing.T) {
|
||||
bs := function(1)
|
||||
assert.Equal(t, dunno, bs)
|
||||
@ -113,13 +98,13 @@ func TestFunction(t *testing.T) {
|
||||
func TestPanicWithBrokenPipe(t *testing.T) {
|
||||
const expectCode = 204
|
||||
|
||||
expectMsgs := map[syscall.Errno]string{
|
||||
syscall.EPIPE: "broken pipe",
|
||||
syscall.ECONNRESET: "connection reset by peer",
|
||||
expectErrnos := []syscall.Errno{
|
||||
syscall.EPIPE,
|
||||
syscall.ECONNRESET,
|
||||
}
|
||||
|
||||
for errno, expectMsg := range expectMsgs {
|
||||
t.Run(expectMsg, func(t *testing.T) {
|
||||
for _, errno := range expectErrnos {
|
||||
t.Run("Recovery from "+errno.Error(), func(t *testing.T) {
|
||||
var buf strings.Builder
|
||||
|
||||
router := New()
|
||||
@ -137,11 +122,36 @@ func TestPanicWithBrokenPipe(t *testing.T) {
|
||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||
// TEST
|
||||
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]")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPanicWithAbortHandler asserts that recovery handles http.ErrAbortHandler as broken pipe
|
||||
func TestPanicWithAbortHandler(t *testing.T) {
|
||||
const expectCode = 204
|
||||
|
||||
var buf strings.Builder
|
||||
router := New()
|
||||
router.Use(RecoveryWithWriter(&buf))
|
||||
router.GET("/recovery", func(c *Context) {
|
||||
// Start writing response
|
||||
c.Header("X-Test", "Value")
|
||||
c.Status(expectCode)
|
||||
|
||||
// Panic with ErrAbortHandler which should be treated as broken pipe
|
||||
panic(http.ErrAbortHandler)
|
||||
})
|
||||
// RUN
|
||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||
// TEST
|
||||
assert.Equal(t, expectCode, w.Code)
|
||||
out := buf.String()
|
||||
assert.Contains(t, out, "net/http: abort Handler")
|
||||
assert.NotContains(t, out, "panic recovered")
|
||||
}
|
||||
|
||||
func TestCustomRecoveryWithWriter(t *testing.T) {
|
||||
errBuffer := new(strings.Builder)
|
||||
buffer := new(strings.Builder)
|
||||
@ -307,3 +317,53 @@ func TestSecureRequestDump(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestReadNthLine tests the readNthLine function with various scenarios.
|
||||
func TestReadNthLine(t *testing.T) {
|
||||
// Create a temporary test file
|
||||
testContent := "line 0 \n line 1 \nline 2 \nline 3 \nline 4"
|
||||
tempFile, err := os.CreateTemp("", "testfile*.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
|
||||
// Write test content to the temporary file
|
||||
if _, err := tempFile.WriteString(testContent); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := tempFile.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Test cases
|
||||
tests := []struct {
|
||||
name string
|
||||
lineNum int
|
||||
fileName string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "Read first line", lineNum: 0, fileName: tempFile.Name(), want: "line 0", wantErr: false},
|
||||
{name: "Read middle line", lineNum: 2, fileName: tempFile.Name(), want: "line 2", wantErr: false},
|
||||
{name: "Read last line", lineNum: 4, fileName: tempFile.Name(), want: "line 4", wantErr: false},
|
||||
{name: "Line number exceeds file length", lineNum: 10, fileName: tempFile.Name(), want: "", wantErr: false},
|
||||
{name: "Negative line number", lineNum: -1, fileName: tempFile.Name(), want: "", wantErr: false},
|
||||
{name: "Non-existent file", lineNum: 1, fileName: "/non/existent/file.txt", want: "", wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := readNthLine(tt.fileName, tt.lineNum)
|
||||
assert.Equal(t, tt.wantErr, err != nil)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkStack(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = stack(stackSkip)
|
||||
}
|
||||
}
|
||||
|
||||
34
render/bson.go
Normal file
34
render/bson.go
Normal 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/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)
|
||||
}
|
||||
@ -19,6 +19,7 @@ import (
|
||||
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
@ -359,6 +360,31 @@ func TestRenderProtoBufFail(t *testing.T) {
|
||||
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) {
|
||||
w := httptest.NewRecorder()
|
||||
data := xmlmap{
|
||||
|
||||
@ -128,7 +128,9 @@ func (w *responseWriter) CloseNotify() <-chan bool {
|
||||
// Flush implements the http.Flusher interface.
|
||||
func (w *responseWriter) Flush() {
|
||||
w.WriteHeaderNow()
|
||||
w.ResponseWriter.(http.Flusher).Flush()
|
||||
if f, ok := w.ResponseWriter.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *responseWriter) Pusher() (pusher http.Pusher) {
|
||||
|
||||
@ -4,7 +4,11 @@
|
||||
|
||||
package gin
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CreateTestContext returns a fresh Engine and a Context associated with it.
|
||||
// This is useful for tests that need to set up a new Gin engine instance
|
||||
@ -29,3 +33,28 @@ func CreateTestContextOnly(w http.ResponseWriter, r *Engine) (c *Context) {
|
||||
c.writermem.reset(w)
|
||||
return
|
||||
}
|
||||
|
||||
// waitForServerReady waits for a server to be ready by making HTTP requests
|
||||
// with exponential backoff. This is more reliable than time.Sleep() for testing.
|
||||
func waitForServerReady(url string, maxAttempts int) error {
|
||||
client := &http.Client{
|
||||
Timeout: 100 * time.Millisecond,
|
||||
}
|
||||
|
||||
for i := 0; i < maxAttempts; i++ {
|
||||
resp, err := client.Get(url)
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Exponential backoff: 10ms, 20ms, 40ms, 80ms, 160ms...
|
||||
backoff := time.Duration(10*(1<<uint(i))) * time.Millisecond
|
||||
if backoff > 500*time.Millisecond {
|
||||
backoff = 500 * time.Millisecond
|
||||
}
|
||||
time.Sleep(backoff)
|
||||
}
|
||||
|
||||
return fmt.Errorf("server at %s did not become ready after %d attempts", url, maxAttempts)
|
||||
}
|
||||
|
||||
16
tree.go
16
tree.go
@ -5,7 +5,6 @@
|
||||
package gin
|
||||
|
||||
import (
|
||||
"math"
|
||||
"net/url"
|
||||
"strings"
|
||||
"unicode"
|
||||
@ -78,14 +77,6 @@ func (n *node) addChild(child *node) {
|
||||
}
|
||||
}
|
||||
|
||||
// safeUint16 converts int to uint16 safely, capping at math.MaxUint16
|
||||
func safeUint16(n int) uint16 {
|
||||
if n > math.MaxUint16 {
|
||||
return math.MaxUint16
|
||||
}
|
||||
return uint16(n)
|
||||
}
|
||||
|
||||
func countParams(path string) uint16 {
|
||||
colons := strings.Count(path, ":")
|
||||
stars := strings.Count(path, "*")
|
||||
@ -680,12 +671,7 @@ walk: // Outer loop for walking the tree
|
||||
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]byte, bool) {
|
||||
const stackBufSize = 128
|
||||
|
||||
// Use a static sized buffer on the stack in the common case.
|
||||
// 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)
|
||||
}
|
||||
buf := make([]byte, 0, max(stackBufSize, len(path)+1))
|
||||
|
||||
ciPath := n.findCaseInsensitivePathRec(
|
||||
path,
|
||||
|
||||
25
utils.go
25
utils.go
@ -6,6 +6,7 @@ package gin
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
@ -18,6 +19,12 @@ import (
|
||||
// BindKey indicates a default bind key.
|
||||
const BindKey = "_gin-gonic/gin/bindkey"
|
||||
|
||||
// localhostIP indicates the default localhost IP address.
|
||||
const localhostIP = "127.0.0.1"
|
||||
|
||||
// localhostIPv6 indicates the default localhost IPv6 address.
|
||||
const localhostIPv6 = "::1"
|
||||
|
||||
// Bind is a helper function for given interface object and returns a Gin middleware.
|
||||
func Bind(val any) HandlerFunc {
|
||||
value := reflect.ValueOf(val)
|
||||
@ -155,10 +162,26 @@ func resolveAddress(addr []string) string {
|
||||
|
||||
// https://stackoverflow.com/questions/53069040/checking-a-string-contains-only-ascii-characters
|
||||
func isASCII(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
for i := range len(s) {
|
||||
if s[i] > unicode.MaxASCII {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// safeInt8 converts int to int8 safely, capping at math.MaxInt8
|
||||
func safeInt8(n int) int8 {
|
||||
if n > math.MaxInt8 {
|
||||
return math.MaxInt8
|
||||
}
|
||||
return int8(n)
|
||||
}
|
||||
|
||||
// safeUint16 converts int to uint16 safely, capping at math.MaxUint16
|
||||
func safeUint16(n int) uint16 {
|
||||
if n > math.MaxUint16 {
|
||||
return math.MaxUint16
|
||||
}
|
||||
return uint16(n)
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
@ -148,3 +149,13 @@ func TestIsASCII(t *testing.T) {
|
||||
assert.True(t, isASCII("test"))
|
||||
assert.False(t, isASCII("🧡💛💚💙💜"))
|
||||
}
|
||||
|
||||
func TestSafeInt8(t *testing.T) {
|
||||
assert.Equal(t, int8(100), safeInt8(100))
|
||||
assert.Equal(t, int8(math.MaxInt8), safeInt8(int(math.MaxInt8)+123))
|
||||
}
|
||||
|
||||
func TestSafeUint16(t *testing.T) {
|
||||
assert.Equal(t, uint16(100), safeUint16(100))
|
||||
assert.Equal(t, uint16(math.MaxUint16), safeUint16(int(math.MaxUint16)+123))
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user