直接修改c.Request.Body后handler拿不到数据,因为其底层是单次读取流,首次读取后指针已达EOF;需捕获内容、封装新io.ReadCloser并重置Body及Content-Length才能复用。

为什么直接修改 c.Request.Body 后 handler 拿不到数据
因为 c.Request.Body 是 io.ReadCloser,底层是单次读取流(如网络连接缓冲区或 bytes.Reader)。调用 io.ReadAll(c.Request.Body) 或 c.ShouldBindJSON() 一次后,内部读取指针已到 EOF;后续再读就是空字节或 io.EOF 错误——这不是 Gin 的 bug,而是 HTTP/1.1 协议层设计使然。
必须重置 Body 流才能复用
中间件若需解析、校验、脱敏或改写请求体,必须在读取后「捕获内容 + 封装新流 + 替换原 c.Request.Body」。否则下游 handler(比如 c.BindJSON())必然失败。
- 用
io.ReadAll()读取原始 body 字节,注意加http.MaxBytesReader防 OOM(例如限制 1MB) - 对读出的
[]byte做任意处理(JSON 解析 + 字段替换、form 解码 + 键值过滤等) - 处理完后,用
bytes.NewReader(data)创建新 reader,并包裹为io.NopCloser() - 关键一步:
c.Request.Body = io.NopCloser(bytes.NewReader(newData)) - 别忘了同步更新
c.Request.ContentLength和c.Request.Header.Set("Content-Length", strconv.Itoa(len(newData))),否则部分客户端或代理可能拒绝请求
ShouldBindBodyWith 是更轻量的替代方案
如果只是想「在中间件里读一次、handler 再读一次」,且不改写 body,c.ShouldBindBodyWith(&v, binding.JSON) 是 Gin 官方推荐方式。它内部自动缓存并复用 body 字节,避免手动重置流的繁琐逻辑。
- 只适用于结构体绑定场景,不支持泛型 map 或原始字节操作
- 不能用于脱敏或字段注入等需要修改 body 内容的场景
- 性能略低于纯字节操作(因涉及两次 JSON 序列化/反序列化),但开发成本低、不易出错
常见踩坑点
实际写中间件时,这几个细节最容易导致线上静默失败:
- 没跳过
GET或无 body 的请求(如HEAD),io.ReadAll(c.Request.Body)会阻塞或返回错误 - 修改 JSON body 后忘记重设
Content-Lengthheader,某些 nginx 配置下会截断响应 - 对
multipart/form-data直接读 raw body,应优先调c.Request.ParseMultipartForm()再操作c.Request.MultipartForm - 用
json.Unmarshal解析后修改 map,但未处理嵌套结构(如user.profile.token),导致脱敏漏杀 - 在重置
c.Request.Body前调用了c.Next(),逻辑顺序错乱,body 已被 handler 消费


















