Compare commits

...

5 Commits

Author SHA1 Message Date
Ruben de Vries
b9dcc5fb5d
Merge a7b757e33830094fcb2af6156cb963a96e87a47b into db309081bc5c137b2aa15701ef53f7f19788da25 2026-02-28 00:57:19 +08:00
Jacob McSwain
db309081bc
chore(logger): allow skipping query string output (#4547)
This is useful for APIs that might have sensitive information in the query string, such as API keys.

This patch does not change the default behavior of the code unless the new `SkipQueryString` config option is passed in.

The "skip" term is a bit of a misnomer here, as this doesn't actually skip that log, but modifies the output. I'm open to suggestions for a more appropriate name.

Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-02-27 23:33:46 +08:00
Bob Du
ba093d1947
chore(binding): upgrade bson dependency to mongo-driver v2 (#4549)
Signed-off-by: Bob Du <i@bobdu.cc>
2026-02-27 23:20:01 +08:00
dependabot[bot]
1b414bd54e
chore(deps): bump goreleaser/goreleaser-action in the actions group (#4546)
Bumps the actions group with 1 update: [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action).


Updates `goreleaser/goreleaser-action` from 6 to 7
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 23:18:28 +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
14 changed files with 296 additions and 24 deletions

View File

@ -21,7 +21,7 @@ jobs:
with:
go-version: "^1"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
uses: goreleaser/goreleaser-action@v7
with:
# either 'goreleaser' (default) or 'goreleaser-pro'
distribution: goreleaser

View File

@ -21,7 +21,7 @@ import (
"github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/v2/bson"
"google.golang.org/protobuf/proto"
)

View File

@ -8,7 +8,7 @@ import (
"io"
"net/http"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/v2/bson"
)
type bsonBinding struct{}

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

@ -32,7 +32,7 @@ import (
testdata "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/v2/bson"
"google.golang.org/protobuf/proto"
)
@ -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)

View File

@ -22,6 +22,7 @@
- [How to write log file](#how-to-write-log-file)
- [Custom Log Format](#custom-log-format)
- [Controlling Log output coloring](#controlling-log-output-coloring)
- [Avoid logging query strings](#avoid-loging-query-strings)
- [Model binding and validation](#model-binding-and-validation)
- [Custom Validators](#custom-validators)
- [Only Bind Query String](#only-bind-query-string)
@ -592,6 +593,20 @@ func main() {
}
```
### Avoid logging query strings
```go
func main() {
router := gin.New()
// SkipQueryString indicates that the logger should not log the query string.
// For example, /path?q=1 will be logged as /path
loggerConfig := gin.LoggerConfig{SkipQueryString: true}
router.Use(gin.LoggerWithConfig(loggerConfig))
}
```
### Model binding and validation
To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML, TOML and standard form values (foo=bar&boo=baz).

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

@ -17,7 +17,7 @@ require (
github.com/quic-go/quic-go v0.59.0
github.com/stretchr/testify v1.11.1
github.com/ugorji/go/codec v1.3.1
go.mongodb.org/mongo-driver v1.17.9
go.mongodb.org/mongo-driver/v2 v2.5.0
golang.org/x/net v0.50.0
google.golang.org/protobuf v1.36.10
)

4
go.sum
View File

@ -71,8 +71,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
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=

View File

@ -48,6 +48,11 @@ type LoggerConfig struct {
// Optional.
SkipPaths []string
// SkipQueryString indicates that query strings should not be written
// for cases such as when API keys are passed via query strings.
// Optional. Default value is false.
SkipQueryString bool
// Skip is a Skipper that indicates which logs should not be written.
// Optional.
Skip Skipper
@ -298,7 +303,7 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
param.BodySize = c.Writer.Size()
if raw != "" {
if raw != "" && !conf.SkipQueryString {
path = path + "?" + raw
}

View File

@ -471,3 +471,17 @@ func TestForceConsoleColor(t *testing.T) {
// reset console color mode.
consoleColorMode = autoColor
}
func TestLoggerWithConfigSkipQueryString(t *testing.T) {
buffer := new(strings.Builder)
router := New()
router.Use(LoggerWithConfig(LoggerConfig{
Output: buffer,
SkipQueryString: true,
}))
router.GET("/logged", func(c *Context) { c.Status(http.StatusOK) })
PerformRequest(router, "GET", "/logged?a=21")
assert.Contains(t, buffer.String(), "200")
assert.NotContains(t, buffer.String(), "a=21")
}

View File

@ -7,7 +7,7 @@ package render
import (
"net/http"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/v2/bson"
)
// BSON contains the given interface object.

View File

@ -19,7 +19,7 @@ import (
testdata "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/v2/bson"
"google.golang.org/protobuf/proto"
)

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