validator icon indicating copy to clipboard operation
validator copied to clipboard

[Bug]: 使用v10.26.0版本导致了应用程序的崩溃

Open ccpwcn opened this issue 8 months ago • 13 comments

What happened?

当我使用 v10.26.0 的时候,验证参数,导致了下面的崩溃:

2025/04/03 08:28:01 [Recovery] 2025/04/03 - 08:28:01 panic recovered:
POST /canteen/orderItem/multiple HTTP/1.1
Host: localhost:8586
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Authorization: *
Connection: close
Content-Length: 54
Content-Type: application/json
Referer: http://localhost:9281/h5/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1 HBuilderX
X-Forwarded-For: 127.0.0.1
X-Forwarded-Host: localhost:9281
X-Forwarded-Port: 9281
X-Forwarded-Proto: http


Bad field type param.OrderItem
E:/dependencies/go/pkg/mod/github.com/go-playground/validator/[email protected]/baked_in.go:2192 (0x14629d2)
	isGte: panic(fmt.Sprintf("Bad field type %T", field.Interface()))
E:/dependencies/go/pkg/mod/github.com/go-playground/validator/[email protected]/baked_in.go:2285 (0x14635cb)
	hasMinOf: return isGte(fl)
E:/dependencies/go/pkg/mod/github.com/go-playground/validator/[email protected]/baked_in.go:44 (0x1450ce3)
	wrapFunc.func1: return fn(fl)
