Build a reverse proxy with httputil.ReverseProxy. Why is Rewrite preferred over Director?
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
- Q474Implement Server-Sent Events (SSE) in net/http. How do http.ResponseController, flushing and WriteTimeout interact?
- Q475Write an HTTP client retry helper with exponential backoff and jitter. What makes a request safe to retry?
- Q477How do you test time-dependent concurrent code deterministically? Explain testing/synctest (Go 1.25).