Compare commits

...

4 Commits

Author SHA1 Message Date
Ruben de Vries
376a25726e
Merge a7b757e33830094fcb2af6156cb963a96e87a47b into d9e5cdf9c6f9c1643be6e081516469c71645d93d 2026-01-24 19:55:09 +08:00
dependabot[bot]
d9e5cdf9c6
chore(deps): bump github.com/goccy/go-yaml from 1.19.0 to 1.19.1 (#4476)
Bumps [github.com/goccy/go-yaml](https://github.com/goccy/go-yaml) from 1.19.0 to 1.19.1.
- [Release notes](https://github.com/goccy/go-yaml/releases)
- [Changelog](https://github.com/goccy/go-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/goccy/go-yaml/compare/v1.19.0...v1.19.1)

---
updated-dependencies:
- dependency-name: github.com/goccy/go-yaml
  dependency-version: 1.19.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-24 17:55:09 +08:00
Raju Ahmed
53410d2e07
feat(context): add GetError and GetErrorSlice methods for error retrieval (#4502)
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-01-24 17:54:37 +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
6 changed files with 281 additions and 17 deletions

View File

@ -5,6 +5,7 @@
package gin
import (
"context"
"errors"
"fmt"
"io"
@ -93,6 +94,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
}
/************************************/
@ -114,6 +119,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.
@ -386,6 +395,11 @@ func (c *Context) GetDuration(key any) time.Duration {
return getTyped[time.Duration](c, key)
}
// GetError returns the value associated with the key as an error.
func (c *Context) GetError(key any) error {
return getTyped[error](c, key)
}
// GetIntSlice returns the value associated with the key as a slice of integers.
func (c *Context) GetIntSlice(key any) []int {
return getTyped[[]int](c, key)
@ -451,6 +465,11 @@ func (c *Context) GetStringSlice(key any) []string {
return getTyped[[]string](c, key)
}
// GetErrorSlice returns the value associated with the key as a slice of errors.
func (c *Context) GetErrorSlice(key any) []error {
return getTyped[[]error](c, key)
}
// GetStringMap returns the value associated with the key as a map of interfaces.
func (c *Context) GetStringMap(key any) map[string]any {
return getTyped[map[string]any](c, key)
@ -1408,6 +1427,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
@ -1417,26 +1479,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
@ -1454,8 +1534,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

@ -516,6 +516,14 @@ func TestContextGetDuration(t *testing.T) {
assert.Equal(t, time.Second, c.GetDuration("duration"))
}
func TestContextGetError(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
key := "error"
value := errors.New("test error")
c.Set(key, value)
assert.Equal(t, value, c.GetError(key))
}
func TestContextGetIntSlice(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
key := "int-slice"
@ -618,6 +626,14 @@ func TestContextGetStringSlice(t *testing.T) {
assert.Equal(t, []string{"foo"}, c.GetStringSlice("slice"))
}
func TestContextGetErrorSlice(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
key := "error-slice"
value := []error{errors.New("error1"), errors.New("error2")}
c.Set(key, value)
assert.Equal(t, value, c.GetErrorSlice(key))
}
func TestContextGetStringMap(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
m := make(map[string]any)
@ -3205,6 +3221,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)

2
go.mod
View File

@ -7,7 +7,7 @@ require (
github.com/gin-contrib/sse v1.1.0
github.com/go-playground/validator/v10 v10.28.0
github.com/goccy/go-json v0.10.2
github.com/goccy/go-yaml v1.19.0
github.com/goccy/go-yaml v1.19.1
github.com/json-iterator/go v1.1.12
github.com/mattn/go-isatty v0.0.20
github.com/modern-go/reflect2 v1.0.2

4
go.sum
View File

@ -24,8 +24,8 @@ github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=

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