读 r.Body 会破坏后续 handler 读取,因 http.Request.Body 是单次 io.ReadCloser;需缓存 bodyBytes 并用 io.NopCloser(bytes.NewReader(bodyBytes)) 重设 r.Body,同步更新 r.ContentLength 和 Header 中 Content-Length,GET/HEAD 请求应跳过 body 读取,表单请求优先用 r.ParseForm(),日志中需脱敏、采样、限长。

Go日志中间件里读 r.Body 会破坏后续 handler
直接调用 io.ReadAll(r.Body) 后,后续 handler(比如 Gin 的 c.BindJSON() 或标准库的 json.NewDecoder(r.Body).Decode())会读到空数据或报 EOF。这是因为 http.Request.Body 是单次读取的 io.ReadCloser,没有“重放”机制。
必须把原始 body 缓存并重设 r.Body
安全做法是:一次性读完、存成 []byte,再用 bytes.NewReader() 包一层塞回去,同时同步更新关键字段。否则像 multipart/form-data 解析、Gin binding 等都会失败。
-
r.Body = io.NopCloser(bytes.NewReader(bodyBytes))—— 注意要用io.NopCloser补上Close()方法,否则某些 handler 可能 panic - 必须重设
r.ContentLength = int64(len(bodyBytes)),否则ParseMultipartForm会因长度为 -1 拒绝解析 - 建议手动设置
r.Header.Set("Content-Length", strconv.FormatInt(r.ContentLength, 10)),避免下游依赖 Header 的逻辑出错 - 开头加
r.Body = http.MaxBytesReader(nil, r.Body, 1024*1024)(如 1MB),防 OOM
GET 请求别读 r.Body,application/x-www-form-urlencoded 优先走 r.ParseForm()
绝大多数 GET 请求的 r.Body 是 nil 或空,强行读会卡住或返回错误;而表单类请求(application/x-www-form-urlencoded、multipart/form-data)应优先调用 r.ParseForm(),再从 r.PostForm 或 r.MultipartForm 取值 —— 这比手动解析更健壮,也绕过 body 读取冲突。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- 判断条件:
if r.Method == "GET" || r.Method == "HEAD" { /* 跳过 body 读取 */ } -
r.ParseForm()内部已处理了 body 读取和重置,无需你再碰r.Body - 如果真要记录原始表单字节,仍需先
io.ReadAll+ 重设,但代价高,仅调试用
生产环境慎用完整 body 日志,脱敏和采样更实际
打印完整 bodyBytes 在日志里极易泄露密码、token、身份证等敏感字段,且大请求(如文件上传)会拖慢中间件、吃光内存。
立即学习“go语言免费学习笔记(深入)”;
- 只记录长度:
log.Printf("req.body.len=%d", len(bodyBytes)) - 限制日志截断:
string(bodyBytes[:min(len(bodyBytes), 512)]) - 关键词脱敏:用
strings.ReplaceAll(string(bodyBytes), "password:", "password:***")等简单替换(注意别误伤正常字段) - 按比例采样:比如只对
rand.Intn(100) == 0的请求记录 body,避免全量打点
真正难的不是“怎么读”,而是“读完之后怎么不崩、不泄密、不拖慢”。每次重设 r.Body 都得同步 ContentLength 和 Close 行为,漏一个就可能让下游 handler 静默失败。

















