功能说明
网关层常需要把请求转发到内部服务,同时补充来源信息、剥离不该外泄的调试头。httputil.ReverseProxy 处理了请求复制、连接复用、流式响应和错误处理等细节,比手写转发可靠。示例在转发前记录原始主机到 X-Forwarded-Host,并删除客户端可能伪造的 X-Internal-Debug。
错误处理器统一记录日志并返回 502,避免把上游的原始错误泄露给调用方,也保证调用方拿到结构化状态码而不是空响应。
关键代码
NewSingleHostReverseProxy 会重写 Scheme、Host 和 Path,但不会自动保留原始主机,因此包装原 Director,在其基础上追加头操作。ErrorHandler 接收响应写入器、请求和错误,打印日志后用 http.Error 输出纯文本 502。main 启动一个回显路径和转发主机的后端,再用另一个测试服务器承载代理,客户端请求 /api/items?page=2,验证查询串与头部都被正确传递。
使用注意
Director 与 Rewrite 两种钩子不能混用,新代码优先使用 Rewrite 以便更好地控制查询参数。转发前应校验目标地址来自可信配置,不能由请求参数决定,否则会变成开放代理。默认会追加 X-Forwarded-For,若前面已有可信代理需要配置信任链。请求体大小和超时要在外层限制。TLS 校验、重试和熔断不属于本示例,生产环境需另行配置 Transport。
验证方式
响应状态为 200,正文形如 path=/api/items host=127.0.0.1 加端口,说明路径与查询串被转发、转发主机头已写入。把后端地址改成不可达端口,代理会打印错误并返回 502 与固定提示。可以故意在请求中带 X-Internal-Debug,确认后端收不到该头。
代码示例
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/url"
"strings"
)
func NewReverseProxy(target *url.URL) *httputil.ReverseProxy {
proxy := httputil.NewSingleHostReverseProxy(target)
originalDirector := proxy.Director
proxy.Director = func(request *http.Request) {
originalDirector(request)
request.Header.Set("X-Forwarded-Host", request.Host)
request.Header.Del("X-Internal-Debug")
}
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
fmt.Println("proxy error:", err)
http.Error(w, "upstream unavailable", http.StatusBadGateway)
}
return proxy
}
func main() {
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "path=%s host=%s", r.URL.Path, r.Header.Get("X-Forwarded-Host"))
}))
defer backend.Close()
target, err := url.Parse(backend.URL)
if err != nil {
panic(err)
}
front := httptest.NewServer(NewReverseProxy(target))
defer front.Close()
response, err := http.Get(front.URL + "/api/items?page=2")
if err != nil {
panic(err)
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
panic(err)
}
fmt.Println("status:", response.StatusCode)
fmt.Println("body:", strings.TrimSpace(string(body)))
}