Director未修改req.URL是反向代理转发失败主因,需显式设置Scheme、Host、Path;须补全X-Forwarded-*头;应配置Transport复用连接;ModifyResponse改响应时需关闭Body并设ContentLength。

反向代理转发失败时 Director 函数没改 req.URL 是最常见原因
Go 的 httputil.NewSingleHostReverseProxy 不会自动重写请求目标,它只负责转发——前提是 Director 显式修改了 req.URL.Scheme、req.URL.Host 和 req.URL.Path。漏掉任意一项,请求大概率发到本地或 404。
典型错误现象:502 Bad Gateway 或后端收到空路径、错协议(比如用 http 去连 https 后端)。
-
Director必须重写req.URL.Host,不能只靠NewSingleHostReverseProxy构造时传的 URL - 如果后端是
https,要手动设req.URL.Scheme = "https",否则默认仍是http -
req.URL.Path推荐用singleJoiningSlash拼接,避免双斜杠或路径丢失:req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
不处理 X-Forwarded-* 头会导致下游服务取不到真实客户端 IP
默认情况下,httputil.ReverseProxy 不添加任何 X-Forwarded-* 头。如果你的后端依赖 X-Forwarded-For 做限流、日志或地理定位,它看到的永远是网关本机 IP。
必须在 Director 之后、proxy.ServeHTTP 之前,手动注入头:
立即学习“go语言免费学习笔记(深入)”;
- 用
req.Header.Set("X-Forwarded-For", clientIP(req)),其中clientIP要从req.RemoteAddr解析并过滤代理链(如信任 Nginx,则取X-Real-IP或最后一个非私有 IP) - 补全
X-Forwarded-Proto(根据req.TLS != nil判断)和X-Forwarded-Host(用req.Host) - 注意不要重复追加——
Set会覆盖,Add才追加;但X-Forwarded-For应该是追加,不是覆盖
ReverseProxy.Transport 默认不复用连接,高并发下容易耗尽文件描述符
Go 标准库的 http.Transport 实例默认开启连接复用,但 httputil.NewSingleHostReverseProxy 创建的 proxy 对象,其 Transport 字段是 nil,最终 fallback 到全局 http.DefaultTransport。问题在于:如果你没显式配置,它会沿用默认值,而默认的 MaxIdleConnsPerHost 是 2——这在压测时几秒就打满。
- 务必给 proxy 设置自定义
Transport,至少调大两个关键参数:proxy.Transport = &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 100, IdleConnTimeout: 30 * time.Second, } - 如果后端支持 HTTP/2,记得启用
ForceAttemptHTTP2,否则即使后端是 h2,Go proxy 也会降级为 h1.1 - 别忘了设置
Timeout、KeepAlive等,否则超时逻辑全走默认(可能长达 30 秒)
修改响应体前必须关闭 Flush 并禁用 Chunked 编码
想在返回给客户端前改响应内容(比如注入 header、重写 body),直接读 resp.Body 会卡住——因为 ReverseProxy 默认启用流式传输,body 是边收边发的。一旦你调 io.ReadAll,就会阻塞整个响应流。
正确做法是替换 proxy.ModifyResponse,并在里面接管 body:
- 先
resp.Body.Close(),再用io.ReadAll读完全部内容 - 构造新
bytes.Reader替换resp.Body - 必须显式设置
resp.ContentLength,否则 Go 会自动切回Transfer-Encoding: chunked,导致前端解析失败 - 如果原响应没设
Content-Length且你又不想改 body,就别碰ModifyResponse——它代价高、易出错
真正需要改响应的场景其实很少;多数时候,用中间件或后端自己处理更稳。










