因为 req.Body 是单次消费的 io.ReadCloser,首次读取后再次读会返回 EOF;常见错误是中间件读取后导致下游 handler 无法解析;推荐用 httputil.DumpRequest(req, true) 安全获取原始报文并重放 body。

为什么不能直接读 req.Body 两次?
因为 req.Body 是一个 io.ReadCloser,底层通常是单次消费的流(比如网络 socket 缓冲区)。第一次调用 req.Body.Read() 后,数据就前进了,再次读会返回空或 EOF —— 这不是 bug,是设计使然。
常见错误现象:在中间件里打印了请求体后,下游 handler 拿到空的 req.Body,导致 json.Decode() 失败、ParseForm() 报 invalid character 等。
- 别用
bytes.Buffer或strings.Reader临时存一次再重设req.Body—— 它们不实现io.Closer,强行赋值会导致 panic - 别在 handler 里反复
req.Body.Read(),除非你手动 reset(但标准库没提供 reset 方法) - 如果用了
req.ParseMultipartForm(),它会自动 consumeBody,之后再读就是空
用 httputil.DumpRequest 提取完整原始报文(含 headers + body)
这是最接近“原始报文”的标准方案,它把整个 HTTP 请求(包括起始行、headers、body)序列化为字节切片,适合日志、调试、审计场景。
注意:httputil.DumpRequest 默认会 consume req.Body;若需后续继续使用,得传 true 让它自动重放:
立即学习“go语言免费学习笔记(深入)”;
dump, err := httputil.DumpRequest(req, true)
if err != nil {
// 处理 err,比如 body 已关闭、不可 seek 等
}
// dump 是 []byte,可直接 log 或写入 trace
log.Printf("raw request: %s", dump)
- 第二个参数为
true时,DumpRequest内部会用io.NopCloser(bytes.NewReader(buf))重建req.Body,保证下游可用 - 但如果原始
req.Body是 multipart 或过大(如文件上传),DumpRequest会把整个 body 加载进内存,有 OOM 风险 - 它不会处理 chunked encoding 的 decode,dump 出来的是解码后的明文 body
手动复制 req.Body 并替换(适用于需要控制 body 内容的场景)
当你要做鉴权签名、流量镜像、或修改 body 再转发时,必须自己接管 body 流。核心是:读一次、存两份(一份供当前使用,一份重建给后续 handler)。
body, err := io.ReadAll(req.Body)
if err != nil {
// handle error
}
req.Body.Close() // 必须 close 原始 Body
// 供当前逻辑使用
processBody(body)
// 重建 Body 供下游使用
req.Body = io.NopCloser(bytes.NewReader(body))
-
io.NopCloser是关键 —— 它把bytes.Reader包装成io.ReadCloser,满足req.Body接口要求 - 不要用
strings.NewReader(string(body)),UTF-8 编码可能出错,二进制内容(如图片、protobuf)会损坏 - 大 body(>1MB)建议加限制:
io.LimitReader(req.Body, 2<<20)防止内存爆炸 - 如果 handler 后续还要调用
req.FormValue或req.MultipartForm,确保你没破坏原始 content-type 和 boundary
提取原始 body 而不干扰 ParseForm 或 MultipartForm
标准库的 ParseForm 和 ParseMultipartForm 都会内部读 req.Body,所以不能提前全量读取再重建 —— 否则它们会读不到数据。
正确做法是:只在必要时才读 body,并且让标准库自己去处理解析逻辑。如果你只是想“看看”原始 body,又不想影响后续解析,唯一安全方式是 hook 在解析之后:
// 先让标准库解析
err := req.ParseForm()
if err != nil {
// handle
}
// 此时 body 已被 consume,但 Form 已填充
// 若还需原始 body,只能靠前面的 DumpRequest(true) 或手动 copy(且必须在 ParseForm 前)
- 没有“既让 ParseForm 成功,又能拿到原始未解析 body”的无副作用方案
- 如果必须两者兼顾,只能在
ParseForm前用DumpRequest(req, true),它内部会先读 body、再重建,不影响后续调用 -
req.PostForm和req.MultipartForm是解析结果,不是原始报文 —— 别混淆
httputil.DumpRequest(req, true) 最省心;后者往往要改 body 或加 header,就得手动 io.ReadAll + io.NopCloser 重建。别忘了检查 Content-Length 和 Transfer-Encoding,它们不会随 body 替换自动更新。


















