diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml
index 22edf453..3ca5eb20 100644
--- a/.github/workflows/goreleaser.yml
+++ b/.github/workflows/goreleaser.yml
@@ -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"
diff --git a/README.md b/README.md
index 1a582709..94e08a78 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-[](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
diff --git a/binding/form_mapping.go b/binding/form_mapping.go
index 235692d2..45a39e15 100644
--- a/binding/form_mapping.go
+++ b/binding/form_mapping.go
@@ -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()))
diff --git a/binding/json.go b/binding/json.go
index e21c2ee3..f4ae921a 100644
--- a/binding/json.go
+++ b/binding/json.go
@@ -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()
}
diff --git a/binding/json_test.go b/binding/json_test.go
index fbd5c527..942ee3eb 100644
--- a/binding/json_test.go
+++ b/binding/json_test.go
@@ -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
diff --git a/binding/yaml.go b/binding/yaml.go
index 2535f8c3..6638e739 100644
--- a/binding/yaml.go
+++ b/binding/yaml.go
@@ -9,7 +9,7 @@ import (
"io"
"net/http"
- "gopkg.in/yaml.v3"
+ "github.com/goccy/go-yaml"
)
type yamlBinding struct{}
diff --git a/codec/json/api.go b/codec/json/api.go
new file mode 100644
index 00000000..f2135683
--- /dev/null
+++ b/codec/json/api.go
@@ -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
+}
diff --git a/codec/json/go_json.go b/codec/json/go_json.go
new file mode 100644
index 00000000..42a476ac
--- /dev/null
+++ b/codec/json/go_json.go
@@ -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)
+}
diff --git a/codec/json/json.go b/codec/json/json.go
new file mode 100644
index 00000000..2971f42f
--- /dev/null
+++ b/codec/json/json.go
@@ -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)
+}
diff --git a/codec/json/jsoniter.go b/codec/json/jsoniter.go
new file mode 100644
index 00000000..ea624e77
--- /dev/null
+++ b/codec/json/jsoniter.go
@@ -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)
+}
diff --git a/codec/json/sonic.go b/codec/json/sonic.go
new file mode 100644
index 00000000..69496565
--- /dev/null
+++ b/codec/json/sonic.go
@@ -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)
+}
diff --git a/context.go b/context.go
index bf12830c..842ad2ff 100644
--- a/context.go
+++ b/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,23 @@ 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
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]
+ found = true
+ d[k[i+1:][:j]] = v[0]
}
}
}
- return dicts, exist
+ return d, found
}
// FormFile returns the first file for the provided form key.
diff --git a/context_file_test.go b/context_file_test.go
new file mode 100644
index 00000000..50cc3c8e
--- /dev/null
+++ b/context_file_test.go
@@ -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)
+}
diff --git a/context_test.go b/context_test.go
index ff43cd0a..f51c147f 100644
--- a/context_test.go
+++ b/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)
@@ -3324,3 +3423,134 @@ 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")
+ })
+ }
+}
diff --git a/debug.go b/debug.go
index 0ab14e4e..7fe2762e 100644
--- a/debug.go
+++ b/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.
diff --git a/doc.go b/doc.go
index 1bd03864..9442aa70 100644
--- a/doc.go
+++ b/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"
diff --git a/docs/doc.md b/docs/doc.md
index 0e882a1b..0dd86684 100644
--- a/docs/doc.md
+++ b/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),
diff --git a/errors.go b/errors.go
index b0d70a94..829e9d2c 100644
--- a/errors.go
+++ b/errors.go
@@ -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 {
diff --git a/errors_test.go b/errors_test.go
index 85ed3dd5..6d8df278 100644
--- a/errors_test.go
+++ b/errors_test.go
@@ -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{}
diff --git a/go.mod b/go.mod
index 3d7e38ab..5e3e7f76 100644
--- a/go.mod
+++ b/go.mod
@@ -3,44 +3,42 @@ module github.com/gin-gonic/gin
go 1.23.0
require (
- github.com/bytedance/sonic v1.14.0
+ github.com/bytedance/sonic v1.13.2
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/quic-go/quic-go v0.54.0
github.com/stretchr/testify v1.10.0
- github.com/ugorji/go/codec v1.2.12
- golang.org/x/net v0.40.0
+ github.com/ugorji/go/codec v1.3.0
+ golang.org/x/net v0.42.0
google.golang.org/protobuf v1.36.6
- gopkg.in/yaml.v3 v3.0.1
)
require (
- github.com/bytedance/sonic/loader v0.3.0 // indirect
- github.com/cloudwego/base64x v0.1.6 // indirect
+ github.com/bytedance/sonic/loader v0.2.4 // indirect
+ github.com/cloudwego/base64x v0.1.5 // 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.3.0 // indirect
+ github.com/klauspost/cpuid/v2 v2.0.9 // 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.20.0 // 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.35.0 // indirect
- golang.org/x/text v0.25.0 // indirect
- golang.org/x/tools v0.22.0 // indirect
+ golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // 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.34.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
)
diff --git a/go.sum b/go.sum
index 7c84fec5..c97f1d95 100644
--- a/go.sum
+++ b/go.sum
@@ -1,12 +1,11 @@
-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/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.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
-github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
+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/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/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=
@@ -14,32 +13,26 @@ 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.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
-github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+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/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=
@@ -48,23 +41,19 @@ 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=
@@ -72,28 +61,27 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
-golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
-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.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+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.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
-golang.org/x/sys v0.35.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=
+golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
+golang.org/x/sys v0.34.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.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
@@ -101,3 +89,4 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
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=
diff --git a/internal/json/go_json.go b/internal/json/go_json.go
deleted file mode 100644
index dee09dec..00000000
--- a/internal/json/go_json.go
+++ /dev/null
@@ -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"
diff --git a/internal/json/json.go b/internal/json/json.go
deleted file mode 100644
index 539daa78..00000000
--- a/internal/json/json.go
+++ /dev/null
@@ -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"
diff --git a/internal/json/jsoniter.go b/internal/json/jsoniter.go
deleted file mode 100644
index 287ebf70..00000000
--- a/internal/json/jsoniter.go
+++ /dev/null
@@ -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"
diff --git a/internal/json/sonic.go b/internal/json/sonic.go
deleted file mode 100644
index b3f72424..00000000
--- a/internal/json/sonic.go
+++ /dev/null
@@ -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"
diff --git a/logger.go b/logger.go
index f4a250ac..47827787 100644
--- a/logger.go
+++ b/logger.go
@@ -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
diff --git a/mode.go b/mode.go
index cc313437..dfef07d6 100644
--- a/mode.go
+++ b/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)
diff --git a/recovery.go b/recovery.go
index 8a077dbb..fdd463f3 100644
--- a/recovery.go
+++ b/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: " 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
diff --git a/recovery_test.go b/recovery_test.go
index 3a36fad9..8a9e3475 100644
--- a/recovery_test.go
+++ b/recovery_test.go
@@ -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)
+ }
+ })
+ }
+}
diff --git a/render/json.go b/render/json.go
index 23923c44..2f98676c 100644
--- a/render/json.go
+++ b/render/json.go
@@ -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)
}
diff --git a/render/render_test.go b/render/render_test.go
index 4d8ebb19..d9ae2067 100644
--- a/render/render_test.go
+++ b/render/render_test.go
@@ -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"))
}
diff --git a/render/toml.go b/render/toml.go
index 40f044c8..379ac72d 100644
--- a/render/toml.go
+++ b/render/toml.go
@@ -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)
}
diff --git a/render/yaml.go b/render/yaml.go
index 042bb821..98b06442 100644
--- a/render/yaml.go
+++ b/render/yaml.go
@@ -7,7 +7,7 @@ package render
import (
"net/http"
- "gopkg.in/yaml.v3"
+ "github.com/goccy/go-yaml"
)
// YAML contains the given interface object.
diff --git a/response_writer.go b/response_writer.go
index 753a0b09..ab2f5fec 100644
--- a/response_writer.go
+++ b/response_writer.go
@@ -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
}
diff --git a/response_writer_test.go b/response_writer_test.go
index 259b8fa8..ef198418 100644
--- a/response_writer_test.go
+++ b/response_writer_test.go
@@ -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{}
diff --git a/routergroup_test.go b/routergroup_test.go
index 6848063e..182c5589 100644
--- a/routergroup_test.go
+++ b/routergroup_test.go
@@ -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")
+}
diff --git a/test_helpers.go b/test_helpers.go
index 7508c5c9..a1a7c562 100644
--- a/test_helpers.go
+++ b/test_helpers.go
@@ -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()
diff --git a/testdata/test_file.txt b/testdata/test_file.txt
new file mode 100644
index 00000000..05fc0842
--- /dev/null
+++ b/testdata/test_file.txt
@@ -0,0 +1,2 @@
+This is a test file for Context.File() method testing.
+It contains some sample content to verify file serving functionality.
\ No newline at end of file