You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 04:22:43