Compare commits

...

5 Commits

Author SHA1 Message Date
Ruben de Vries
d660a2aa54
Merge a7b757e33830094fcb2af6156cb963a96e87a47b into 5260de6a83283abb87e827130accd495ad543cf3 2026-02-18 18:13:58 +08:00
dependabot[bot]
5260de6a83
chore(deps): bump golang.org/x/net from 0.49.0 to 0.50.0 (#4538)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.49.0 to 0.50.0.
- [Commits](https://github.com/golang/net/compare/v0.49.0...v0.50.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.50.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-18 00:40:02 +08:00
dependabot[bot]
5f424ff6f6
chore(deps): bump github.com/bytedance/sonic from 1.14.2 to 1.15.0 (#4539)
Bumps [github.com/bytedance/sonic](https://github.com/bytedance/sonic) from 1.14.2 to 1.15.0.
- [Release notes](https://github.com/bytedance/sonic/releases)
- [Commits](https://github.com/bytedance/sonic/compare/v1.14.2...v1.15.0)

---
updated-dependencies:
- dependency-name: github.com/bytedance/sonic
  dependency-version: 1.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-18 00:39:40 +08:00
Amirhf
216a4a7c28
test(render): add comprehensive tests for MsgPack render (#4537)
* test(render): add comprehensive tests for MsgPack render

* test(render): make msgpack tests deterministic

Decode the rendered msgpack output and assert values instead of comparing raw bytes (which can vary with map iteration order).
Enable MsgpackHandle.RawToString so msgpack strings decode as Go strings.

---------

Co-authored-by: AmirHossein Fallah <amirhossein.fallah@arvancloud.ir>
2026-02-18 00:38:36 +08:00
Ruben de Vries
a7b757e338
add a thread safe context that can be used when UseInternalContext is enabled. 2026-01-21 13:12:08 +01:00
7 changed files with 317 additions and 43 deletions

View File

@ -5,6 +5,7 @@
package gin
import (
"context"
"errors"
"fmt"
"io"
@ -94,6 +95,10 @@ type Context struct {
// SameSite allows a server to define a cookie attribute making it impossible for
// the browser to send this cookie along with cross-site requests.
sameSite http.SameSite
internalContextMu sync.RWMutex
internalContext context.Context
internalContextCancelCause context.CancelCauseFunc
}
/************************************/
@ -115,6 +120,10 @@ func (c *Context) reset() {
c.sameSite = 0
*c.params = (*c.params)[:0]
*c.skippedNodes = (*c.skippedNodes)[:0]
if c.useInternalContext() {
c.WithInternalContext(context.Background())
}
}
// Copy returns a copy of the current context that can be safely used outside the request's scope.
@ -1429,6 +1438,49 @@ func (c *Context) SetAccepted(formats ...string) {
/***** GOLANG.ORG/X/NET/CONTEXT *****/
/************************************/
// WithInternalContext replaces the internal context stored with the provided one in a thread safe manner.
// It's important that any context you pass in is not something the wraps *gin.Context,
// if you want to wrap a context and then provide it to WithInternalContext, use InternalContext().
// If you don't plan to provide the context back to WithInternalContext you can safely use *Context directly.
// Otherwise you'll end up with a stack overflow.
//
// For example:
// var c *Context // given a context
// // you can safely wrap it and pass it downstream
// myDownstreamFunction(context.WithValue(c, ...))
//
// // but when you want to call WithInternalContext you should do it like this
// c.WithInternalContext(context.WithValue(c.InternalContext(), ...))
func (c *Context) WithInternalContext(ctx context.Context) {
if !c.useInternalContext() {
panic("Can't use WithInternalContext when UseInternalContext is false")
}
c.internalContextMu.Lock()
defer c.internalContextMu.Unlock()
c.internalContext, c.internalContextCancelCause = context.WithCancelCause(ctx)
}
// InternalContext provides the currently stored internal context in a thread safe manner.
// Use this if you want to wrap a context.Context which you'll end up providing to WithInternalContext.
// If you don't plan to provide the context back to WithInternalContext you can safely use *Context directly.
func (c *Context) InternalContext() context.Context {
if !c.useInternalContext() {
panic("Can't use InternalContext when UseInternalContext is false")
}
c.internalContextMu.RLock()
defer c.internalContextMu.RUnlock()
return c.internalContext
}
// hasRequestContext returns whether c.Request has Context and fallback.
func (c *Context) useInternalContext() bool {
return c.engine != nil && c.engine.UseInternalContext
}
// hasRequestContext returns whether c.Request has Context and fallback.
func (c *Context) hasRequestContext() bool {
hasFallback := c.engine != nil && c.engine.ContextWithFallback
@ -1438,26 +1490,44 @@ func (c *Context) hasRequestContext() bool {
// Deadline returns that there is no deadline (ok==false) when c.Request has no Context.
func (c *Context) Deadline() (deadline time.Time, ok bool) {
if !c.hasRequestContext() {
return
if c.useInternalContext() {
c.internalContextMu.RLock()
defer c.internalContextMu.RUnlock()
return c.internalContext.Deadline()
} else if c.hasRequestContext() {
return c.Request.Context().Deadline()
}
return c.Request.Context().Deadline()
return
}
// Done returns nil (chan which will wait forever) when c.Request has no Context.
func (c *Context) Done() <-chan struct{} {
if !c.hasRequestContext() {
return nil
if c.useInternalContext() {
c.internalContextMu.RLock()
defer c.internalContextMu.RUnlock()
return c.internalContext.Done()
} else if c.hasRequestContext() {
return c.Request.Context().Done()
}
return c.Request.Context().Done()
return nil
}
// Err returns nil when c.Request has no Context.
func (c *Context) Err() error {
if !c.hasRequestContext() {
return nil
if c.useInternalContext() {
c.internalContextMu.RLock()
defer c.internalContextMu.RUnlock()
return c.internalContext.Err()
} else if c.hasRequestContext() {
return c.Request.Context().Err()
}
return c.Request.Context().Err()
return nil
}
// Value returns the value associated with this context for key, or nil
@ -1475,8 +1545,14 @@ func (c *Context) Value(key any) any {
return val
}
}
if !c.hasRequestContext() {
return nil
if c.useInternalContext() {
c.internalContextMu.RLock()
defer c.internalContextMu.RUnlock()
return c.internalContext.Value(key)
} else if c.hasRequestContext() {
return c.Request.Context().Value(key)
}
return c.Request.Context().Value(key)
return nil
}

View File

@ -3239,6 +3239,142 @@ func TestContextWithFallbackValueFromRequestContext(t *testing.T) {
}
}
func TestContextUseInternalContextDeadline(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder(), func(c *Context) {
// enable UseInternalContext feature flag
c.engine.UseInternalContext = true
})
deadline, ok := c.Deadline()
assert.Zero(t, deadline)
assert.False(t, ok)
c2, _ := CreateTestContext(httptest.NewRecorder(), func(c *Context) {
// enable UseInternalContext feature flag
c.engine.UseInternalContext = true
})
d := time.Now().Add(time.Second)
ctx, cancel := context.WithDeadline(context.Background(), d)
defer cancel()
c2.WithInternalContext(ctx)
deadline, ok = c2.Deadline()
assert.Equal(t, d, deadline)
assert.True(t, ok)
}
func TestContextUseInternalContextDone(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder(), func(c *Context) {
// enable UseInternalContext feature flag
c.engine.UseInternalContext = true
})
assert.Nil(t, c.Done())
c2, _ := CreateTestContext(httptest.NewRecorder(), func(c *Context) {
// enable UseInternalContext feature flag
c.engine.UseInternalContext = true
})
ctx, cancel := context.WithCancel(context.Background())
c2.WithInternalContext(ctx)
cancel()
assert.NotNil(t, <-c2.Done())
}
func TestContextUseInternalContextErr(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder(), func(c *Context) {
// enable UseInternalContext feature flag
c.engine.UseInternalContext = true
})
require.NoError(t, c.Err())
c2, _ := CreateTestContext(httptest.NewRecorder(), func(c *Context) {
// enable UseInternalContext feature flag
c.engine.UseInternalContext = true
})
ctx, cancel := context.WithCancel(context.Background())
c2.WithInternalContext(ctx)
cancel()
assert.EqualError(t, c2.Err(), context.Canceled.Error())
}
func TestContextUseInternalContextValue(t *testing.T) {
type contextKey string
tests := []struct {
name string
getContextAndKey func() (*Context, any)
value any
}{
{
name: "c with struct context key",
getContextAndKey: func() (*Context, any) {
type KeyStruct struct{} // https://staticcheck.dev/docs/checks/#SA1029
var key KeyStruct
c, _ := CreateTestContext(httptest.NewRecorder(), func(c *Context) {
// enable UseInternalContext feature flag
c.engine.UseInternalContext = true
})
c.WithInternalContext(context.WithValue(context.TODO(), key, "value"))
return c, key
},
value: "value",
},
{
name: "c with struct context key and request context with different value",
getContextAndKey: func() (*Context, any) {
type KeyStruct struct{} // https://staticcheck.dev/docs/checks/#SA1029
var key KeyStruct
c, _ := CreateTestContext(httptest.NewRecorder(), func(c *Context) {
// enable UseInternalContext feature flag
c.engine.UseInternalContext = true
// enable ContextWithFallback feature flag
c.engine.ContextWithFallback = true
c.Request, _ = http.NewRequest(http.MethodPost, "/", nil)
})
c.WithInternalContext(context.WithValue(context.TODO(), key, "value"))
c.Request = c.Request.WithContext(context.WithValue(context.TODO(), key, "other value"))
return c, key
},
value: "value",
},
{
name: "c with string context key",
getContextAndKey: func() (*Context, any) {
c, _ := CreateTestContext(httptest.NewRecorder(), func(c *Context) {
// enable UseInternalContext feature flag
c.engine.UseInternalContext = true
})
c.WithInternalContext(context.WithValue(context.TODO(), contextKey("key"), "value"))
return c, contextKey("key")
},
value: "value",
},
{
name: "c with background internal context",
getContextAndKey: func() (*Context, any) {
c, _ := CreateTestContext(httptest.NewRecorder(), func(c *Context) {
// enable UseInternalContext feature flag
c.engine.UseInternalContext = true
})
c.WithInternalContext(context.Background())
return c, "key"
},
value: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c, key := tt.getContextAndKey()
assert.Equal(t, tt.value, c.Value(key))
})
}
}
func TestContextCopyShouldNotCancel(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)

25
gin.go
View File

@ -169,9 +169,14 @@ type Engine struct {
// UseH2C enable h2c support.
UseH2C bool
// ContextWithFallback enable fallback Context.Deadline(), Context.Done(), Context.Err() and Context.Value() when Context.Request.Context() is not nil.
// ContextWithFallback enable fallback Context.Deadline(), Context.Done(), Context.Err() and Context.Value()
// through Context.Request when Context.Request.Context() is not nil.
ContextWithFallback bool
// UseInternalContext enable fallback Context.Deadline(), Context.Done(), Context.Err()
// through InternalContext and supersedes ContextWithFallback
UseInternalContext bool
delims render.Delims
secureJSONPrefix string
HTMLRender render.HTMLRender
@ -669,6 +674,24 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c.Request = req
c.reset()
// If we're using internalContext then we need to pass on errors from the request context
if c.useInternalContext() {
reqCtx := req.Context()
// we need to get the cancelCause function now, so that we have the function for this request
// because the c is a pointer to a Context that will possibly go back into the pool and be reused
c.internalContextMu.RLock()
cancelCause := c.internalContextCancelCause
c.internalContextMu.RUnlock()
go func() {
<-reqCtx.Done()
if err := reqCtx.Err(); err != nil {
cancelCause(err)
}
}()
}
engine.handleHTTPRequest(c)
engine.pool.Put(c)

12
go.mod
View File

@ -5,7 +5,7 @@ go 1.24.0
toolchain go1.24.7
require (
github.com/bytedance/sonic v1.14.2
github.com/bytedance/sonic v1.15.0
github.com/gin-contrib/sse v1.1.0
github.com/go-playground/validator/v10 v10.28.0
github.com/goccy/go-json v0.10.5
@ -18,7 +18,7 @@ require (
github.com/stretchr/testify v1.11.1
github.com/ugorji/go/codec v1.3.1
go.mongodb.org/mongo-driver v1.17.9
golang.org/x/net v0.49.0
golang.org/x/net v0.50.0
google.golang.org/protobuf v1.36.10
)
@ -26,7 +26,7 @@ require gopkg.in/yaml.v3 v3.0.1 // indirect
require (
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/bytedance/sonic/loader v0.5.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.10 // indirect
@ -41,7 +41,7 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
go.uber.org/mock v0.6.0 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
)

24
go.sum
View File

@ -1,9 +1,9 @@
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@ -77,15 +77,15 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

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

View File

@ -14,9 +14,12 @@ import (
// 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) {
func CreateTestContext(w http.ResponseWriter, opts ...func(c *Context)) (c *Context, r *Engine) {
r = New()
c = r.allocateContext(0)
for _, opt := range opts {
opt(c)
}
c.reset()
c.writermem.reset(w)
return