It’s pretty easy to start building a proxy in Go. The simplest example of creating a (reverse) proxy looks like this:
proxy := httputil.NewSingleHostReverseProxy(targetURL)But one thing that’s not obvious to me yet is the best way to work with upstreams (i.e. targets to proxy to) that are not known beforehand.
For example, Caddy has support for dynamic upstreams. But it looks like you do need to know them beforehand?
So I’m not sure yet what the “best practice approach” is to proxy to a different target for different requests (e.g. performance-wise). But I guess it depends on the exact use-case(s).
I did learn that you can use httputil.ReverseProxy and Rewrite to do something more custom per request:
func NewProxy() *httputil.ReverseProxy { return &httputil.ReverseProxy{ Transport: &http.Transport{ IdleConnTimeout: 2 * time.Minute, MaxIdleConnsPerHost: 32, MaxIdleConns: 100, }, Rewrite: func(pr *httputil.ProxyRequest) { target, ok := ValidatedTargetFromContext(pr.In.Context()) if !ok { // Leave no outbound target, so the transport fails closed. pr.Out.URL = &url.URL{} return } pr.SetURL(target) pr.Out.Host = target.Host }, }}Here ValidatedTargetFromContext only returns a parsed *url.URL after checking its scheme and host against an explicit allowlist (or other policy).
Parsing alone is not sufficient; otherwise this can become an SSRF/open-proxy endpoint.
In this example, the validated target is passed through the request context (ValidatedTargetFromContext).
But this doesn’t feel great (and I haven’t explored what performance looks like when using this yet).
Maybe it’s better to implement a “non-standard” (i.e. not using ServeHTTP) handler and just pass extra information to it?
Something like:
customProxy.ProxyHTTP(w http.ResponseWriter, r *http.Request, targetURL string)Note
Looks like Caddy also started from
httputil.ReverseProxy: