必须手动缓存并重置 req.body——因 http.request.body 是一次性 io.readcloser,读完即 eof;推荐用 io.readall + bytes.newreader + io.nopcloser 重建,并确保只在首个中间件中执行且置于最前。

不能直接多次读 req.Body,必须手动缓存并重置——这是 Go 标准库的设计限制,不是 Echo 的 bug。
为什么 Echo 中间件里 req.Body 只能读一次
http.Request.Body 是一个 io.ReadCloser,底层通常是 socket 连接或一次性内存 buffer。Go 不做自动缓存,io.ReadAll(req.Body) 或 c.Bind() 读完后指针就到 EOF,后续再读返回空或 EOF 错误。
- 常见错误现象:
json.NewDecoder(c.Request().Body).Decode(&v)成功后,handler 里再调c.Bind()就 panic 或静默失败 - 中间件校验 JSON Schema 后,业务 handler 拿不到原始 body,导致无法重放、审计或调试
- 日志中间件想记录原始请求体,但 bind 或 decode 已经把它“吃掉”了
推荐做法:用 io.ReadAll + bytes.NewReader + io.NopCloser
这是最通用、兼容性最强的方案,适用于所有 Go 版本(包括 1.16+),且不依赖框架特定逻辑。
- 先调
io.ReadAll(req.Body)把全部内容读进[]byte - 显式调
req.Body.Close()(否则连接可能泄漏) - 用
bytes.NewReader(buf)创建新 reader,再包一层io.NopCloser()得到可重复使用的io.ReadCloser - 赋值回
req.Body = io.NopCloser(bytes.NewReader(buf)) - 注意:
io.NopCloser不支持Seek(),所以别写req.Body.Seek(0, 0);如需 seek,直接传bytes.NewReader(buf)给 decoder
示例中间件:
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
func bodyCacheMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
buf, err := io.ReadAll(c.Request().Body)
if err != nil {
return c.String(http.StatusBadRequest, "failed to read body")
}
c.Request().Body.Close() // 必须关闭
c.Request().Body = io.NopCloser(bytes.NewReader(buf))
return next(c)
}
}
替代方案:用 req.GetBody(仅限 Go 1.8+,且需上游设置)
req.GetBody 是标准库原生支持多次读取的机制,但前提是它被正确初始化——多数反向代理或网关(如 Nginx、Traefik)不会设置它,Echo 默认也不设。
- 如果你控制整个 HTTP 入口(比如直接监听
http.Server),可以在创建http.Request前手动设置:req.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(buf)), nil } - 但在典型 Echo 应用中,
req.GetBody为nil,直接调用会 panic - 所以生产环境别依赖它,除非你明确知道上游已配置好
容易被忽略的关键点
很多人写了缓存逻辑却仍失败,问题常出在三处:
- 忘了
req.Body.Close(),导致连接未释放、复用时卡死 - 把
io.NopCloser和bytes.Reader混用,误以为能Seek(),结果 decoder 失败 - 在多个中间件里重复读 body(比如日志中间件 + 校验中间件),没统一用同一个缓存副本,造成二次读空
真正安全的做法是:只在一个中间件里做缓存,并确保它在所有依赖 body 的中间件之前执行(e.Use(bodyCacheMiddleware) 放最前面)。