E:/dependencies/go/pkg/mod/github.com/go-playground/validator/[email protected]/validator.go:473 (0x14765e1)
	(*validate).traverseField: if !ct.fn(ctx, v) {
E:/dependencies/go/pkg/mod/github.com/go-playground/validator/[email protected]/validator.go:315 (0x147882f)
	(*validate).traverseField: v.traverseField(ctx, parent, current.Index(i), ns, structNs, reusableCF, ct)
E:/dependencies/go/pkg/mod/github.com/go-playground/validator/[email protected]/validator.go:78 (0x1473f0e)
	(*validate).validateStruct: v.traverseField(ctx, current, current.Field(f.idx), ns, structNs, f, f.cTags)
E:/dependencies/go/pkg/mod/github.com/go-playground/validator/[email protected]/validator_instance.go:396 (0x147dcc8)
	(*Validate).StructCtx: vd.validateStruct(ctx, top, val, val.Type(), vd.ns[0:0], vd.actualNs[0:0], nil)
E:/dependencies/go/pkg/mod/github.com/go-playground/validator/[email protected]/validator_instance.go:369 (0x147d7d4)
	(*Validate).Struct: return v.StructCtx(context.Background(), s)
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/binding/default_validator.go:83 (0x158a74b)
	(*defaultValidator).validateStruct: return v.validate.Struct(obj)
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/binding/default_validator.go:60 (0x158a42a)
	(*defaultValidator).ValidateStruct: return v.validateStruct(obj)
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/binding/binding.go:121 (0x1589da9)
	validate: return Validator.ValidateStruct(obj)
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/binding/json.go:55 (0x15902b7)
	decodeJSON: return validate(obj)
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/binding/json.go:37 (0x15900d5)
	jsonBinding.Bind: return decodeJSON(req.Body, obj)
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:752 (0x15a4b98)
	(*Context).ShouldBindWith: return b.Bind(c.Request, obj)
E:/code/GoglandProjects/realMsgService/ctrl/order_item.go:173 (0x18598f3)
	(*OrderItemController).CreateOrderItems: if err := c.ShouldBindWith(&in, binding.JSON); err != nil {
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:185 (0x159fdb9)
	(*Context).Next: c.handlers[c.index](c)
E:/code/GoglandProjects/realMsgService/web/middleware/online.go:18 (0x18936d2)
	Online: c.Next()
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:185 (0x159fdb9)
	(*Context).Next: c.handlers[c.index](c)
E:/code/GoglandProjects/realMsgService/web/middleware/permission.go:16 (0x1895c97)
	PermissionMiddleware.func1: c.Next()
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:185 (0x159fdb9)
	(*Context).Next: c.handlers[c.index](c)
E:/code/GoglandProjects/realMsgService/web/middleware/auth.go:73 (0x1892804)
	Auth: c.Next()
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:185 (0x159fdb9)
	(*Context).Next: c.handlers[c.index](c)
E:/code/GoglandProjects/realMsgService/web/middleware/elapsed.go:14 (0x18929c5)
	Elapsed: c.Next()
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:185 (0x159fdb9)
	(*Context).Next: c.handlers[c.index](c)
E:/code/GoglandProjects/realMsgService/web/middleware/safety_trace.go:71 (0x189486b)
	SafetyTracer: c.Next()
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:185 (0x159fdb9)
	(*Context).Next: c.handlers[c.index](c)
E:/code/GoglandProjects/realMsgService/web/middleware/repeat_read.go:25 (0x1893a26)
	RepeatRead: c.Next()
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:185 (0x159fdb9)
	(*Context).Next: c.handlers[c.index](c)
E:/dependencies/go/pkg/mod/github.com/gin-contrib/[email protected]/zap.go:76 (0x189057a)
	GinzapWithConfig.func1: c.Next()
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:185 (0x159fdb9)
	(*Context).Next: c.handlers[c.index](c)
E:/code/GoglandProjects/realMsgService/web/middleware/request_id.go:19 (0x1893cf8)
	RequestIDMiddleware: c.Next()
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:185 (0x159fdb9)
	(*Context).Next: c.handlers[c.index](c)
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/recovery.go:102 (0x15b26bc)
	CustomRecoveryWithWriter.func1: c.Next()
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:185 (0x159fdb9)
	(*Context).Next: c.handlers[c.index](c)
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/gin.go:633 (0x15b00e9)
	(*Engine).handleHTTPRequest: c.Next()
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/gin.go:589 (0x15afbbb)
	(*Engine).ServeHTTP: engine.handleHTTPRequest(c)
C:/Program Files/Go/src/net/http/server.go:3210 (0xbb3f16)
	serverHandler.ServeHTTP: handler.ServeHTTP(rw, req)
C:/Program Files/Go/src/net/http/server.go:2092 (0xb9cc74)
	(*conn).serve: serverHandler{c.server}.ServeHTTP(w, w.req)
C:/Program Files/Go/src/runtime/asm_amd64.s:1700 (0x6a59c0)
	goexit: BYTE	$0x90	// NOP

当我退回 v10.14.0 的时候就没有问题了。

realMsgService/ctrl/order_item.go:173 是这样的:

Image

这段代码我已经很久没有改过它,这一次崩溃,就是因为 github.com/go-playground/validator/v10 从 v10.14.0 升到了 v10.26.0 导致的。这给我造成了非常严重的客户信任危机。

代码中使用的结构体 param.OrderItemsCreateInput 是这样的:

package param

type OrderItem struct {
	SkuId   int64 `json:"skuId,omitempty" form:"skuId" binding:"required,gte=1"`
	Subject int8  `json:"subject,omitempty" form:"subject" binding:"required,oneof=1 2"`
	Amount  int64 `json:"amount,omitempty" form:"amount" binding:"required,gte=1"`
}

type OrderItemsCreateInput struct {
	OrderItems []OrderItem `json:"orderItems,omitempty" form:"orderItems" binding:"required,dive,min=1,max=100"`
}

Version

v10.26.0

Example Code

func (ctrl *OrderItemController) CreateOrderItems(c *gin.Context) {
	var in param.OrderItemsCreateInput
	if err := c.ShouldBindWith(&in, binding.JSON); err != nil {
		c.JSON(http.StatusBadRequest, ctrl.ApiCheck(c, gin.H{"msg": ctrl.processError(err)}))
		return
	}
	session, err := concurrency.NewSession(g.EtcdCli())
	if err != nil {
		c.JSON(http.StatusInternalServerError, ctrl.ApiFail(c, gin.H{"msg": err.Error()}))
		return
	}
	defer func(session *concurrency.Session) {
		if err := session.Close(); err != nil {
			g.LogWithContext(c.Request.Context()).Error("关闭etcd的session出现错误", zap.Error(err))
		}
	}(session)
	mutex := concurrency.NewMutex(session, fmt.Sprintf("/create-order-item-lock/%d", ctrl.MustLoginUser(c).UserId))
	// 竞争锁,最多等待3秒
	ctx, cancel := context.WithTimeout(c, time.Second*3)
	defer cancel()
	err = mutex.TryLock(ctx)
	if errors.Is(err, concurrency.ErrLocked) {
		c.JSON(http.StatusOK, ctrl.ApiFail(c, gin.H{"msg": "这会儿下单的人太多了"}))
		return
	}
	if err != nil {
		c.JSON(http.StatusInternalServerError, ctrl.ApiFail(c, gin.H{"msg": err.Error()}))
		return
	}
	defer func(mutex *concurrency.Mutex, ctx context.Context) {
		if err := mutex.Unlock(ctx); err != nil {
			g.Log().Error("释放etcd的锁出现错误", zap.Error(err))
		}
	}(mutex, ctx)
	// 创建订单
	if orderId, err := ctrl.createOrderItemsAction(c, in); err != nil {
		c.JSON(http.StatusInternalServerError, ctrl.ApiFail(c, gin.H{"msg": err.Error()}))
		return
	} else {
		c.JSON(http.StatusOK, ctrl.ApiOk(c, gin.H{
			"code": http.StatusOK,
			"data": map[string]interface{}{
				"orderId": orderId,
			},
		}))
		return
	}
}

ccpwcn avatar Apr 03 '25 01:04 ccpwcn

Hey @ccpwcn , sorry to hear about this issue. Nothing should have changed that would affected these validations, especially such core ones.

Is it possible to provide a simpler reproducible example only using the validator code not through gin? Mainly so I can take a look tomorrow , it’s very late here, will help me debug faster 🙏 If not I can likely piece it together from your example.

deankarn avatar Apr 03 '25 04:04 deankarn

I tried really quickly to reproduce but was unable to using the below code.

I even checked the git blame, the gte logic hasn't changed since 2023 and that was a fix for float types and not int64 which I think are used in your case and not changes since 2020, so it must be upstream of that, but if I'm unable to reproduce it would be hard to know where the issue lies.

package main

import (
	"fmt"

	"github.com/go-playground/validator/v10"
)

type OrderItem struct {
	SkuId   int64 `json:"skuId,omitempty" form:"skuId" binding:"required,gte=1"`
	Subject int8  `json:"subject,omitempty" form:"subject" binding:"required,oneof=1 2"`
	Amount  int64 `json:"amount,omitempty" form:"amount" binding:"required,gte=1"`
}

type OrderItemsCreateInput struct {
	OrderItems []OrderItem `json:"orderItems,omitempty" form:"orderItems" binding:"required,dive,min=1,max=100"`
}

func main() {
	validator := validator.New()

	input := OrderItemsCreateInput{
		OrderItems: []OrderItem{
			{
				SkuId:   3,
				Subject: 2,
				Amount:  3,
			},
		},
	}

	errs := validator.Struct(input)

	fmt.Println(errs)

}

deankarn avatar Apr 03 '25 05:04 deankarn

@deankarn I successfully reproduced this crash/panic

package main

import (
	"fmt"

	"github.com/go-playground/validator/v10"
)

type OrderItem struct {
	SkuId   int64 `json:"skuId,omitempty" form:"skuId" validate:"required,gte=1"`
	Subject int8  `json:"subject,omitempty" form:"subject" validate:"required,oneof=1 2"`
	Amount  int64 `json:"amount,omitempty" form:"amount" validate:"required,gte=1"`
}

type OrderItemsCreateInput struct {
	OrderItems []OrderItem `json:"orderItems,omitempty" form:"orderItems" validate:"required,dive,min=1,max=100"`
}

func main() {
	validator := validator.New()

	input := OrderItemsCreateInput{
		OrderItems: []OrderItem{
			{
				SkuId:   3,
				Subject: 2,
				Amount:  3,
			},
		},
	}

	errs := validator.Struct(input)

	fmt.Println(errs)

}

nodivbyzero avatar Apr 03 '25 14:04 nodivbyzero

Do you mean this exact same code fails on your computer @nodivbyzero ?

What version of Go and OS are you using, I was using:

  • Go v1.24.2
  • Macos Sequoia 15.4

deankarn avatar Apr 04 '25 04:04 deankarn

Hey @ccpwcn , sorry to hear about this issue. Nothing should have changed that would affected these validations, especially such core ones.

Is it possible to provide a simpler reproducible example only using the validator code not through gin? Mainly so I can take a look tomorrow , it’s very late here, will help me debug faster 🙏 If not I can likely piece it together from your example.

I used go version go1.23.2 windows/amd64, Windows 10, No more code for this bug, the code are customer private, but this bug is bound to occur.

ccpwcn avatar Apr 04 '25 05:04 ccpwcn

Hey @ccpwcn , sorry to hear about this issue. Nothing should have changed that would affected these validations, especially such core ones.

Is it possible to provide a simpler reproducible example only using the validator code not through gin? Mainly so I can take a look tomorrow , it’s very late here, will help me debug faster 🙏 If not I can likely piece it together from your example.

v10.14.0 no problem, v10.26.0 is bound to crash/panic.

ccpwcn avatar Apr 04 '25 05:04 ccpwcn

@deankarn not exactly the same. I changed binding:"required,dive,min=1,max=100" to validate:"required,dive,min=1,max=100" in the provided example.

My Go version:

$ go version  
go version go1.24.1 darwin/arm64

Here is the unit-test which fails right now:

	type Foo struct {
		A int
	}
	type Bar struct {
		B []Foo `validate:"dive,min=1"`
	}

	fooBarTest := &Bar{
		B: []Foo{
			{
				A: 1,
			},
		},
	}
	errs = validate.Struct(fooBarTest)
	Equal(t, errs, nil)

nodivbyzero avatar Apr 04 '25 22:04 nodivbyzero

I am confused, the original bug report stack trace shows the gte validation erroring which is only on the inner struct.

I see now though why it would fail, this should have never worked before, if it did it was a bug, because min and max shouldn’t work with a struct.

deankarn avatar Apr 05 '25 04:04 deankarn

Image You're right. To constrain the maximum and minimum values of a number, I should use lt、lte、gt、gte.

The problem is: Indeed, our usage is incorrect, however, this still cannot explain the issue of not reporting an error in v10.14.0 but reporting an error in v10.26.0. After all, my code has not been modified for a long time, and the sudden error caught me off guard.

ccpwcn avatar Apr 05 '25 04:04 ccpwcn

Image You're right. To constrain the maximum and minimum values of a number, I should use lt、lte、gt、gte.

The problem is: Indeed, our usage is incorrect, however, this still cannot explain the issue of not reporting an error in v10.14.0 but reporting an error in v10.26.0. After all, my code has not been modified for a long time, and the sudden error caught me off guard.

It can prompt me that a certain option is incorrect, but crashing directly is still too uncomfortable.

ccpwcn avatar Apr 05 '25 04:04 ccpwcn

Image But, I look it again, my code no problem!!! min, max options used on slice, gte used on int64, oneof used on int8.

ccpwcn avatar Apr 05 '25 04:04 ccpwcn

But, I look it again, my code no problem!!! min, max options used on slice, gte used on int64, oneof used on int8.

But the current definition is not running min & max on a/the slice because they are after the dive , they are being applied to each OrderItem in the slice. If you want them to apply to the slice of OrderItem they need to come before the dive.

This not me deflecting blame for having a bug, but this is also why it’s important to have unit tests to catch unexpected things before they can affect a customer. I highly recommend everyone always them :)

deankarn avatar Apr 05 '25 15:04 deankarn

But, I look it again, my code no problem!!! min, max options used on slice, gte used on int64, oneof used on int8.

But the current definition is not running min & max on a/the slice because they are after the dive , they are being applied to each OrderItem in the slice. If you want them to apply to the slice of OrderItem they need to come before the dive.

This not me deflecting blame for having a bug, but this is also why it’s important to have unit tests to catch unexpected things before they can affect a customer. I highly recommend everyone always them :)

I see, Are you saying that if I want to constrain the []OrderItems slice itself, I should place the constraint before the dive, and if I want to constrain every member of the []OrerItems slice, I should place it after the dive? What do I think is wrong with this? Because in a slice, there may not only be int types, but also other types. How can we use min and max to constrain them uniformly?

ccpwcn avatar Apr 08 '25 14:04 ccpwcn