net/http.DumpRequest 返回空请求体是因为其会消耗 req.Body,导致后续无法读取;需先用 io.ReadAll 读取并用 bytes.NewReader 和 io.NopCloser 重建 Body 才能安全打印且不影响后续处理。

为什么 net/http.DumpRequest 返回的请求体经常是空的?
默认情况下,net/http.DumpRequest 会读取并消耗 req.Body,但原始请求体(比如 POST 的 JSON 数据)一旦被读取就无法再次读取——后续 handler 里再调用 req.Body.Read() 就会得到空内容。这不是 bug,而是 Go 的 io.ReadCloser 设计使然。
- 调用前必须确保
req.Body可重复读:常见做法是先用io.ReadAll(req.Body)拿到原始字节,再用bytes.NewReader()重建req.Body - 如果只是调试打印,不关心后续 handler 逻辑,可跳过重置;但若要保留请求流程,必须重置
req.Body -
req.MultipartReader()或含文件上传的请求,DumpRequest无法安全处理,会提前消费 body 导致解析失败
如何安全地打印带 body 的请求(含重置 Body)
正确做法是把原始 body 提前读出、存为字节切片,然后分别用于 dump 和恢复 body。注意:不能直接传 req.Body 给 DumpRequest 后再指望它还能用。
bodyBytes, err := io.ReadAll(req.Body)
if err != nil {
log.Printf("read body failed: %v", err)
return
}
req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
dump, err := httputil.DumpRequest(req, true)
if err != nil {
log.Printf("dump failed: %v", err)
return
}
log.Printf("request dump:\n%s", string(dump))
- 必须用
io.NopCloser包装bytes.NewReader,因为req.Body类型是io.ReadCloser -
httputil.DumpRequest在标准库中,需导入net/http/httputil - 第二个参数设为
true才会包含 body;设为false只打印 headers 和 method/path
DumpRequest 对 chunked encoding 或 transfer-encoding 的处理限制
当请求使用 Transfer-Encoding: chunked(常见于 HTTP/1.1 流式上传),DumpRequest 无法还原原始 chunk 格式——它只输出解码后的 body 内容,且会自动添加 Content-Length 头,掩盖了原始传输细节。
- 无法区分“客户端发的是 chunked”还是“服务端已解码”,dump 结果看起来总像有
Content-Length - 若需观察原始 wire-level 数据(比如调试代理或中间件),应改用抓包工具(如 Wireshark)或在 TCP 层拦截
- 对 gzip 压缩的请求体,
DumpRequest不会自动解压,body 字节就是原始压缩流——除非你手动解压后再 dump
替代方案:用中间件统一记录请求(避免到处写 dump 逻辑)
硬编码 DumpRequest 容易漏掉或重复,更健壮的方式是封装成 http.Handler 中间件,在入口处统一处理。
func DumpRequestMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewReader(bodyBytes))
dump, _ := httputil.DumpRequest(r, true)
log.Printf("→ %s %s\n%s", r.Method, r.URL.Path, string(dump))
// 注意:这里要重新赋值 bodyBytes 给 next,否则下游收不到 body
r.Body = io.NopCloser(bytes.NewReader(bodyBytes))
next.ServeHTTP(w, r)
})
}
- 中间件里两次用
bytes.NewReader(bodyBytes)是必须的:一次给 dump,一次给下游 handler - 实际部署时建议加开关控制是否启用 dump,避免日志爆炸
- 生产环境慎用——大文件上传时
io.ReadAll会吃光内存,应限制 body size 或改用 streaming 方式
真正难的不是调用 DumpRequest,而是处理好 body 的生命周期和边界条件。尤其在复用 request、带流式 body、或集成第三方库时,Body 被提前关闭或多次读取的问题比 dump 结果本身更常导致故障。


















