如何在Transport.RoundTrip中保持http.Request.Body不关闭?代理重试求助
解决代理重试时的"invalid Read on closed Body"问题及proxyTo实现思路
兄弟,这个问题我太熟了!之前做反向代理重试逻辑的时候正好踩过这个坑,核心原因就是HTTP请求的Body是io.ReadCloser类型,当http.Transport.RoundTrip执行完成(哪怕请求失败),它都会自动关闭这个Body流,导致你第二次调用proxyTo时,没法再读取已经关闭的Body,就触发了那个报错。
核心解决方案:缓存请求体内容
要解决这个问题,我们需要在第一次转发前,把原始请求的Body内容完整读取并缓存起来,这样每次重试的时候,都可以基于缓存的内容重新创建一个新的io.ReadCloser,避免复用已经被关闭的原始Body。
完整代码实现
1. 先修改proxy函数,增加请求体缓存逻辑
func proxy(w http.ResponseWriter, r *http.Request) { // 缓存原始请求体 var bodyBytes []byte var err error if r.Body != nil { bodyBytes, err = io.ReadAll(r.Body) if err != nil { http.Error(w, "failed to read request body", http.StatusInternalServerError) return } // 读取后把Body重新放回去,不影响第一次转发 r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) } // 第一次转发到9902 ok := proxyTo("http://127.0.0.1:9902", w, r) if !ok { // 重试前重新创建Body,用缓存的字节数组 if len(bodyBytes) > 0 { r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) } // 第二次转发到9903 proxyTo("http://127.0.0.1:9903", w, r) } }
2. proxyTo函数的完整实现
这个函数的核心是修改请求的URL指向目标服务,转发请求后把响应完整写回客户端:
import ( "io" "log" "net/http" "net/url" "time" "bytes" ) func proxyTo(addr string, w http.ResponseWriter, r *http.Request) bool { // 解析目标服务地址 targetURL, err := url.Parse(addr) if err != nil { http.Error(w, "invalid target address", http.StatusInternalServerError) return false } // 修改请求的URL信息,指向目标服务 r.URL.Scheme = targetURL.Scheme r.URL.Host = targetURL.Host r.URL.Path = targetURL.Path r.URL.RawQuery = targetURL.RawQuery // 重置Host头,避免携带原始请求的Host导致目标服务识别错误 r.Host = targetURL.Host // 配置HTTP客户端,设置超时避免请求挂起 client := &http.Client{ Timeout: 5 * time.Second, Transport: &http.Transport{ DisableKeepAlives: false, // 可以根据需求添加更多配置,比如TLS设置、最大连接数等 }, } // 发送请求到目标服务 resp, err := client.Do(r) if err != nil { log.Printf("proxy to %s failed: %v", addr, err) return false } defer resp.Body.Close() // 将目标服务的响应头、状态码、响应体完整写回客户端 for k, v := range resp.Header { w.Header()[k] = v } w.WriteHeader(resp.StatusCode) _, err = io.Copy(w, resp.Body) if err != nil { log.Printf("failed to copy response body: %v", err) return false } return true }
关键细节说明
- 缓存请求体:用
io.ReadAll把原始Body读取到字节数组,再用io.NopCloser包装成可关闭的流放回请求,保证第一次转发正常;重试时重新包装一次,就能得到全新的未关闭Body。 - 重置Host头:如果不修改
r.Host,目标服务收到的Host会是原始请求的Host,可能导致服务路由错误,必须设置为目标地址的Host。 - 客户端超时:一定要给
http.Client设置超时,避免某个目标服务挂起导致整个代理请求卡住。 - 响应完整转发:要把目标服务的所有响应头、状态码和响应体都拷贝回客户端,保证请求的完整性。
内容的提问来源于stack exchange,提问作者fine_time
相关产品推荐
相关产品推荐

