Gin框架中Middleware执行后请求Body为nil的问题排查
问题
我设置了如下路由:
setpw := api.Group("/user/pw") setpw.Use(middlewares.OTPMiddleware()) setpw.POST("/set", controllers.SetPassword)
在OTPMiddleware中仅验证OTP是否有效,之后调用context.Next()进入controllers.SetPassword,但此时请求Body无法正常解析,报错信息显示请求Body状态异常。
OTPMiddleware代码:
func OTPMiddleware() gin.HandlerFunc { return func(c *gin.Context) { var phonedb models.PhoneToken // 解析JSON Body到PhoneToken模型 if err := c.ShouldBindJSON(&phonedb); err != nil { c.JSON(http.StatusBadRequest, gin.H{"head": "error", "body": err.Error()}) c.Abort() return } // 其他验证逻辑 // 进入下一个处理器 c.Next() } }
SetPassword代码:
func SetPassword(c *gin.Context) { log.Println("Request Body:", c.Request.Body) var setpw models.SetPassword // 绑定JSON到SetPassword模型 if err := c.ShouldBindJSON(&setpw); err != nil { c.JSON(http.StatusBadRequest, gin.H{"head": "error", "body": "Parsing failed"}) return } }
错误信息:
Request Body: &{0xc0006d4990 <nil> <nil> true true {0 0} true false false 0x6ead20}
原因与解决方案
原因
Gin的ShouldBindJSON方法会读取并消耗请求的Body流(c.Request.Body本质是io.ReadCloser),读取完成后流的指针会移动到末尾,且默认情况下Body流只能被读取一次。后续控制器再调用ShouldBindJSON时,就无法读取到任何内容,导致解析失败。从错误信息里的true(对应http.Request.Body的closed字段)也能看出,Body已经被读取完毕或关闭。
解决方案
有两种实用的处理方式:
方式一:缓存Body内容,让流可重复读取
在中间件中先读取Body的全部内容,将其缓存后重新设置c.Request.Body为可重复读取的流,确保后续处理器能正常读取:
import ( "bytes" "io" ) func OTPMiddleware() gin.HandlerFunc { return func(c *gin.Context) { var phonedb models.PhoneToken // 读取Body全部内容 bodyBytes, err := io.ReadAll(c.Request.Body) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"head": "error", "body": err.Error()}) c.Abort() return } // 重新设置Body为可重复读取的Buffer c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // 绑定到phonedb模型 if err := c.ShouldBindJSON(&phonedb); err != nil { c.JSON(http.StatusBadRequest, gin.H{"head": "error", "body": err.Error()}) c.Abort() return } // 再次重置Body,保证控制器能读取 c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // OTP验证逻辑 c.Next() } }
方式二:合并结构体,一次绑定后传递数据
如果中间件和控制器需要的字段可以合并,定义一个包含所有所需字段的结构体,在中间件中绑定一次后,将解析好的数据存入上下文,后续控制器直接从上下文获取:
// 定义合并后的结构体,包含OTP和设置密码的所有字段 type OTPAndSetPassword struct { models.PhoneToken models.SetPassword } func OTPMiddleware() gin.HandlerFunc { return func(c *gin.Context) { var data OTPAndSetPassword if err := c.ShouldBindJSON(&data); err != nil { c.JSON(http.StatusBadRequest, gin.H{"head": "error", "body": err.Error()}) c.Abort() return } // 执行OTP验证逻辑 // 将设置密码所需的数据存入上下文 c.Set("setpwData", data.SetPassword) c.Next() } } // SetPassword控制器 func SetPassword(c *gin.Context) { // 从上下文获取预解析好的数据 setpwVal, ok := c.Get("setpwData") if !ok { c.JSON(http.StatusBadRequest, gin.H{"head": "error", "body": "无效请求数据"}) return } // 类型断言转换为目标结构体 setpwData, ok := setpwVal.(models.SetPassword) if !ok { c.JSON(http.StatusBadRequest, gin.H{"head": "error", "body": "数据类型错误"}) return } // 后续业务逻辑直接使用setpwData即可 }
内容的提问来源于stack exchange,提问作者M.Nasiri
相关产品推荐
相关产品推荐

