reverseproxy 的 director 必须重写 request.url.host 和 scheme,否则请求虽发往目标服务,但 host 头仍为客户端域名,导致后端因租户识别、路由或 tls 终止失败而返回 404 或证书错误;go 默认不修改这两字段,影响连接建立与 tls 握手。

为什么 ReverseProxy 的 Director 必须重写 Request.URL.Host 和 Request.URL.Scheme
不重写这两个字段,请求会原样发到目标服务,但 Host 头仍为客户端发起时的域名(比如 api.example.com),而目标服务可能依赖 Host 判断租户、路由或 TLS 终止逻辑,导致 404 或证书错误。Go 的 httputil.NewSingleHostReverseProxy 默认只改 Request.URL.Path 和 Request.Host,但不会动 Scheme 和 Host 字段本身——而这俩恰恰影响底层连接建立和 TLS 握手。
常见错误现象:http: proxy error: x509: certificate is valid for example.com, not localhost,或后端 Nginx 返回 400 “The plain HTTP request was sent to HTTPS port”。
- 必须在
Director函数里显式设置:c.Request.URL.Scheme = "https"、c.Request.URL.Host = "backend.example.com:443" - 如果目标是 HTTPS,且你用的是自签名证书,需额外配置
proxy.Transport的TLSClientConfig.InsecureSkipVerify = true(仅限测试) - 别依赖
c.Request.Host—— 它只是 Header 中的字符串,不影响实际 dial 连接
router.Any("/proxy/*path", handler) 中的通配符匹配与路径透传怎么控制
Gin 的 * 通配符会捕获完整子路径(含斜杠),比如请求 /proxy/v1/users/123,c.Param("path") 得到的是 /v1/users/123,不是 v1/users/123。若直接拼到目标 URL 后,容易出现双斜杠(如 https://upstream//v1/users/123),触发 400 错误。
正确做法是剥离开头的 /,再拼接:
func(c *gin.Context) {
path := c.Param("path")
if len(path) > 0 && path[0] == '/' {
path = path[1:]
}
c.Request.URL.Path = "/target-prefix/" + path
proxy.ServeHTTP(c.Writer, c.Request)
}
- 不要用
strings.TrimPrefix(c.Param("path"), "/")—— 它对空字符串返回空,但c.Param("path")在/proxy/时就是"/",TrimPrefix后变为空,导致目标路径只剩根路径 - 若需完全透传原始路径(包括 query 参数),无需手动处理
c.Request.URL.RawQuery——ReverseProxy默认保留它 - 注意:Gin 的
router.GET("/proxy/:all", ...)不会匹配多级路径,必须用*通配符
Header 透传时为什么 Authorization 和 Cookie 常被丢弃
Go 标准库的 ReverseProxy 默认会过滤掉部分敏感 Header,比如 Authorization、Cookie、X-Forwarded-For 等,不是 bug,而是安全策略。它只保留白名单内的 Header(如 User-Agent、Accept),其余全清空。
必须显式在 Director 后、ServeHTTP 前补全:
proxy.Director = func(req *http.Request) {
// ... 其他重写逻辑
}
// 在 handler 里:
c.Request.Header.Set("Authorization", c.GetHeader("Authorization"))
c.Request.Header.Set("Cookie", c.GetHeader("Cookie"))
proxy.ServeHTTP(c.Writer, c.Request)
- 不能在
Director里设req.Header—— 此时req是新拷贝,c.Request.Header还是原始对象 -
X-Real-IP和X-Forwarded-For需手动追加客户端真实 IP,否则后端拿不到源地址 - 若后端校验
Origin或Referer,也要一并透传,否则 CORS 或防爬逻辑可能失败
动态更新代理目标时,为什么要用 sync.RWMutex 而不是普通 sync.Mutex
读多写少场景下(比如每秒上千次代理请求,但目标地址几小时才改一次),用普通互斥锁会导致所有读操作排队等待写锁释放,吞吐骤降。而 sync.RWMutex 允许多个 goroutine 并发读,只有写时才独占。
典型错误写法:mu.Lock(); targetURL = newURL; mu.Unlock() —— 这会让所有并发代理请求卡在 mu.Lock() 上等待。
- 读取时用
mu.RLock()+defer mu.RUnlock(),性能几乎无损 - 更新时才用
mu.Lock(),且应校验 URL 格式(url.Parse)后再赋值,避免写入非法值导致后续 panic - 别把
ReverseProxy实例缓存在全局变量里反复复用——每次目标变更都得重建httputil.NewSingleHostReverseProxy,否则内部 transport 缓存了旧连接
proxy.Transport.CloseIdleConnections(),否则流量会在数分钟内持续打向不可达地址。











