go 122 lines · 3 tabs

Building a Reverse Proxy in Go with httputil.ReverseProxy and Header Rewriting

Shared by codesnips Aug 2026
3 tabs
package proxy

import (
	"log"
	"net"
	"net/http"
	"net/http/httputil"
	"time"
)

func New(pool *Pool) *httputil.ReverseProxy {
	p := &httputil.ReverseProxy{
		Rewrite: func(pr *httputil.ProxyRequest) {
			target := pool.Next()
			pr.SetURL(target)
			pr.SetXForwarded()
			pr.Out.Host = target.Host
			pr.Out.Header.Set("X-Proxy-By", "go-edge/1.0")
		},
		ModifyResponse: func(resp *http.Response) error {
			resp.Header.Del("Server")
			resp.Header.Set("X-Cache", "MISS")
			return nil
		},
		ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
			log.Printf("proxy error for %s: %v", r.URL.Path, err)
			http.Error(w, "upstream unavailable", http.StatusBadGateway)
		},
		Transport: &http.Transport{
			DialContext: (&net.Dialer{
				Timeout:   5 * time.Second,
				KeepAlive: 30 * time.Second,
			}).DialContext,
			ForceAttemptHTTP2:   true,
			MaxIdleConns:        100,
			MaxIdleConnsPerHost: 20,
			IdleConnTimeout:     90 * time.Second,
			TLSHandshakeTimeout: 5 * time.Second,
		},
	}
	return p
}
3 files · go Explain with highlit

A reverse proxy sits in front of one or more backend servers and forwards client requests to them, often adding cross-cutting concerns like TLS termination, header normalization, and upstream selection. Go's standard library ships net/http/httputil.ReverseProxy, which handles the hard parts — streaming bodies, connection reuse, and hop-by-hop header stripping — while leaving the routing and rewriting policy to the caller.

The proxy package tab builds a proxy with a custom Rewrite function rather than the older Director. The Rewrite callback receives a *httputil.ProxyRequest exposing both the inbound In request and the outbound Out request; pr.SetURL points the outbound request at the chosen backend and pr.SetXForwarded populates X-Forwarded-For, X-Forwarded-Host, and X-Forwarded-Proto from the inbound connection. Using SetXForwarded instead of hand-writing those headers avoids the classic spoofing bug where a client-supplied X-Forwarded-For is trusted and appended to. The code also injects an X-Proxy-By header and rewrites Host so the backend sees its own name.

Backend selection lives in Backends pool, a tiny round-robin balancer. Next uses atomic.AddUint64 on a counter and mods by the slice length, so it is safe under concurrent requests without a mutex. parseBackends validates each URL up front with url.Parse so a misconfiguration fails at startup rather than on the first request.

Two other hooks make the proxy production-shaped. ModifyResponse strips a leaky Server header from upstream responses and stamps an X-Cache marker. ErrorHandler converts upstream failures — a downed backend, a context cancellation — into a clean 502 Bad Gateway instead of leaking Go's default error text, and logs the cause.

The Transport is customized too: bounded dial timeouts, capped idle connections, and ForceAttemptHTTP2 keep a single misbehaving backend from exhausting resources. The main server tab wires it into an http.Server with explicit ReadHeaderTimeout and WriteTimeout, which the default server lacks and which matter when proxying slow clients. Together the files show the real division of labor: ReverseProxy streams bytes, and the surrounding code owns policy — who to talk to, what headers to trust, and how to fail.


Related snips

Share this code

Here's the card — post it anywhere.

Building a Reverse Proxy in Go with httputil.ReverseProxy and Header Rewriting — share card
Link copied