← 中文资源

简体中文版本

反向代理处理器

用 ReverseProxy 转发请求,写入转发主机头、移除内部调试头,并在上游不可用时返回 502。

阅读英文原文

功能说明

网关层常需要把请求转发到内部服务,同时补充来源信息、剥离不该外泄的调试头。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)))
}