mirror of
https://github.com/gin-gonic/gin.git
synced 2025-10-14 04:08:15 +08:00
Merge branch 'master' of github.com:gin-gonic/gin into support-go1.25
This commit is contained in:
commit
e6de1831f9
4
.github/workflows/gin.yml
vendored
4
.github/workflows/gin.yml
vendored
@ -20,7 +20,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1"
|
||||
- name: Setup golangci-lint
|
||||
@ -55,7 +55,7 @@ jobs:
|
||||
GOPROXY: https://proxy.golang.org
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go }}
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
cache: false
|
||||
|
7
.github/workflows/goreleaser.yml
vendored
7
.github/workflows/goreleaser.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1"
|
||||
- name: Run GoReleaser
|
||||
@ -29,3 +29,8 @@ jobs:
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Trigger Go module reindex (pkg.go.dev)
|
||||
run: |
|
||||
echo "Triggering Go module reindex at proxy.golang.org"
|
||||
curl -sSf "https://proxy.golang.org/github.com/${GITHUB_REPOSITORY,,}/@v/${GITHUB_REF_NAME}.info"
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
<img align="right" width="159px" src="https://raw.githubusercontent.com/gin-gonic/logo/master/color.png">
|
||||
|
||||
[](https://github.com/gin-gonic/gin/actions?query=branch%3Amaster)
|
||||
[](https://github.com/gin-gonic/gin/actions/workflows/gin.yml)
|
||||
[](https://codecov.io/gh/gin-gonic/gin)
|
||||
[](https://goreportcard.com/report/github.com/gin-gonic/gin)
|
||||
[](https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc)
|
||||
@ -104,6 +104,7 @@ The documentation is also available on [gin-gonic.com](https://gin-gonic.com) in
|
||||
- [Persian](https://gin-gonic.com/fa/docs/)
|
||||
- [Português](https://gin-gonic.com/pt/docs/)
|
||||
- [Russian](https://gin-gonic.com/ru/docs/)
|
||||
- [Indonesian](https://gin-gonic.com/id/docs/)
|
||||
|
||||
### Articles
|
||||
|
||||
|
@ -13,8 +13,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -175,7 +175,7 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
|
||||
|
||||
// BindUnmarshaler is the interface used to wrap the UnmarshalParam method.
|
||||
type BindUnmarshaler interface {
|
||||
// UnmarshalParam decodes and assigns a value from an form or query param.
|
||||
// UnmarshalParam decodes and assigns a value from a form or query param.
|
||||
UnmarshalParam(param string) error
|
||||
}
|
||||
|
||||
@ -333,9 +333,9 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
|
||||
case multipart.FileHeader:
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
|
||||
return json.API.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
|
||||
case reflect.Map:
|
||||
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
|
||||
return json.API.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
|
||||
case reflect.Ptr:
|
||||
if !value.Elem().IsValid() {
|
||||
value.Set(reflect.New(value.Type().Elem()))
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
)
|
||||
|
||||
// EnableDecoderUseNumber is used to call the UseNumber method on the JSON
|
||||
@ -42,7 +42,7 @@ func (jsonBinding) BindBody(body []byte, obj any) error {
|
||||
}
|
||||
|
||||
func decodeJSON(r io.Reader, obj any) error {
|
||||
decoder := json.NewDecoder(r)
|
||||
decoder := json.API.NewDecoder(r)
|
||||
if EnableDecoderUseNumber {
|
||||
decoder.UseNumber()
|
||||
}
|
||||
|
@ -5,8 +5,16 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
"github.com/gin-gonic/gin/render"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/modern-go/reflect2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -28,3 +36,181 @@ func TestJSONBindingBindBodyMap(t *testing.T) {
|
||||
assert.Equal(t, "FOO", s["foo"])
|
||||
assert.Equal(t, "world", s["hello"])
|
||||
}
|
||||
|
||||
func TestCustomJsonCodec(t *testing.T) {
|
||||
// Restore json encoding configuration after testing
|
||||
oldMarshal := json.API
|
||||
defer func() {
|
||||
json.API = oldMarshal
|
||||
}()
|
||||
// Custom json api
|
||||
json.API = customJsonApi{}
|
||||
|
||||
// test decode json
|
||||
obj := customReq{}
|
||||
err := jsonBinding{}.BindBody([]byte(`{"time_empty":null,"time_struct": "2001-12-05 10:01:02.345","time_nil":null,"time_pointer":"2002-12-05 10:01:02.345"}`), &obj)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, zeroTime, obj.TimeEmpty)
|
||||
assert.Equal(t, time.Date(2001, 12, 5, 10, 1, 2, 345000000, time.Local), obj.TimeStruct)
|
||||
assert.Nil(t, obj.TimeNil)
|
||||
assert.Equal(t, time.Date(2002, 12, 5, 10, 1, 2, 345000000, time.Local), *obj.TimePointer)
|
||||
// test encode json
|
||||
w := httptest.NewRecorder()
|
||||
err2 := (render.PureJSON{Data: obj}).Render(w)
|
||||
require.NoError(t, err2)
|
||||
assert.JSONEq(t, "{\"time_empty\":null,\"time_struct\":\"2001-12-05 10:01:02.345\",\"time_nil\":null,\"time_pointer\":\"2002-12-05 10:01:02.345\"}\n", w.Body.String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
type customReq struct {
|
||||
TimeEmpty time.Time `json:"time_empty"`
|
||||
TimeStruct time.Time `json:"time_struct"`
|
||||
TimeNil *time.Time `json:"time_nil"`
|
||||
TimePointer *time.Time `json:"time_pointer"`
|
||||
}
|
||||
|
||||
var customConfig = jsoniter.Config{
|
||||
EscapeHTML: true,
|
||||
SortMapKeys: true,
|
||||
ValidateJsonRawMessage: true,
|
||||
}.Froze()
|
||||
|
||||
func init() {
|
||||
customConfig.RegisterExtension(&TimeEx{})
|
||||
customConfig.RegisterExtension(&TimePointerEx{})
|
||||
}
|
||||
|
||||
type customJsonApi struct{}
|
||||
|
||||
func (j customJsonApi) Marshal(v any) ([]byte, error) {
|
||||
return customConfig.Marshal(v)
|
||||
}
|
||||
|
||||
func (j customJsonApi) Unmarshal(data []byte, v any) error {
|
||||
return customConfig.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
func (j customJsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
|
||||
return customConfig.MarshalIndent(v, prefix, indent)
|
||||
}
|
||||
|
||||
func (j customJsonApi) NewEncoder(writer io.Writer) json.Encoder {
|
||||
return customConfig.NewEncoder(writer)
|
||||
}
|
||||
|
||||
func (j customJsonApi) NewDecoder(reader io.Reader) json.Decoder {
|
||||
return customConfig.NewDecoder(reader)
|
||||
}
|
||||
|
||||
// region Time Extension
|
||||
|
||||
var (
|
||||
zeroTime = time.Time{}
|
||||
timeType = reflect2.TypeOfPtr((*time.Time)(nil)).Elem()
|
||||
defaultTimeCodec = &timeCodec{}
|
||||
)
|
||||
|
||||
type TimeEx struct {
|
||||
jsoniter.DummyExtension
|
||||
}
|
||||
|
||||
func (te *TimeEx) CreateDecoder(typ reflect2.Type) jsoniter.ValDecoder {
|
||||
if typ == timeType {
|
||||
return defaultTimeCodec
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (te *TimeEx) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder {
|
||||
if typ == timeType {
|
||||
return defaultTimeCodec
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type timeCodec struct{}
|
||||
|
||||
func (tc timeCodec) IsEmpty(ptr unsafe.Pointer) bool {
|
||||
t := *((*time.Time)(ptr))
|
||||
return t.Equal(zeroTime)
|
||||
}
|
||||
|
||||
func (tc timeCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
|
||||
t := *((*time.Time)(ptr))
|
||||
if t.Equal(zeroTime) {
|
||||
stream.WriteNil()
|
||||
return
|
||||
}
|
||||
stream.WriteString(t.In(time.Local).Format("2006-01-02 15:04:05.000"))
|
||||
}
|
||||
|
||||
func (tc timeCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
|
||||
ts := iter.ReadString()
|
||||
if len(ts) == 0 {
|
||||
*((*time.Time)(ptr)) = zeroTime
|
||||
return
|
||||
}
|
||||
t, err := time.ParseInLocation("2006-01-02 15:04:05.000", ts, time.Local)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
*((*time.Time)(ptr)) = t
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region *Time Extension
|
||||
|
||||
var (
|
||||
timePointerType = reflect2.TypeOfPtr((**time.Time)(nil)).Elem()
|
||||
defaultTimePointerCodec = &timePointerCodec{}
|
||||
)
|
||||
|
||||
type TimePointerEx struct {
|
||||
jsoniter.DummyExtension
|
||||
}
|
||||
|
||||
func (tpe *TimePointerEx) CreateDecoder(typ reflect2.Type) jsoniter.ValDecoder {
|
||||
if typ == timePointerType {
|
||||
return defaultTimePointerCodec
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tpe *TimePointerEx) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder {
|
||||
if typ == timePointerType {
|
||||
return defaultTimePointerCodec
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type timePointerCodec struct{}
|
||||
|
||||
func (tpc timePointerCodec) IsEmpty(ptr unsafe.Pointer) bool {
|
||||
t := *((**time.Time)(ptr))
|
||||
return t == nil || (*t).Equal(zeroTime)
|
||||
}
|
||||
|
||||
func (tpc timePointerCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
|
||||
t := *((**time.Time)(ptr))
|
||||
if t == nil || (*t).Equal(zeroTime) {
|
||||
stream.WriteNil()
|
||||
return
|
||||
}
|
||||
stream.WriteString(t.In(time.Local).Format("2006-01-02 15:04:05.000"))
|
||||
}
|
||||
|
||||
func (tpc timePointerCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
|
||||
ts := iter.ReadString()
|
||||
if len(ts) == 0 {
|
||||
*((**time.Time)(ptr)) = nil
|
||||
return
|
||||
}
|
||||
t, err := time.ParseInLocation("2006-01-02 15:04:05.000", ts, time.Local)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
*((**time.Time)(ptr)) = &t
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"github.com/goccy/go-yaml"
|
||||
)
|
||||
|
||||
type yamlBinding struct{}
|
||||
|
57
codec/json/api.go
Normal file
57
codec/json/api.go
Normal file
@ -0,0 +1,57 @@
|
||||
// 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 json
|
||||
|
||||
import "io"
|
||||
|
||||
// API the json codec in use.
|
||||
var API Core
|
||||
|
||||
// Core the api for json codec.
|
||||
type Core interface {
|
||||
Marshal(v any) ([]byte, error)
|
||||
Unmarshal(data []byte, v any) error
|
||||
MarshalIndent(v any, prefix, indent string) ([]byte, error)
|
||||
NewEncoder(writer io.Writer) Encoder
|
||||
NewDecoder(reader io.Reader) Decoder
|
||||
}
|
||||
|
||||
// Encoder an interface writes JSON values to an output stream.
|
||||
type Encoder interface {
|
||||
// SetEscapeHTML specifies whether problematic HTML characters
|
||||
// should be escaped inside JSON quoted strings.
|
||||
// The default behavior is to escape &, <, and > to \u0026, \u003c, and \u003e
|
||||
// to avoid certain safety problems that can arise when embedding JSON in HTML.
|
||||
//
|
||||
// In non-HTML settings where the escaping interferes with the readability
|
||||
// of the output, SetEscapeHTML(false) disables this behavior.
|
||||
SetEscapeHTML(on bool)
|
||||
|
||||
// Encode writes the JSON encoding of v to the stream,
|
||||
// followed by a newline character.
|
||||
//
|
||||
// See the documentation for Marshal for details about the
|
||||
// conversion of Go values to JSON.
|
||||
Encode(v any) error
|
||||
}
|
||||
|
||||
// Decoder an interface reads and decodes JSON values from an input stream.
|
||||
type Decoder interface {
|
||||
// UseNumber causes the Decoder to unmarshal a number into an any as a
|
||||
// Number instead of as a float64.
|
||||
UseNumber()
|
||||
|
||||
// DisallowUnknownFields causes the Decoder to return an error when the destination
|
||||
// is a struct and the input contains object keys which do not match any
|
||||
// non-ignored, exported fields in the destination.
|
||||
DisallowUnknownFields()
|
||||
|
||||
// Decode reads the next JSON-encoded value from its
|
||||
// input and stores it in the value pointed to by v.
|
||||
//
|
||||
// See the documentation for Unmarshal for details about
|
||||
// the conversion of JSON into a Go value.
|
||||
Decode(v any) error
|
||||
}
|
42
codec/json/go_json.go
Normal file
42
codec/json/go_json.go
Normal file
@ -0,0 +1,42 @@
|
||||
// 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.
|
||||
|
||||
//go:build go_json
|
||||
|
||||
package json
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
// Package indicates what library is being used for JSON encoding.
|
||||
const Package = "github.com/goccy/go-json"
|
||||
|
||||
func init() {
|
||||
API = gojsonApi{}
|
||||
}
|
||||
|
||||
type gojsonApi struct{}
|
||||
|
||||
func (j gojsonApi) Marshal(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func (j gojsonApi) Unmarshal(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
func (j gojsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
|
||||
return json.MarshalIndent(v, prefix, indent)
|
||||
}
|
||||
|
||||
func (j gojsonApi) NewEncoder(writer io.Writer) Encoder {
|
||||
return json.NewEncoder(writer)
|
||||
}
|
||||
|
||||
func (j gojsonApi) NewDecoder(reader io.Reader) Decoder {
|
||||
return json.NewDecoder(reader)
|
||||
}
|
41
codec/json/json.go
Normal file
41
codec/json/json.go
Normal file
@ -0,0 +1,41 @@
|
||||
// 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.
|
||||
|
||||
//go:build !jsoniter && !go_json && !(sonic && (linux || windows || darwin))
|
||||
|
||||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Package indicates what library is being used for JSON encoding.
|
||||
const Package = "encoding/json"
|
||||
|
||||
func init() {
|
||||
API = jsonApi{}
|
||||
}
|
||||
|
||||
type jsonApi struct{}
|
||||
|
||||
func (j jsonApi) Marshal(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func (j jsonApi) Unmarshal(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
func (j jsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
|
||||
return json.MarshalIndent(v, prefix, indent)
|
||||
}
|
||||
|
||||
func (j jsonApi) NewEncoder(writer io.Writer) Encoder {
|
||||
return json.NewEncoder(writer)
|
||||
}
|
||||
|
||||
func (j jsonApi) NewDecoder(reader io.Reader) Decoder {
|
||||
return json.NewDecoder(reader)
|
||||
}
|
44
codec/json/jsoniter.go
Normal file
44
codec/json/jsoniter.go
Normal file
@ -0,0 +1,44 @@
|
||||
// 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.
|
||||
|
||||
//go:build jsoniter
|
||||
|
||||
package json
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// Package indicates what library is being used for JSON encoding.
|
||||
const Package = "github.com/json-iterator/go"
|
||||
|
||||
func init() {
|
||||
API = jsoniterApi{}
|
||||
}
|
||||
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
|
||||
type jsoniterApi struct{}
|
||||
|
||||
func (j jsoniterApi) Marshal(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func (j jsoniterApi) Unmarshal(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
func (j jsoniterApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
|
||||
return json.MarshalIndent(v, prefix, indent)
|
||||
}
|
||||
|
||||
func (j jsoniterApi) NewEncoder(writer io.Writer) Encoder {
|
||||
return json.NewEncoder(writer)
|
||||
}
|
||||
|
||||
func (j jsoniterApi) NewDecoder(reader io.Reader) Decoder {
|
||||
return json.NewDecoder(reader)
|
||||
}
|
44
codec/json/sonic.go
Normal file
44
codec/json/sonic.go
Normal file
@ -0,0 +1,44 @@
|
||||
// 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.
|
||||
|
||||
//go:build sonic && (linux || windows || darwin)
|
||||
|
||||
package json
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
// Package indicates what library is being used for JSON encoding.
|
||||
const Package = "github.com/bytedance/sonic"
|
||||
|
||||
func init() {
|
||||
API = sonicApi{}
|
||||
}
|
||||
|
||||
var json = sonic.ConfigStd
|
||||
|
||||
type sonicApi struct{}
|
||||
|
||||
func (j sonicApi) Marshal(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func (j sonicApi) Unmarshal(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
func (j sonicApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
|
||||
return json.MarshalIndent(v, prefix, indent)
|
||||
}
|
||||
|
||||
func (j sonicApi) NewEncoder(writer io.Writer) Encoder {
|
||||
return json.NewEncoder(writer)
|
||||
}
|
||||
|
||||
func (j sonicApi) NewDecoder(reader io.Reader) Decoder {
|
||||
return json.NewDecoder(reader)
|
||||
}
|
42
context.go
42
context.go
@ -216,6 +216,14 @@ func (c *Context) AbortWithStatus(code int) {
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
// AbortWithStatusPureJSON calls `Abort()` and then `PureJSON` internally.
|
||||
// This method stops the chain, writes the status code and return a JSON body without escaping.
|
||||
// It also sets the Content-Type as "application/json".
|
||||
func (c *Context) AbortWithStatusPureJSON(code int, jsonObj any) {
|
||||
c.Abort()
|
||||
c.PureJSON(code, jsonObj)
|
||||
}
|
||||
|
||||
// AbortWithStatusJSON calls `Abort()` and then `JSON` internally.
|
||||
// This method stops the chain, writes the status code and return a JSON body.
|
||||
// It also sets the Content-Type as "application/json".
|
||||
@ -565,7 +573,7 @@ func (c *Context) QueryMap(key string) (dicts map[string]string) {
|
||||
// whether at least one value exists for the given key.
|
||||
func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
|
||||
c.initQueryCache()
|
||||
return c.get(c.queryCache, key)
|
||||
return getMapFromFormData(c.queryCache, key)
|
||||
}
|
||||
|
||||
// PostForm returns the specified key from a POST urlencoded form or multipart form
|
||||
@ -638,22 +646,32 @@ func (c *Context) PostFormMap(key string) (dicts map[string]string) {
|
||||
// whether at least one value exists for the given key.
|
||||
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
|
||||
c.initFormCache()
|
||||
return c.get(c.formCache, key)
|
||||
return getMapFromFormData(c.formCache, key)
|
||||
}
|
||||
|
||||
// get is an internal method and returns a map which satisfies conditions.
|
||||
func (c *Context) get(m map[string][]string, key string) (map[string]string, bool) {
|
||||
dicts := make(map[string]string)
|
||||
exist := false
|
||||
// getMapFromFormData return a map which satisfies conditions.
|
||||
// It parses from data with bracket notation like "key[subkey]=value" into a map.
|
||||
func getMapFromFormData(m map[string][]string, key string) (map[string]string, bool) {
|
||||
d := make(map[string]string)
|
||||
found := false
|
||||
keyLen := len(key)
|
||||
|
||||
for k, v := range m {
|
||||
if i := strings.IndexByte(k, '['); i >= 1 && k[0:i] == key {
|
||||
if j := strings.IndexByte(k[i+1:], ']'); j >= 1 {
|
||||
exist = true
|
||||
dicts[k[i+1:][:j]] = v[0]
|
||||
}
|
||||
if len(k) < keyLen+3 { // key + "[" + at least one char + "]"
|
||||
continue
|
||||
}
|
||||
|
||||
if k[:keyLen] != key || k[keyLen] != '[' {
|
||||
continue
|
||||
}
|
||||
|
||||
if j := strings.IndexByte(k[keyLen+1:], ']'); j > 0 {
|
||||
found = true
|
||||
d[k[keyLen+1:keyLen+1+j]] = v[0]
|
||||
}
|
||||
}
|
||||
return dicts, exist
|
||||
|
||||
return d, found
|
||||
}
|
||||
|
||||
// FormFile returns the first file for the provided form key.
|
||||
|
35
context_file_test.go
Normal file
35
context_file_test.go
Normal file
@ -0,0 +1,35 @@
|
||||
package gin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestContextFileSimple tests the Context.File() method with a simple case
|
||||
func TestContextFileSimple(t *testing.T) {
|
||||
// Test serving an existing file
|
||||
testFile := "testdata/test_file.txt"
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
|
||||
c.File(testFile)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "This is a test file")
|
||||
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
// TestContextFileNotFound tests serving a non-existent file
|
||||
func TestContextFileNotFound(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
|
||||
c.File("non_existent_file.txt")
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
}
|
322
context_test.go
322
context_test.go
@ -28,7 +28,7 @@ import (
|
||||
|
||||
"github.com/gin-contrib/sse"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -75,6 +75,79 @@ func must(err error) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestContextFile tests the Context.File() method
|
||||
func TestContextFile(t *testing.T) {
|
||||
// Test serving an existing file
|
||||
t.Run("serve existing file", func(t *testing.T) {
|
||||
// Create a temporary test file
|
||||
testFile := "testdata/test_file.txt"
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
|
||||
c.File(testFile)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "This is a test file")
|
||||
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
})
|
||||
|
||||
// Test serving a non-existent file
|
||||
t.Run("serve non-existent file", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
|
||||
c.File("non_existent_file.txt")
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
})
|
||||
|
||||
// Test serving a directory (should return 200 with directory listing or 403 Forbidden)
|
||||
t.Run("serve directory", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
|
||||
c.File(".")
|
||||
|
||||
// Directory serving can return either 200 (with listing) or 403 (forbidden)
|
||||
assert.True(t, w.Code == http.StatusOK || w.Code == http.StatusForbidden)
|
||||
})
|
||||
|
||||
// Test with HEAD request
|
||||
t.Run("HEAD request", func(t *testing.T) {
|
||||
testFile := "testdata/test_file.txt"
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodHead, "/test", nil)
|
||||
|
||||
c.File(testFile)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Empty(t, w.Body.String()) // HEAD request should not return body
|
||||
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
})
|
||||
|
||||
// Test with Range request
|
||||
t.Run("Range request", func(t *testing.T) {
|
||||
testFile := "testdata/test_file.txt"
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
c.Request.Header.Set("Range", "bytes=0-10")
|
||||
|
||||
c.File(testFile)
|
||||
|
||||
assert.Equal(t, http.StatusPartialContent, w.Code)
|
||||
assert.Equal(t, "bytes", w.Header().Get("Accept-Ranges"))
|
||||
assert.Contains(t, w.Header().Get("Content-Range"), "bytes 0-10")
|
||||
})
|
||||
}
|
||||
|
||||
func TestContextFormFile(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
mw := multipart.NewWriter(buf)
|
||||
@ -1680,6 +1753,32 @@ func TestContextAbortWithStatusJSON(t *testing.T) {
|
||||
assert.JSONEq(t, "{\"foo\":\"fooValue\",\"bar\":\"barValue\"}", jsonStringBody)
|
||||
}
|
||||
|
||||
func TestContextAbortWithStatusPureJSON(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
c.index = 4
|
||||
|
||||
in := new(testJSONAbortMsg)
|
||||
in.Bar = "barValue"
|
||||
in.Foo = "fooValue"
|
||||
|
||||
c.AbortWithStatusPureJSON(http.StatusUnsupportedMediaType, in)
|
||||
|
||||
assert.Equal(t, abortIndex, c.index)
|
||||
assert.Equal(t, http.StatusUnsupportedMediaType, c.Writer.Status())
|
||||
assert.Equal(t, http.StatusUnsupportedMediaType, w.Code)
|
||||
assert.True(t, c.IsAborted())
|
||||
|
||||
contentType := w.Header().Get("Content-Type")
|
||||
assert.Equal(t, "application/json; charset=utf-8", contentType)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
_, err := buf.ReadFrom(w.Body)
|
||||
require.NoError(t, err)
|
||||
jsonStringBody := buf.String()
|
||||
assert.JSONEq(t, "{\"foo\":\"fooValue\",\"bar\":\"barValue\"}", jsonStringBody)
|
||||
}
|
||||
|
||||
func TestContextError(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
assert.Empty(t, c.Errors)
|
||||
@ -1892,13 +1991,12 @@ func TestContextContentType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestContextBindRequestTooLarge(t *testing.T) {
|
||||
// When using sonic or go-json as JSON encoder, they do not propagate the http.MaxBytesError error
|
||||
// When using go-json as JSON encoder, they do not propagate the http.MaxBytesError error
|
||||
// The response will fail with a generic 400 instead of 413
|
||||
// https://github.com/goccy/go-json/issues/485
|
||||
// https://github.com/bytedance/sonic/issues/800
|
||||
var expectedCode int
|
||||
switch json.Package {
|
||||
case "github.com/goccy/go-json", "github.com/bytedance/sonic":
|
||||
case "github.com/goccy/go-json":
|
||||
expectedCode = http.StatusBadRequest
|
||||
default:
|
||||
expectedCode = http.StatusRequestEntityTooLarge
|
||||
@ -3324,3 +3422,219 @@ func TestContextSetCookieData(t *testing.T) {
|
||||
assert.Contains(t, setCookie, "SameSite=None")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetMapFromFormData(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
data map[string][]string
|
||||
key string
|
||||
expected map[string]string
|
||||
found bool
|
||||
}{
|
||||
{
|
||||
name: "Basic bracket notation",
|
||||
data: map[string][]string{
|
||||
"ids[a]": {"hi"},
|
||||
"ids[b]": {"3.14"},
|
||||
},
|
||||
key: "ids",
|
||||
expected: map[string]string{
|
||||
"a": "hi",
|
||||
"b": "3.14",
|
||||
},
|
||||
found: true,
|
||||
},
|
||||
{
|
||||
name: "Mixed data with bracket notation",
|
||||
data: map[string][]string{
|
||||
"ids[a]": {"hi"},
|
||||
"ids[b]": {"3.14"},
|
||||
"names[a]": {"mike"},
|
||||
"names[b]": {"maria"},
|
||||
"other[key]": {"value"},
|
||||
"simple": {"data"},
|
||||
},
|
||||
key: "ids",
|
||||
expected: map[string]string{
|
||||
"a": "hi",
|
||||
"b": "3.14",
|
||||
},
|
||||
found: true,
|
||||
},
|
||||
{
|
||||
name: "Names key",
|
||||
data: map[string][]string{
|
||||
"ids[a]": {"hi"},
|
||||
"ids[b]": {"3.14"},
|
||||
"names[a]": {"mike"},
|
||||
"names[b]": {"maria"},
|
||||
"other[key]": {"value"},
|
||||
},
|
||||
key: "names",
|
||||
expected: map[string]string{
|
||||
"a": "mike",
|
||||
"b": "maria",
|
||||
},
|
||||
found: true,
|
||||
},
|
||||
{
|
||||
name: "Key not found",
|
||||
data: map[string][]string{
|
||||
"ids[a]": {"hi"},
|
||||
"names[b]": {"maria"},
|
||||
},
|
||||
key: "notfound",
|
||||
expected: map[string]string{},
|
||||
found: false,
|
||||
},
|
||||
{
|
||||
name: "Empty data",
|
||||
data: map[string][]string{},
|
||||
key: "ids",
|
||||
expected: map[string]string{},
|
||||
found: false,
|
||||
},
|
||||
{
|
||||
name: "Malformed bracket notation",
|
||||
data: map[string][]string{
|
||||
"ids[a": {"hi"}, // Missing closing bracket
|
||||
"ids]b": {"3.14"}, // Missing opening bracket
|
||||
"idsab": {"value"}, // No brackets
|
||||
},
|
||||
key: "ids",
|
||||
expected: map[string]string{},
|
||||
found: false,
|
||||
},
|
||||
{
|
||||
name: "Nested bracket notation",
|
||||
data: map[string][]string{
|
||||
"ids[a][b]": {"nested"},
|
||||
"ids[c]": {"simple"},
|
||||
},
|
||||
key: "ids",
|
||||
expected: map[string]string{
|
||||
"a": "nested",
|
||||
"c": "simple",
|
||||
},
|
||||
found: true,
|
||||
},
|
||||
{
|
||||
name: "Simple key without brackets",
|
||||
data: map[string][]string{
|
||||
"simple": {"data"},
|
||||
"ids[a]": {"hi"},
|
||||
},
|
||||
key: "simple",
|
||||
expected: map[string]string{},
|
||||
found: false,
|
||||
},
|
||||
{
|
||||
name: "Mixed simple and bracket keys",
|
||||
data: map[string][]string{
|
||||
"simple": {"data"},
|
||||
"ids[a]": {"hi"},
|
||||
"ids[b]": {"3.14"},
|
||||
"other": {"value"},
|
||||
},
|
||||
key: "ids",
|
||||
expected: map[string]string{
|
||||
"a": "hi",
|
||||
"b": "3.14",
|
||||
},
|
||||
found: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result, found := getMapFromFormData(tc.data, tc.key)
|
||||
assert.Equal(t, tc.expected, result, "result mismatch")
|
||||
assert.Equal(t, tc.found, found, "found mismatch")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGetMapFromFormData(b *testing.B) {
|
||||
// Test case 1: Small dataset with bracket notation
|
||||
smallData := map[string][]string{
|
||||
"ids[a]": {"hi"},
|
||||
"ids[b]": {"3.14"},
|
||||
"names[a]": {"mike"},
|
||||
"names[b]": {"maria"},
|
||||
}
|
||||
|
||||
// Test case 2: Medium dataset with mixed data
|
||||
mediumData := map[string][]string{
|
||||
"ids[a]": {"hi"},
|
||||
"ids[b]": {"3.14"},
|
||||
"ids[c]": {"test"},
|
||||
"ids[d]": {"value"},
|
||||
"names[a]": {"mike"},
|
||||
"names[b]": {"maria"},
|
||||
"names[c]": {"john"},
|
||||
"names[d]": {"jane"},
|
||||
"other[key1]": {"value1"},
|
||||
"other[key2]": {"value2"},
|
||||
"simple": {"data"},
|
||||
"another": {"info"},
|
||||
}
|
||||
|
||||
// Test case 3: Large dataset with many bracket keys
|
||||
largeData := make(map[string][]string)
|
||||
for i := 0; i < 100; i++ {
|
||||
key := fmt.Sprintf("ids[%d]", i)
|
||||
largeData[key] = []string{fmt.Sprintf("value%d", i)}
|
||||
}
|
||||
for i := 0; i < 50; i++ {
|
||||
key := fmt.Sprintf("names[%d]", i)
|
||||
largeData[key] = []string{fmt.Sprintf("name%d", i)}
|
||||
}
|
||||
for i := 0; i < 25; i++ {
|
||||
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++ {
|
||||
key := fmt.Sprintf("nonmatching%d", i)
|
||||
worstCaseData[key] = []string{fmt.Sprintf("value%d", i)}
|
||||
}
|
||||
worstCaseData["ids[a]"] = []string{"hi"}
|
||||
worstCaseData["ids[b]"] = []string{"3.14"}
|
||||
|
||||
// Test case 5: Dataset with short keys (best case for early exit)
|
||||
shortKeysData := map[string][]string{
|
||||
"a": {"value1"},
|
||||
"b": {"value2"},
|
||||
"ids[a]": {"hi"},
|
||||
"ids[b]": {"3.14"},
|
||||
}
|
||||
|
||||
benchmarks := []struct {
|
||||
name string
|
||||
data map[string][]string
|
||||
key string
|
||||
}{
|
||||
{"Small_Bracket", smallData, "ids"},
|
||||
{"Small_Names", smallData, "names"},
|
||||
{"Medium_Bracket", mediumData, "ids"},
|
||||
{"Medium_Names", mediumData, "names"},
|
||||
{"Medium_Other", mediumData, "other"},
|
||||
{"Large_Bracket", largeData, "ids"},
|
||||
{"Large_Names", largeData, "names"},
|
||||
{"Large_Other", largeData, "other"},
|
||||
{"WorstCase_Bracket", worstCaseData, "ids"},
|
||||
{"ShortKeys_Bracket", shortKeysData, "ids"},
|
||||
{"Empty_Key", smallData, "notfound"},
|
||||
}
|
||||
|
||||
for _, bm := range benchmarks {
|
||||
b.Run(bm.name, func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = getMapFromFormData(bm.data, bm.key)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
2
debug.go
2
debug.go
@ -13,7 +13,7 @@ import (
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
const ginSupportMinGoVer = 21
|
||||
const ginSupportMinGoVer = 23
|
||||
|
||||
// IsDebugging returns true if the framework is running in debug mode.
|
||||
// Use SetMode(gin.ReleaseMode) to disable debug mode.
|
||||
|
16
doc.go
16
doc.go
@ -2,5 +2,21 @@
|
||||
Package gin implements a HTTP web framework called gin.
|
||||
|
||||
See https://gin-gonic.com/ for more information about gin.
|
||||
|
||||
Example:
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"message": "pong",
|
||||
})
|
||||
})
|
||||
r.Run() // listen and serve on 0.0.0.0:8080
|
||||
}
|
||||
*/
|
||||
package gin // import "github.com/gin-gonic/gin"
|
||||
|
62
docs/doc.md
62
docs/doc.md
@ -63,6 +63,7 @@
|
||||
- [http2 server push](#http2-server-push)
|
||||
- [Define format for the log of routes](#define-format-for-the-log-of-routes)
|
||||
- [Set and get a cookie](#set-and-get-a-cookie)
|
||||
- [Custom json codec at runtime](#custom-json-codec-at-runtime)
|
||||
- [Don't trust all proxies](#dont-trust-all-proxies)
|
||||
- [Testing](#testing)
|
||||
|
||||
@ -872,7 +873,7 @@ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-1
|
||||
|
||||
If the server should bind a default value to a field when the client does not provide one, specify the default value using the `default` key within the `form` tag:
|
||||
|
||||
```
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
@ -2371,6 +2372,65 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
### Custom json codec at runtime
|
||||
|
||||
Gin support custom json serialization and deserialization logic without using compile tags.
|
||||
|
||||
1. Define a custom struct implements the `json.Core` interface.
|
||||
|
||||
2. Before your engine starts, assign values to `json.API` using the custom struct.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
var customConfig = jsoniter.Config{
|
||||
EscapeHTML: true,
|
||||
SortMapKeys: true,
|
||||
ValidateJsonRawMessage: true,
|
||||
}.Froze()
|
||||
|
||||
// implement api.JsonApi
|
||||
type customJsonApi struct {
|
||||
}
|
||||
|
||||
func (j customJsonApi) Marshal(v any) ([]byte, error) {
|
||||
return customConfig.Marshal(v)
|
||||
}
|
||||
|
||||
func (j customJsonApi) Unmarshal(data []byte, v any) error {
|
||||
return customConfig.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
func (j customJsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
|
||||
return customConfig.MarshalIndent(v, prefix, indent)
|
||||
}
|
||||
|
||||
func (j customJsonApi) NewEncoder(writer io.Writer) json.Encoder {
|
||||
return customConfig.NewEncoder(writer)
|
||||
}
|
||||
|
||||
func (j customJsonApi) NewDecoder(reader io.Reader) json.Decoder {
|
||||
return customConfig.NewDecoder(reader)
|
||||
}
|
||||
|
||||
func main() {
|
||||
//Replace the default json api
|
||||
json.API = customJsonApi{}
|
||||
|
||||
//Start your gin engine
|
||||
router := gin.Default()
|
||||
router.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
## Don't trust all proxies
|
||||
|
||||
Gin lets you specify which headers to hold the real client IP (if any),
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
)
|
||||
|
||||
// ErrorType is an unsigned 64-bit error code as defined in the gin spec.
|
||||
@ -77,7 +77,7 @@ func (msg *Error) JSON() any {
|
||||
|
||||
// MarshalJSON implements the json.Marshaller interface.
|
||||
func (msg *Error) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(msg.JSON())
|
||||
return json.API.Marshal(msg.JSON())
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
@ -157,7 +157,7 @@ func (a errorMsgs) JSON() any {
|
||||
|
||||
// MarshalJSON implements the json.Marshaller interface.
|
||||
func (a errorMsgs) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(a.JSON())
|
||||
return json.API.Marshal(a.JSON())
|
||||
}
|
||||
|
||||
func (a errorMsgs) String() string {
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -33,7 +33,7 @@ func TestError(t *testing.T) {
|
||||
"meta": "some data",
|
||||
}, err.JSON())
|
||||
|
||||
jsonBytes, _ := json.Marshal(err)
|
||||
jsonBytes, _ := json.API.Marshal(err)
|
||||
assert.JSONEq(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
|
||||
|
||||
err.SetMeta(H{ //nolint: errcheck
|
||||
@ -92,13 +92,13 @@ Error #03: third
|
||||
H{"error": "second", "meta": "some data"},
|
||||
H{"error": "third", "status": "400"},
|
||||
}, errs.JSON())
|
||||
jsonBytes, _ := json.Marshal(errs)
|
||||
jsonBytes, _ := json.API.Marshal(errs)
|
||||
assert.JSONEq(t, "[{\"error\":\"first\"},{\"error\":\"second\",\"meta\":\"some data\"},{\"error\":\"third\",\"status\":\"400\"}]", string(jsonBytes))
|
||||
errs = errorMsgs{
|
||||
{Err: errors.New("first"), Type: ErrorTypePrivate},
|
||||
}
|
||||
assert.Equal(t, H{"error": "first"}, errs.JSON())
|
||||
jsonBytes, _ = json.Marshal(errs)
|
||||
jsonBytes, _ = json.API.Marshal(errs)
|
||||
assert.JSONEq(t, "{\"error\":\"first\"}", string(jsonBytes))
|
||||
|
||||
errs = errorMsgs{}
|
||||
|
42
go.mod
42
go.mod
@ -3,44 +3,42 @@ module github.com/gin-gonic/gin
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.13.2
|
||||
github.com/bytedance/sonic v1.14.0
|
||||
github.com/gin-contrib/sse v1.1.0
|
||||
github.com/go-playground/validator/v10 v10.26.0
|
||||
github.com/go-playground/validator/v10 v10.27.0
|
||||
github.com/goccy/go-json v0.10.2
|
||||
github.com/goccy/go-yaml v1.18.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/modern-go/reflect2 v1.0.2
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/quic-go/quic-go v0.52.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/ugorji/go/codec v1.2.12
|
||||
golang.org/x/net v0.40.0
|
||||
google.golang.org/protobuf v1.36.6
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
github.com/quic-go/quic-go v0.54.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/ugorji/go/codec v1.3.0
|
||||
golang.org/x/net v0.42.0
|
||||
google.golang.org/protobuf v1.36.9
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.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/reflect2 v1.0.2 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
97
go.sum
97
go.sum
@ -1,14 +1,9 @@
|
||||
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
||||
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@ -16,33 +11,25 @@ github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3G
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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/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=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
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=
|
||||
@ -51,58 +38,50 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OH
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/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/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
|
||||
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
|
||||
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
||||
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||
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.52.0 h1:/SlHrCRElyaU6MaEPKqKr9z83sBg2v4FLLvWM+Z47pA=
|
||||
github.com/quic-go/quic-go v0.52.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
|
@ -1,25 +0,0 @@
|
||||
// Copyright 2017 Bo-Yi Wu. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go_json
|
||||
|
||||
package json
|
||||
|
||||
import json "github.com/goccy/go-json"
|
||||
|
||||
var (
|
||||
// Marshal is exported by gin/json package.
|
||||
Marshal = json.Marshal
|
||||
// Unmarshal is exported by gin/json package.
|
||||
Unmarshal = json.Unmarshal
|
||||
// MarshalIndent is exported by gin/json package.
|
||||
MarshalIndent = json.MarshalIndent
|
||||
// NewDecoder is exported by gin/json package.
|
||||
NewDecoder = json.NewDecoder
|
||||
// NewEncoder is exported by gin/json package.
|
||||
NewEncoder = json.NewEncoder
|
||||
)
|
||||
|
||||
// Package indicates what library is being used for JSON encoding.
|
||||
const Package = "github.com/goccy/go-json"
|
@ -1,25 +0,0 @@
|
||||
// Copyright 2017 Bo-Yi Wu. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !jsoniter && !go_json && !(sonic && (linux || windows || darwin))
|
||||
|
||||
package json
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
var (
|
||||
// Marshal is exported by gin/json package.
|
||||
Marshal = json.Marshal
|
||||
// Unmarshal is exported by gin/json package.
|
||||
Unmarshal = json.Unmarshal
|
||||
// MarshalIndent is exported by gin/json package.
|
||||
MarshalIndent = json.MarshalIndent
|
||||
// NewDecoder is exported by gin/json package.
|
||||
NewDecoder = json.NewDecoder
|
||||
// NewEncoder is exported by gin/json package.
|
||||
NewEncoder = json.NewEncoder
|
||||
)
|
||||
|
||||
// Package indicates what library is being used for JSON encoding.
|
||||
const Package = "encoding/json"
|
@ -1,26 +0,0 @@
|
||||
// Copyright 2017 Bo-Yi Wu. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build jsoniter
|
||||
|
||||
package json
|
||||
|
||||
import jsoniter "github.com/json-iterator/go"
|
||||
|
||||
var (
|
||||
json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
// Marshal is exported by gin/json package.
|
||||
Marshal = json.Marshal
|
||||
// Unmarshal is exported by gin/json package.
|
||||
Unmarshal = json.Unmarshal
|
||||
// MarshalIndent is exported by gin/json package.
|
||||
MarshalIndent = json.MarshalIndent
|
||||
// NewDecoder is exported by gin/json package.
|
||||
NewDecoder = json.NewDecoder
|
||||
// NewEncoder is exported by gin/json package.
|
||||
NewEncoder = json.NewEncoder
|
||||
)
|
||||
|
||||
// Package indicates what library is being used for JSON encoding.
|
||||
const Package = "github.com/json-iterator/go"
|
@ -1,26 +0,0 @@
|
||||
// Copyright 2022 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.
|
||||
|
||||
//go:build sonic && (linux || windows || darwin)
|
||||
|
||||
package json
|
||||
|
||||
import "github.com/bytedance/sonic"
|
||||
|
||||
var (
|
||||
json = sonic.ConfigStd
|
||||
// Marshal is exported by gin/json package.
|
||||
Marshal = json.Marshal
|
||||
// Unmarshal is exported by gin/json package.
|
||||
Unmarshal = json.Unmarshal
|
||||
// MarshalIndent is exported by gin/json package.
|
||||
MarshalIndent = json.MarshalIndent
|
||||
// NewDecoder is exported by gin/json package.
|
||||
NewDecoder = json.NewDecoder
|
||||
// NewEncoder is exported by gin/json package.
|
||||
NewEncoder = json.NewEncoder
|
||||
)
|
||||
|
||||
// Package indicates what library is being used for JSON encoding.
|
||||
const Package = "github.com/bytedance/sonic"
|
@ -44,7 +44,7 @@ type LoggerConfig struct {
|
||||
// Optional. Default value is gin.DefaultWriter.
|
||||
Output io.Writer
|
||||
|
||||
// SkipPaths is an url path array which logs are not written.
|
||||
// SkipPaths is a URL path array which logs are not written.
|
||||
// Optional.
|
||||
SkipPaths []string
|
||||
|
||||
|
2
mode.go
2
mode.go
@ -65,7 +65,7 @@ func SetMode(value string) {
|
||||
}
|
||||
|
||||
switch value {
|
||||
case DebugMode, "":
|
||||
case DebugMode:
|
||||
atomic.StoreInt32(&ginMode, debugCode)
|
||||
case ReleaseMode:
|
||||
atomic.StoreInt32(&ginMode, releaseCode)
|
||||
|
39
recovery.go
39
recovery.go
@ -17,6 +17,8 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||
)
|
||||
|
||||
const dunno = "???"
|
||||
@ -67,19 +69,15 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
|
||||
}
|
||||
}
|
||||
if logger != nil {
|
||||
stack := stack(3)
|
||||
httpRequest, _ := httputil.DumpRequest(c.Request, false)
|
||||
headers := strings.Split(string(httpRequest), "\r\n")
|
||||
maskAuthorization(headers)
|
||||
headersToStr := strings.Join(headers, "\r\n")
|
||||
const stackSkip = 3
|
||||
if brokenPipe {
|
||||
logger.Printf("%s\n%s%s", err, headersToStr, reset)
|
||||
logger.Printf("%s\n%s%s", err, secureRequestDump(c.Request), reset)
|
||||
} else if IsDebugging() {
|
||||
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
|
||||
timeFormat(time.Now()), headersToStr, err, stack, reset)
|
||||
timeFormat(time.Now()), secureRequestDump(c.Request), err, stack(stackSkip), reset)
|
||||
} else {
|
||||
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
|
||||
timeFormat(time.Now()), err, stack, reset)
|
||||
timeFormat(time.Now()), err, stack(stackSkip), reset)
|
||||
}
|
||||
}
|
||||
if brokenPipe {
|
||||
@ -95,6 +93,21 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// secureRequestDump returns a sanitized HTTP request dump where the Authorization header,
|
||||
// if present, is replaced with a masked value ("Authorization: *") to avoid leaking sensitive credentials.
|
||||
//
|
||||
// Currently, only the Authorization header is sanitized. All other headers and request data remain unchanged.
|
||||
func secureRequestDump(r *http.Request) string {
|
||||
httpRequest, _ := httputil.DumpRequest(r, false)
|
||||
lines := strings.Split(bytesconv.BytesToString(httpRequest), "\r\n")
|
||||
for i, line := range lines {
|
||||
if strings.HasPrefix(line, "Authorization:") {
|
||||
lines[i] = "Authorization: *"
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\r\n")
|
||||
}
|
||||
|
||||
func defaultHandleRecovery(c *Context, _ any) {
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
}
|
||||
@ -126,16 +139,6 @@ func stack(skip int) []byte {
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// maskAuthorization replaces any "Authorization: <token>" header with "Authorization: *", hiding sensitive credentials.
|
||||
func maskAuthorization(headers []string) {
|
||||
for idx, header := range headers {
|
||||
key, _, _ := strings.Cut(header, ":")
|
||||
if strings.EqualFold(key, "Authorization") {
|
||||
headers[idx] = key + ": *"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -88,24 +88,6 @@ func TestPanicWithAbort(t *testing.T) {
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func TestMaskAuthorization(t *testing.T) {
|
||||
secret := "Bearer aaaabbbbccccddddeeeeffff"
|
||||
headers := []string{
|
||||
"Host: www.example.com",
|
||||
"Authorization: " + secret,
|
||||
"User-Agent: curl/7.51.0",
|
||||
"Accept: */*",
|
||||
"Content-Type: application/json",
|
||||
"Content-Length: 1",
|
||||
}
|
||||
maskAuthorization(headers)
|
||||
|
||||
for _, h := range headers {
|
||||
assert.NotContains(t, h, secret)
|
||||
}
|
||||
assert.Contains(t, headers, "Authorization: *")
|
||||
}
|
||||
|
||||
func TestSource(t *testing.T) {
|
||||
bs := source(nil, 0)
|
||||
assert.Equal(t, dunnoBytes, bs)
|
||||
@ -263,3 +245,65 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
|
||||
|
||||
SetMode(TestMode)
|
||||
}
|
||||
|
||||
func TestSecureRequestDump(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
req *http.Request
|
||||
wantContains string
|
||||
wantNotContain string
|
||||
}{
|
||||
{
|
||||
name: "Authorization header standard case",
|
||||
req: func() *http.Request {
|
||||
r, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
r.Header.Set("Authorization", "Bearer secret-token")
|
||||
return r
|
||||
}(),
|
||||
wantContains: "Authorization: *",
|
||||
wantNotContain: "Bearer secret-token",
|
||||
},
|
||||
{
|
||||
name: "authorization header lowercase",
|
||||
req: func() *http.Request {
|
||||
r, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
r.Header.Set("authorization", "some-secret")
|
||||
return r
|
||||
}(),
|
||||
wantContains: "Authorization: *",
|
||||
wantNotContain: "some-secret",
|
||||
},
|
||||
{
|
||||
name: "Authorization header mixed case",
|
||||
req: func() *http.Request {
|
||||
r, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
r.Header.Set("AuThOrIzAtIoN", "token123")
|
||||
return r
|
||||
}(),
|
||||
wantContains: "Authorization: *",
|
||||
wantNotContain: "token123",
|
||||
},
|
||||
{
|
||||
name: "No Authorization header",
|
||||
req: func() *http.Request {
|
||||
r, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
return r
|
||||
}(),
|
||||
wantContains: "",
|
||||
wantNotContain: "Authorization: *",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := secureRequestDump(tt.req)
|
||||
if tt.wantContains != "" && !strings.Contains(result, tt.wantContains) {
|
||||
t.Errorf("maskHeaders() = %q, want contains %q", result, tt.wantContains)
|
||||
}
|
||||
if tt.wantNotContain != "" && strings.Contains(result, tt.wantNotContain) {
|
||||
t.Errorf("maskHeaders() = %q, want NOT contain %q", result, tt.wantNotContain)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -11,8 +11,8 @@ import (
|
||||
"net/http"
|
||||
"unicode"
|
||||
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
)
|
||||
|
||||
// JSON contains the given interface object.
|
||||
@ -66,7 +66,7 @@ func (r JSON) WriteContentType(w http.ResponseWriter) {
|
||||
// WriteJSON marshals the given interface object and writes it with custom ContentType.
|
||||
func WriteJSON(w http.ResponseWriter, obj any) error {
|
||||
writeContentType(w, jsonContentType)
|
||||
jsonBytes, err := json.Marshal(obj)
|
||||
jsonBytes, err := json.API.Marshal(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -77,7 +77,7 @@ func WriteJSON(w http.ResponseWriter, obj any) error {
|
||||
// Render (IndentedJSON) marshals the given interface object and writes it with custom ContentType.
|
||||
func (r IndentedJSON) Render(w http.ResponseWriter) error {
|
||||
r.WriteContentType(w)
|
||||
jsonBytes, err := json.MarshalIndent(r.Data, "", " ")
|
||||
jsonBytes, err := json.API.MarshalIndent(r.Data, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -93,7 +93,7 @@ func (r IndentedJSON) WriteContentType(w http.ResponseWriter) {
|
||||
// Render (SecureJSON) marshals the given interface object and writes it with custom ContentType.
|
||||
func (r SecureJSON) Render(w http.ResponseWriter) error {
|
||||
r.WriteContentType(w)
|
||||
jsonBytes, err := json.Marshal(r.Data)
|
||||
jsonBytes, err := json.API.Marshal(r.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -116,7 +116,7 @@ func (r SecureJSON) WriteContentType(w http.ResponseWriter) {
|
||||
// Render (JsonpJSON) marshals the given interface object and writes it and its callback with custom ContentType.
|
||||
func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
|
||||
r.WriteContentType(w)
|
||||
ret, err := json.Marshal(r.Data)
|
||||
ret, err := json.API.Marshal(r.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -154,7 +154,7 @@ func (r JsonpJSON) WriteContentType(w http.ResponseWriter) {
|
||||
// Render (AsciiJSON) marshals the given interface object and writes it with custom ContentType.
|
||||
func (r AsciiJSON) Render(w http.ResponseWriter) error {
|
||||
r.WriteContentType(w)
|
||||
ret, err := json.Marshal(r.Data)
|
||||
ret, err := json.API.Marshal(r.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -183,7 +183,7 @@ func (r AsciiJSON) WriteContentType(w http.ResponseWriter) {
|
||||
// Render (PureJSON) writes custom ContentType and encodes the given interface object.
|
||||
func (r PureJSON) Render(w http.ResponseWriter) error {
|
||||
r.WriteContentType(w)
|
||||
encoder := json.NewEncoder(w)
|
||||
encoder := json.API.NewEncoder(w)
|
||||
encoder.SetEscapeHTML(false)
|
||||
return encoder.Encode(r.Data)
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ func (r Reader) Render(w http.ResponseWriter) (err error) {
|
||||
}
|
||||
r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10)
|
||||
}
|
||||
r.writeHeaders(w, r.Headers)
|
||||
r.writeHeaders(w)
|
||||
_, err = io.Copy(w, r.Reader)
|
||||
return
|
||||
}
|
||||
@ -37,10 +37,10 @@ func (r Reader) WriteContentType(w http.ResponseWriter) {
|
||||
writeContentType(w, []string{r.ContentType})
|
||||
}
|
||||
|
||||
// writeHeaders writes custom Header.
|
||||
func (r Reader) writeHeaders(w http.ResponseWriter, headers map[string]string) {
|
||||
// writeHeaders writes headers from r.Headers into response.
|
||||
func (r Reader) writeHeaders(w http.ResponseWriter) {
|
||||
header := w.Header()
|
||||
for k, v := range headers {
|
||||
for k, v := range r.Headers {
|
||||
if header.Get(k) == "" {
|
||||
header.Set(k, v)
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -173,7 +173,7 @@ func TestRenderJsonpJSONError(t *testing.T) {
|
||||
err = jsonpJSON.Render(ew)
|
||||
assert.Equal(t, `write "`+`(`+`" error`, err.Error())
|
||||
|
||||
data, _ := json.Marshal(jsonpJSON.Data) // error was returned while writing data
|
||||
data, _ := json.API.Marshal(jsonpJSON.Data) // error was returned while writing data
|
||||
ew.bufString = string(data)
|
||||
err = jsonpJSON.Render(ew)
|
||||
assert.Equal(t, `write "`+string(data)+`" error`, err.Error())
|
||||
@ -285,7 +285,14 @@ b:
|
||||
|
||||
err := (YAML{data}).Render(w)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "|4-\n a : Easy!\n b:\n \tc: 2\n \td: [3, 4]\n \t\n", w.Body.String())
|
||||
|
||||
// With github.com/goccy/go-yaml, the output format is different from gopkg.in/yaml.v3
|
||||
// We're checking that the output contains the expected data, not the exact formatting
|
||||
output := w.Body.String()
|
||||
assert.Contains(t, output, "a : Easy!")
|
||||
assert.Contains(t, output, "b:")
|
||||
assert.Contains(t, output, "c: 2")
|
||||
assert.Contains(t, output, "d: [3, 4]")
|
||||
assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@ type TOML struct {
|
||||
Data any
|
||||
}
|
||||
|
||||
var TOMLContentType = []string{"application/toml; charset=utf-8"}
|
||||
var tomlContentType = []string{"application/toml; charset=utf-8"}
|
||||
|
||||
// Render (TOML) marshals the given interface object and writes data with custom ContentType.
|
||||
func (r TOML) Render(w http.ResponseWriter) error {
|
||||
@ -32,5 +32,5 @@ func (r TOML) Render(w http.ResponseWriter) error {
|
||||
|
||||
// WriteContentType (TOML) writes TOML ContentType for response.
|
||||
func (r TOML) WriteContentType(w http.ResponseWriter) {
|
||||
writeContentType(w, TOMLContentType)
|
||||
writeContentType(w, tomlContentType)
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ package render
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"github.com/goccy/go-yaml"
|
||||
)
|
||||
|
||||
// YAML contains the given interface object.
|
||||
|
@ -6,6 +6,7 @@ package gin
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@ -16,6 +17,8 @@ const (
|
||||
defaultStatus = http.StatusOK
|
||||
)
|
||||
|
||||
var errHijackAlreadyWritten = errors.New("gin: response already written")
|
||||
|
||||
// ResponseWriter ...
|
||||
type ResponseWriter interface {
|
||||
http.ResponseWriter
|
||||
@ -106,6 +109,9 @@ func (w *responseWriter) Written() bool {
|
||||
|
||||
// Hijack implements the http.Hijacker interface.
|
||||
func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if w.Written() {
|
||||
return nil, nil, errHijackAlreadyWritten
|
||||
}
|
||||
if w.size < 0 {
|
||||
w.size = 0
|
||||
}
|
||||
|
@ -5,6 +5,8 @@
|
||||
package gin
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@ -124,6 +126,74 @@ func TestResponseWriterHijack(t *testing.T) {
|
||||
w.Flush()
|
||||
}
|
||||
|
||||
type mockHijacker struct {
|
||||
*httptest.ResponseRecorder
|
||||
hijacked bool
|
||||
}
|
||||
|
||||
// Hijack implements the http.Hijacker interface. It just records that it was called.
|
||||
func (m *mockHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
m.hijacked = true
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func TestResponseWriterHijackAfterWrite(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
action func(w ResponseWriter) error // Action to perform before hijacking
|
||||
expectWrittenBeforeHijack bool
|
||||
expectHijackSuccess bool
|
||||
expectWrittenAfterHijack bool
|
||||
expectError error
|
||||
}{
|
||||
{
|
||||
name: "hijack before write should succeed",
|
||||
action: func(w ResponseWriter) error { return nil },
|
||||
expectWrittenBeforeHijack: false,
|
||||
expectHijackSuccess: true,
|
||||
expectWrittenAfterHijack: true, // Hijack itself marks the writer as written
|
||||
expectError: nil,
|
||||
},
|
||||
{
|
||||
name: "hijack after write should fail",
|
||||
action: func(w ResponseWriter) error {
|
||||
_, err := w.Write([]byte("test"))
|
||||
return err
|
||||
},
|
||||
expectWrittenBeforeHijack: true,
|
||||
expectHijackSuccess: false,
|
||||
expectWrittenAfterHijack: true,
|
||||
expectError: errHijackAlreadyWritten,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
hijacker := &mockHijacker{ResponseRecorder: httptest.NewRecorder()}
|
||||
writer := &responseWriter{}
|
||||
writer.reset(hijacker)
|
||||
w := ResponseWriter(writer)
|
||||
|
||||
// Check initial state
|
||||
assert.False(t, w.Written(), "should not be written initially")
|
||||
|
||||
// Perform pre-hijack action
|
||||
require.NoError(t, tc.action(w), "unexpected error during pre-hijack action")
|
||||
|
||||
// Check state before hijacking
|
||||
assert.Equal(t, tc.expectWrittenBeforeHijack, w.Written(), "unexpected w.Written() state before hijack")
|
||||
|
||||
// Attempt to hijack
|
||||
_, _, hijackErr := w.Hijack()
|
||||
|
||||
// Check results
|
||||
require.ErrorIs(t, hijackErr, tc.expectError, "unexpected error from Hijack()")
|
||||
assert.Equal(t, tc.expectHijackSuccess, hijacker.hijacked, "unexpected hijacker.hijacked state")
|
||||
assert.Equal(t, tc.expectWrittenAfterHijack, w.Written(), "unexpected w.Written() state after hijack")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseWriterFlush(t *testing.T) {
|
||||
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
writer := &responseWriter{}
|
||||
|
@ -11,6 +11,8 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var MaxHandlers = 32
|
||||
|
||||
func init() {
|
||||
SetMode(TestMode)
|
||||
}
|
||||
@ -193,3 +195,25 @@ func testRoutesInterface(t *testing.T, r IRoutes) {
|
||||
assert.Equal(t, r, r.Static("/static", "."))
|
||||
assert.Equal(t, r, r.StaticFS("/static2", Dir(".", false)))
|
||||
}
|
||||
|
||||
func TestRouterGroupCombineHandlersTooManyHandlers(t *testing.T) {
|
||||
group := &RouterGroup{
|
||||
Handlers: make(HandlersChain, MaxHandlers), // Assume group already has MaxHandlers middleware
|
||||
}
|
||||
tooManyHandlers := make(HandlersChain, MaxHandlers) // Add MaxHandlers more, total 2 * MaxHandlers
|
||||
|
||||
// This should trigger panic
|
||||
assert.Panics(t, func() {
|
||||
group.combineHandlers(tooManyHandlers)
|
||||
}, "should panic due to too many handlers")
|
||||
}
|
||||
|
||||
func TestRouterGroupCombineHandlersEmptySliceNotNil(t *testing.T) {
|
||||
group := &RouterGroup{
|
||||
Handlers: HandlersChain{},
|
||||
}
|
||||
|
||||
result := group.combineHandlers(HandlersChain{})
|
||||
assert.NotNil(t, result, "result should not be nil even with empty handlers")
|
||||
assert.Empty(t, result, "empty handlers should return empty chain")
|
||||
}
|
||||
|
@ -6,7 +6,10 @@ package gin
|
||||
|
||||
import "net/http"
|
||||
|
||||
// CreateTestContext returns a fresh engine and context for testing purposes
|
||||
// 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
|
||||
// along with a context, for example, to test middleware that doesn't depend on
|
||||
// specific routes. The ResponseWriter `w` is used to initialize the context's writer.
|
||||
func CreateTestContext(w http.ResponseWriter) (c *Context, r *Engine) {
|
||||
r = New()
|
||||
c = r.allocateContext(0)
|
||||
@ -15,7 +18,11 @@ func CreateTestContext(w http.ResponseWriter) (c *Context, r *Engine) {
|
||||
return
|
||||
}
|
||||
|
||||
// CreateTestContextOnly returns a fresh context base on the engine for testing purposes
|
||||
// CreateTestContextOnly returns a fresh Context associated with the provided Engine `r`.
|
||||
// This is useful for tests that operate on an existing, possibly pre-configured,
|
||||
// Gin engine instance and need a new context for it.
|
||||
// The ResponseWriter `w` is used to initialize the context's writer.
|
||||
// The context is allocated with the `maxParams` setting from the provided engine.
|
||||
func CreateTestContextOnly(w http.ResponseWriter, r *Engine) (c *Context) {
|
||||
c = r.allocateContext(r.maxParams)
|
||||
c.reset()
|
||||
|
2
testdata/test_file.txt
vendored
Normal file
2
testdata/test_file.txt
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
This is a test file for Context.File() method testing.
|
||||
It contains some sample content to verify file serving functionality.
|
Loading…
x
Reference in New Issue
Block a user