Compare commits

...

4 Commits

Author SHA1 Message Date
Ruben de Vries
3c55cddadd
Merge 4218569663d3d23c7004581f86fa2018ae668be0 into 2a794cd0b0faa7d829291375b27a3467ea972b0d 2025-12-03 19:54:13 -08:00
OHZEKI Naoki
2a794cd0b0
fix(debug): version mismatch (#4403)
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-04 10:49:37 +08:00
guonaihong
b917b14ff9
fix(binding): empty value error (#2169)
* fix empty value error

Here is the code that can report an error
```go
package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"io"
	"net/http"
	"os"
	"time"
)

type header struct {
	Duration   time.Duration `header:"duration"`
	CreateTime time.Time     `header:"createTime" time_format:"unix"`
}

func needFix1() {
	g := gin.Default()
	g.GET("/", func(c *gin.Context) {
		h := header{}
		err := c.ShouldBindHeader(&h)
		if err != nil {
			c.JSON(500, fmt.Sprintf("fail:%s\n", err))
			return
		}

		c.JSON(200, h)
	})

	g.Run(":8081")
}

func needFix2() {
	g := gin.Default()
	g.GET("/", func(c *gin.Context) {
		h := header{}
		err := c.ShouldBindHeader(&h)
		if err != nil {
			c.JSON(500, fmt.Sprintf("fail:%s\n", err))
			return
		}

		c.JSON(200, h)
	})

	g.Run(":8082")
}

func sendNeedFix1() {
	// send to needFix1
	sendBadData("http://127.0.0.1:8081", "duration")
}

func sendNeedFix2() {
	// send to needFix2
	sendBadData("http://127.0.0.1:8082", "createTime")
}

func sendBadData(url, key string) {
	req, err := http.NewRequest("GET", "http://127.0.0.1:8081", nil)
	if err != nil {
		fmt.Printf("err:%s\n", err)
		return
	}

	// Only the key and no value can cause an error
	req.Header.Add(key, "")
	rsp, err := http.DefaultClient.Do(req)
	if err != nil {
		return
	}
	io.Copy(os.Stdout, rsp.Body)
	rsp.Body.Close()
}

func main() {
	go needFix1()
	go needFix2()

	time.Sleep(time.Second / 1000 * 200) // 200ms
	sendNeedFix1()
	sendNeedFix2()
}

```

* modify code

* add comment

* test(binding): use 'any' alias and require.NoError in form mapping tests

- Replace 'interface{}' with 'any' alias in bindTestData struct
- Change assert.NoError to require.NoError in TestMappingTimeUnixNano and TestMappingTimeDuration to fail fast on mapping errors

---------

Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-03 19:18:10 +08:00
Ruben de Vries
4218569663
add a thread safe context that can be used when UseInternalContext is enabled. 2025-10-28 11:42:23 +01:00
7 changed files with 287 additions and 20 deletions

View File

@ -300,6 +300,11 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
}
func setWithProperType(val string, value reflect.Value, field reflect.StructField) error {
// If it is a string type, no spaces are removed, and the user data is not modified here
if value.Kind() != reflect.String {
val = strings.TrimSpace(val)
}
switch value.Kind() {
case reflect.Int:
return setIntField(val, 0, value)
@ -404,6 +409,11 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
timeFormat = time.RFC3339
}
if val == "" {
value.Set(reflect.ValueOf(time.Time{}))
return nil
}
switch tf := strings.ToLower(timeFormat); tf {
case "unix", "unixmilli", "unixmicro", "unixnano":
tv, err := strconv.ParseInt(val, 10, 64)
@ -427,11 +437,6 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
return nil
}
if val == "" {
value.Set(reflect.ValueOf(time.Time{}))
return nil
}
l := time.Local
if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC {
l = time.UTC
@ -475,6 +480,10 @@ func setSlice(vals []string, value reflect.Value, field reflect.StructField) err
}
func setTimeDuration(val string, value reflect.Value) error {
if val == "" {
val = "0"
}
d, err := time.ParseDuration(val)
if err != nil {
return err

View File

@ -226,7 +226,35 @@ func TestMappingTime(t *testing.T) {
require.Error(t, err)
}
type bindTestData struct {
need any
got any
in map[string][]string
}
func TestMappingTimeUnixNano(t *testing.T) {
type needFixUnixNanoEmpty struct {
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
}
// ok
tests := []bindTestData{
{need: &needFixUnixNanoEmpty{}, got: &needFixUnixNanoEmpty{}, in: formSource{"createTime": []string{" "}}},
{need: &needFixUnixNanoEmpty{}, got: &needFixUnixNanoEmpty{}, in: formSource{"createTime": []string{}}},
}
for _, v := range tests {
err := mapForm(v.got, v.in)
require.NoError(t, err)
assert.Equal(t, v.need, v.got)
}
}
func TestMappingTimeDuration(t *testing.T) {
type needFixDurationEmpty struct {
Duration time.Duration `form:"duration"`
}
var s struct {
D time.Duration
}
@ -236,6 +264,17 @@ func TestMappingTimeDuration(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 5*time.Second, s.D)
// ok
tests := []bindTestData{
{need: &needFixDurationEmpty{}, got: &needFixDurationEmpty{}, in: formSource{"duration": []string{" "}}},
{need: &needFixDurationEmpty{}, got: &needFixDurationEmpty{}, in: formSource{"duration": []string{}}},
}
for _, v := range tests {
err := mapForm(v.got, v.in)
require.NoError(t, err)
assert.Equal(t, v.need, v.got)
}
// error
err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form")
require.Error(t, err)

View File

@ -5,6 +5,7 @@
package gin
import (
"context"
"errors"
"fmt"
"io"
@ -101,6 +102,9 @@ 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
}
/************************************/
@ -122,6 +126,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.
@ -1402,6 +1410,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 = 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
@ -1411,26 +1462,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
@ -1448,8 +1517,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

@ -3164,6 +3164,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

@ -13,7 +13,7 @@ import (
"sync/atomic"
)
const ginSupportMinGoVer = 23
const ginSupportMinGoVer = 24
// IsDebugging returns true if the framework is running in debug mode.
// Use SetMode(gin.ReleaseMode) to disable debug mode.

7
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

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