Go

Build a reverse proxy with httputil.ReverseProxy. Why is Rewrite preferred over Director?

Question 476HardGo 1.22 to 1.25

httputil.ReverseProxy handles hop-by-hop header removal, streaming, trailers, HTTP/2, protocol upgrades (WebSockets) and error handling. Go 1.20 added the Rewrite hook, which receives a *ProxyRequest with both the inbound (In) and outbound (Out) requests.

func newProxy(target *url.URL) *httputil.ReverseProxy {
	return &httputil.ReverseProxy{
		Rewrite: func(pr *httputil.ProxyRequest) {
			pr.SetURL(target)  // scheme, host, path joining; rewrites Host header
			pr.SetXForwarded() // X-Forwarded-For/Host/Proto from pr.In
			pr.Out.Header.Set("X-Request-Id", pr.In.Header.Get("X-Request-Id"))
		},
		ModifyResponse: func(resp *http.Response) error {
			resp.Header.Del("Server")
			return nil
		},
		ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
			http.Error(w, "upstream unavailable", http.StatusBadGateway)
		},
		FlushInterval: -1, // flush immediately (SSE / streaming)
	}
}

Why Rewrite: with the old Director, the proxy removes hop-by-hop headers after Director runs, so a malicious client could send Connection: X-Forwarded-For and get headers your Director set stripped. Director also appends to any client-supplied X-Forwarded-For by default (spoofable), while with Rewrite the forwarded headers are removed from Out and only set if you call SetXForwarded. Setting both hooks is an error. NewSingleHostReverseProxy still uses Director and does not rewrite the Host header — a common bug with virtual-hosted upstreams. Also tune the proxy's Transport (idle conns per host) since it is a high-fan-in client.

More on Standard Library, HTTP & Systems Design in Go

All 35 Standard Library, HTTP & Systems Design in Go questions