Go 的 http.Client 默认不复用连接且请求体不可重放,需手动配置Body可重复读、用httputil序列化快照、白名单覆盖敏感Header、显式设置超时与连接池参数。

Go 的 http.Client 默认不复用连接,重放请求前必须手动配置
HTTP 请求重放不是简单地把 req.Body 读一遍再发第二次——Go 的 http.Request Body 是单次可读的 io.ReadCloser,第二次调用 client.Do(req) 会因 Body 已关闭或 EOF 报错:http: request body closed 或 http: invalid ReadByte call on closed body。
真正要重放,得让 Body 可重复读。常见做法是把原始 Body 全部读进内存(适合小请求),再用 bytes.NewReader 包装:
bodyBytes, _ := io.ReadAll(req.Body) req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
但注意:大文件上传或流式 Body 不能这么干,会吃光内存;生产环境应加长度限制(比如 http.MaxBytesReader)并明确拒绝超长 Body。
用 httputil.DumpRequestOut 保存原始请求快照,别自己拼字符串
联调时经常要“回放上次发的请求”,靠手敲 URL、Header、Body 极易出错。Go 标准库 httputil 提供了可靠的序列化工具,能完整保留 HTTP/1.1 格式细节(包括空行、大小写 Header、分块编码状态等)。
立即学习“go语言免费学习笔记(深入)”;
实操建议:
- 收到请求后立刻用
httputil.DumpRequestOut(req, true)生成原始字节快照,存本地文件或内存 map - 重放时用
httputil.ReadRequest解析快照,它比手动http.NewRequest更安全——自动处理 Transfer-Encoding、Content-Length 冲突、Header 多值等边界情况 - 避免用
fmt.Sprintf拼接请求:Header 值含空格或换行会破坏协议,url.Values.Encode()不适用于非表单 Body
重放时 Host 和 Referer 等 Header 很可能需要动态覆盖
直接 dump + replay 常见失败点不是 Body,而是 Host、Referer、Cookie、Authorization 这类上下文敏感 Header。比如原请求发给 localhost:8080,重放时目标已切到 staging.example.com,但 Host 还是旧值,后端 Nginx 可能 404 或路由错。
安全做法是重放前做白名单式覆盖:
- 只允许修改
Host、Referer、Origin、Cookie这几项(其他 Header 保持 dump 时原样) - 用
req.Host = "staging.example.com"而非req.Header.Set("Host", ...)——前者影响底层连接目标,后者只改请求行 Header - Cookie 若含
HttpOnly或过期字段,重放时需清理或更新,否则可能被浏览器拦截或服务端拒绝
并发重放多个请求时,http.Client 的 Timeout 和 Transport 必须显式设置
默认 http.Client 没有超时,一次卡死的请求会让整个重放流程挂住;默认 Transport 的连接池参数也偏保守,高并发下容易出现 dial tcp: i/o timeout 或 too many open files。
最小可用配置示例:
client := &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 30 * time.Second,
},
}注意:MaxIdleConnsPerHost 必须设(默认是 2),否则并发请求会排队等待连接复用;如果重放目标是同一台后端,这个值太小会成为瓶颈;太大则可能触发对方防火墙限流。
复杂点在于:联调常需同时重放几十个不同域名的请求(比如微服务网关+下游多个服务),这时 MaxIdleConnsPerHost 比 MaxIdleConns 更关键——每个域名都算独立 Host。










