Compare commits

...

9 Commits

Author SHA1 Message Date
unbyte
c0b9f3b4d8
Merge 96f63d68d3086cd0883f77ac39c5cc73aedd530d into 19b877fa50cbbb9282763099fb177a1e5cc5c850 2025-12-06 10:55:06 +08:00
OHZEKI Naoki
19b877fa50
test(debug): improve the test coverage of debug.go to 100% (#4404) 2025-12-05 11:18:08 +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
dependabot[bot]
fad706f121
chore(deps): bump github.com/goccy/go-yaml from 1.18.0 to 1.19.0 (#4458)
Bumps [github.com/goccy/go-yaml](https://github.com/goccy/go-yaml) from 1.18.0 to 1.19.0.
- [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.18.0...v1.19.0)

---
updated-dependencies:
- dependency-name: github.com/goccy/go-yaml
  dependency-version: 1.19.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>
2025-12-02 20:09:41 +08:00
Bo-Yi Wu
96f63d68d3
Merge branch 'master' into fix-cors 2025-05-11 22:36:26 +08:00
unbyte
5f94d6f3e8
Merge branch 'master' into fix-cors 2021-12-28 15:15:08 +08:00
unbyte
51ef7a6a10
Merge branch 'master' into fix-cors 2021-11-02 15:12:48 +08:00
Helios
22b88e0ed1 AutoRedirect API to handle before auto redirection 2020-08-23 00:41:04 +08:00
9 changed files with 176 additions and 24 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

@ -13,7 +13,9 @@ import (
"sync/atomic"
)
const ginSupportMinGoVer = 23
const ginSupportMinGoVer = 24
var runtimeVersion = runtime.Version()
// IsDebugging returns true if the framework is running in debug mode.
// Use SetMode(gin.ReleaseMode) to disable debug mode.
@ -77,7 +79,7 @@ func getMinVer(v string) (uint64, error) {
}
func debugPrintWARNINGDefault() {
if v, e := getMinVer(runtime.Version()); e == nil && v < ginSupportMinGoVer {
if v, e := getMinVer(runtimeVersion); e == nil && v < ginSupportMinGoVer {
debugPrint(`[WARNING] Now Gin requires Go 1.24+.
`)

View File

@ -12,7 +12,6 @@ import (
"log"
"net/http"
"os"
"runtime"
"strings"
"sync"
"testing"
@ -21,10 +20,6 @@ import (
"github.com/stretchr/testify/require"
)
// TODO
// func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) {
// func debugPrint(format string, values ...any) {
func TestIsDebugging(t *testing.T) {
SetMode(DebugMode)
assert.True(t, IsDebugging())
@ -48,6 +43,18 @@ func TestDebugPrint(t *testing.T) {
assert.Equal(t, "[GIN-debug] these are 2 error messages\n", re)
}
func TestDebugPrintFunc(t *testing.T) {
DebugPrintFunc = func(format string, values ...any) {
fmt.Fprintf(DefaultWriter, "[GIN-debug] "+format, values...)
}
re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrint("debug print func test: %d", 123)
SetMode(TestMode)
})
assert.Regexp(t, `^\[GIN-debug\] debug print func test: 123`, re)
}
func TestDebugPrintError(t *testing.T) {
re := captureOutput(t, func() {
SetMode(DebugMode)
@ -104,12 +111,17 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
debugPrintWARNINGDefault()
SetMode(TestMode)
})
m, e := getMinVer(runtime.Version())
if e == nil && m < ginSupportMinGoVer {
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.24+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} else {
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
}
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
}
func TestDebugPrintWARNINGDefaultWithUnsupportedVersion(t *testing.T) {
runtimeVersion = "go1.23.12"
re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrintWARNINGDefault()
SetMode(TestMode)
})
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.24+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
}
func TestDebugPrintWARNINGNew(t *testing.T) {

22
gin.go
View File

@ -178,8 +178,10 @@ type Engine struct {
FuncMap template.FuncMap
allNoRoute HandlersChain
allNoMethod HandlersChain
allAutoRedirect HandlersChain
noRoute HandlersChain
noMethod HandlersChain
autoRedirect HandlersChain
pool sync.Pool
trees methodTrees
maxParams uint16
@ -334,6 +336,13 @@ func (engine *Engine) NoMethod(handlers ...HandlerFunc) {
engine.rebuild405Handlers()
}
// AutoRedirect sets the handlers called when auto redirected
// (RedirectTrailingSlash and RedirectFixedPath)
func (engine *Engine) AutoRedirect(handlers ...HandlerFunc) {
engine.autoRedirect = handlers
engine.rebuildAutoRedirectHandlers()
}
// Use attaches a global middleware to the router. i.e. the middleware attached through Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware.
@ -341,6 +350,7 @@ func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
engine.rebuildAutoRedirectHandlers()
return engine
}
@ -361,6 +371,10 @@ func (engine *Engine) rebuild405Handlers() {
engine.allNoMethod = engine.combineHandlers(engine.noMethod)
}
func (engine *Engine) rebuildAutoRedirectHandlers() {
engine.allAutoRedirect = engine.combineHandlers(engine.autoRedirect)
}
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
assert1(path[0] == '/', "path must begin with '/'")
assert1(method != "", "HTTP method can not be empty")
@ -724,6 +738,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
return
}
if httpMethod != http.MethodConnect && rPath != "/" {
c.handlers = engine.allAutoRedirect
if value.tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
return
@ -819,13 +834,14 @@ func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
func redirectRequest(c *Context) {
req := c.Request
rPath := req.URL.Path
rURL := req.URL.String()
code := http.StatusMovedPermanently // Permanent redirect, request with GET method
if req.Method != http.MethodGet {
code = http.StatusTemporaryRedirect
}
c.Next()
rPath := req.URL.Path
rURL := req.URL.String()
debugPrint("redirecting request %d: %s --> %s", code, rPath, rURL)
http.Redirect(c.Writer, req, rURL, code)
c.writermem.WriteHeaderNow()

View File

@ -601,6 +601,59 @@ func TestNoMethodWithGlobalHandlers(t *testing.T) {
compareFunc(t, router.allNoMethod[2], middleware0)
}
func TestAutoRedirectWithoutGlobalHandlers(t *testing.T) {
var middleware0 HandlerFunc = func(c *Context) {}
var middleware1 HandlerFunc = func(c *Context) {}
router := New()
router.AutoRedirect(middleware0)
assert.Nil(t, router.Handlers)
assert.Len(t, router.autoRedirect, 1)
assert.Len(t, router.allAutoRedirect, 1)
compareFunc(t, router.autoRedirect[0], middleware0)
compareFunc(t, router.allAutoRedirect[0], middleware0)
router.AutoRedirect(middleware1, middleware0)
assert.Len(t, router.autoRedirect, 2)
assert.Len(t, router.allAutoRedirect, 2)
compareFunc(t, router.autoRedirect[0], middleware1)
compareFunc(t, router.allAutoRedirect[0], middleware1)
compareFunc(t, router.autoRedirect[1], middleware0)
compareFunc(t, router.allAutoRedirect[1], middleware0)
}
func TestAutoRedirectWithGlobalHandlers(t *testing.T) {
var middleware0 HandlerFunc = func(c *Context) {}
var middleware1 HandlerFunc = func(c *Context) {}
var middleware2 HandlerFunc = func(c *Context) {}
router := New()
router.Use(middleware2)
router.AutoRedirect(middleware0)
assert.Len(t, router.allAutoRedirect, 2)
assert.Len(t, router.Handlers, 1)
assert.Len(t, router.autoRedirect, 1)
compareFunc(t, router.Handlers[0], middleware2)
compareFunc(t, router.autoRedirect[0], middleware0)
compareFunc(t, router.allAutoRedirect[0], middleware2)
compareFunc(t, router.allAutoRedirect[1], middleware0)
router.Use(middleware1)
assert.Len(t, router.allAutoRedirect, 3)
assert.Len(t, router.Handlers, 2)
assert.Len(t, router.autoRedirect, 1)
compareFunc(t, router.Handlers[0], middleware2)
compareFunc(t, router.Handlers[1], middleware1)
compareFunc(t, router.autoRedirect[0], middleware0)
compareFunc(t, router.allAutoRedirect[0], middleware2)
compareFunc(t, router.allAutoRedirect[1], middleware1)
compareFunc(t, router.allAutoRedirect[2], middleware0)
}
func compareFunc(t *testing.T, a, b any) {
sf1 := reflect.ValueOf(a)
sf2 := reflect.ValueOf(b)

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.18.0
github.com/goccy/go-yaml v1.19.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

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.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
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/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

@ -273,6 +273,27 @@ func TestRouteRedirectFixedPath(t *testing.T) {
assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
}
func TestRouteRedirectWithHandler(t *testing.T) {
router := New()
router.RedirectTrailingSlash = true
router.GET("/path", func(c *Context) {})
passed := []bool{false, false}
router.Use(func(c *Context) {
passed[0] = true
c.Next()
})
router.AutoRedirect(func(c *Context) {
passed[1] = true
c.Next()
})
w := performRequest(router, http.MethodGet, "/path/")
assert.Equal(t, "/path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code)
assert.True(t, passed[0])
assert.True(t, passed[1])
}
// TestContextParamsGet tests that a parameter can be parsed from the URL.
func TestRouteParamsByName(t *testing.T) {
name := ""