You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 03:11:40