Compare commits

...

4 Commits

Author SHA1 Message Date
Michelle
def8ff788b
Merge d905a35c2108c13ac240ffb4e3b7c5f0c2343b46 into 2a794cd0b0faa7d829291375b27a3467ea972b0d 2025-12-03 19:54:01 -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
Michelle Laurenti
d905a35c21 feat(form): support BindUnmarshaler slice elements
gin-gonic#4312
2025-07-23 12:02:14 +02:00
3 changed files with 117 additions and 7 deletions

View File

@ -184,8 +184,15 @@ type BindUnmarshaler interface {
// If the value implements the BindUnmarshaler interface, it will be used to set the value, we will return `true`
// to skip the default value setting.
func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
switch v := value.Addr().Interface().(type) {
if value.Kind() != reflect.Ptr {
value = value.Addr()
}
switch value.Interface().(type) {
case BindUnmarshaler:
if !value.Elem().IsValid() {
value.Set(reflect.New(value.Type().Elem()))
}
v := value.Interface().(BindUnmarshaler)
return true, v.UnmarshalParam(val)
}
return false, nil
@ -300,6 +307,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 +416,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 +444,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
@ -456,6 +468,13 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
func setArray(vals []string, value reflect.Value, field reflect.StructField) error {
for i, s := range vals {
if ok, err := trySetCustom(s, value.Index(i)); ok {
if err != nil {
return err
} else {
continue
}
}
err := setWithProperType(s, value.Index(i), field)
if err != nil {
return err
@ -475,6 +494,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)
@ -550,6 +589,54 @@ func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) {
assert.Equal(t, "happiness", s.FileData.Name)
}
func TestMappingCustomStructTypeSliceWithFormTag(t *testing.T) {
var s struct {
FileData []customUnmarshalParamType `form:"data"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`, `http:/bar:sadness`}}, "form")
require.NoError(t, err)
assert.Equal(t, "file", s.FileData[0].Protocol)
assert.Equal(t, "/foo", s.FileData[0].Path)
assert.Equal(t, "happiness", s.FileData[0].Name)
assert.Equal(t, "http", s.FileData[1].Protocol)
assert.Equal(t, "/bar", s.FileData[1].Path)
assert.Equal(t, "sadness", s.FileData[1].Name)
}
func TestMappingCustomStructTypeSliceWithURITag(t *testing.T) {
var s struct {
FileData []customUnmarshalParamType `uri:"data"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`, `http:/bar:sadness`}}, "uri")
require.NoError(t, err)
assert.Equal(t, "file", s.FileData[0].Protocol)
assert.Equal(t, "/foo", s.FileData[0].Path)
assert.Equal(t, "happiness", s.FileData[0].Name)
assert.Equal(t, "http", s.FileData[1].Protocol)
assert.Equal(t, "/bar", s.FileData[1].Path)
assert.Equal(t, "sadness", s.FileData[1].Name)
}
func TestMappingCustomPointerStructTypeSliceWithFormTag(t *testing.T) {
var s struct {
FileData []*customUnmarshalParamType `form:"data"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`, `http:/bar:sadness`}}, "form")
require.NoError(t, err)
assert.Equal(t, "file", s.FileData[0].Protocol)
assert.Equal(t, "/foo", s.FileData[0].Path)
assert.Equal(t, "happiness", s.FileData[0].Name)
assert.Equal(t, "http", s.FileData[1].Protocol)
assert.Equal(t, "/bar", s.FileData[1].Path)
assert.Equal(t, "sadness", s.FileData[1].Name)
}
type customPath []string
func (p *customPath) UnmarshalParam(param string) error {

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.