skip to content

Forwarding to Upstreams

httputil.ReverseProxy forwards a request to another server, with Rewrite, ModifyResponse and ErrorHandler as the seams you customise. Interviewers ask which headers a proxy has to fix.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

5

What does httputil.NewSingleHostReverseProxy do, and how do you serve it in a Go HTTP server?

level: juniorimportance: must knowfreq 60%

answer

  1. one target URL, one handler
  2. it writes a Director for you
  3. target's path is joined in front
  4. the Host header is left alone
  5. mount it on a mux and reuse it

basics

~10 s

httputil.NewSingleHostReverseProxy returns a *httputil.ReverseProxy whose Director points every request at one upstream URL's scheme, host and base path. ReverseProxy implements http.Handler, so you mount it on a ServeMux like any other handler.

solid answer

~40 s

It takes a `*url.URL` target and returns a `*httputil.ReverseProxy` with a `Director` already written for you: the Director sets the outbound request's `URL.Scheme` and `URL.Host` to the target's, and joins the target's path in front of the inbound path. It deliberately leaves `req.Host` alone, so the upstream still sees the Host the client sent. Because `*ReverseProxy` implements `http.Handler`, you serve it by mounting it — `mux.Handle("/api/", proxy)` — and ordinary middleware wraps it like any handler. From there `ServeHTTP` does the work: it clones the request, strips hop-by-hop headers, appends the client's address to `X-Forwarded-For`, performs the round trip, then copies the upstream's status, headers and body back to the client. You create one proxy value at startup and reuse it for every request.

code

go · 10 lines
go
target, err := url.Parse("http://127.0.0.1:9001")
if err != nil {
	log.Fatal(err)
}
proxy := httputil.NewSingleHostReverseProxy(target)

mux := http.NewServeMux()
// *httputil.ReverseProxy implements http.Handler
mux.Handle("/api/", proxy)
log.Fatal(http.ListenAndServe(":8080", mux))

go deeper

for a junior

Be ready to write the four lines: parse a target URL, build the proxy, mount it on a mux, and say that it is an http.Handler. Knowing that one value serves all requests is expected.

for a middle

Explain what the generated Director actually mutates — scheme, host, joined path — and what it leaves alone, and describe the work ServeHTTP does around it: clone, strip, round trip, copy back.

for a senior

Show that you treat the defaults as decisions: which Host the upstream should see, whether the mux prefix belongs on the forwarded path, and what happens to the client when the round trip fails.

for a principal

Frame it as an interface your teams depend on. A shared proxy handler fixes forwarding conventions for every service behind it, so the defaults you leave in place become the platform's contract.

## What the constructor actually builds `httputil.NewSingleHostReverseProxy(target *url.URL) *ReverseProxy` is a convenience wrapper. `httputil.ReverseProxy` is a struct with hooks; the constructor fills in exactly one of them — `Director`, the function that turns the inbound request into the outbound one. Everything else on the struct (`Transport`, `ModifyResponse`, `ErrorHandler`, `ErrorLog`) stays at its zero value, and you set what you need afterwards. The generated Director does three things to the request that will be sent upstream: - sets `URL.Scheme` to the target's scheme (`http` or `https`), - sets `URL.Host` to the target's host and port, - joins the target's path in front of the request's path, so a target of `http://up:9000/base` and an inbound `/dir` produce `/base/dir`. It does **not** touch `req.Host`. That matters: in Go's HTTP client the `Host` field, when non-empty, overrides the host taken from the URL. So the upstream receives the Host name the browser asked for, not the target's. If the upstream does virtual hosting on its own name you have to change that yourself. ## Serving it `(*ReverseProxy).ServeHTTP` gives the type an `http.Handler` implementation, so there is nothing special about wiring it up: ```go mux := http.NewServeMux() mux.Handle("/api/", proxy) ``` One proxy value serves all requests concurrently — it holds no per-request state, so build it once at start-up rather than per request. Because it is a plain handler, logging, authentication or rate-limiting middleware wraps it exactly the way it wraps any other handler, and the proxy simply becomes the innermost one. Note what the mux does *not* do for you: `mux.Handle("/api/", proxy)` matches on the prefix but does not remove it, so the upstream receives `/api/users`, not `/users`. If you want it removed, wrap with `http.StripPrefix("/api", proxy)`. ## What ServeHTTP does around your Director The request the Director mutates is a clone, not the caller's request. Around the hook, `ServeHTTP`: 1. clones the inbound request with the request's context, so a client disconnect cancels the upstream call; 2. runs the Director; 3. removes hop-by-hop headers — the per-connection ones such as `Connection`, `Keep-Alive`, `Transfer-Encoding` and `Upgrade`, plus any header named in the `Connection` header — so they do not leak to the next hop; 4. appends the client's IP address, taken from `RemoteAddr`, to `X-Forwarded-For`; 5. performs the round trip; 6. removes hop-by-hop headers from the response, copies the remaining headers and status to the `http.ResponseWriter`, then streams the body back. If the round trip fails, there is no response to copy, and the proxy writes `502 Bad Gateway` and logs the error. ## The mental model to carry away A Go reverse proxy is not a TCP tunnel. It parses the client's request, constructs a **new** HTTP request of its own, and speaks HTTP to the upstream as a client — which is why the upstream sees whatever headers your hook left on the outbound request and nothing more. That framing explains all the follow-on questions: why the Host header is a decision you own, why the client's address has to be forwarded in a header, and why anything per-connection is dropped at the boundary. ## Minimal shape ```go target, err := url.Parse("http://127.0.0.1:9001") if err != nil { log.Fatal(err) } proxy := httputil.NewSingleHostReverseProxy(target) http.Handle("/api/", proxy) ``` That is a working reverse proxy in five lines, and every later refinement — rewriting the path, deciding what forwarding headers to trust, turning an unreachable upstream into a nicer status — is a field you set on the same struct.

  • Does the upstream see the client's Host header or the target's?
    The client's. The Director built by NewSingleHostReverseProxy sets `URL.Scheme` and `URL.Host` but never touches `req.Host`, and a non-empty `Host` field wins over the URL when the request is sent. If the upstream routes by its own name, set the outbound `Host` yourself in the hook.
  • If the client requests /dir and the target URL is http://up:9000/base, what path does the upstream receive?
    `/base/dir`. The target's path is treated as a base and joined in front of the inbound path. The mux pattern is not removed, so if you mounted the proxy at `/api/` the upstream sees `/base/api/dir` unless you wrap it in `http.StripPrefix`.
  • Do you need one ReverseProxy per request?
    No. It holds no per-request state and is safe for concurrent use, so build one at start-up and mount it. Per-request decisions belong inside the hook functions, which receive the request as a parameter.

It is a receptionist rather than a patch cable: it answers your call itself, dials the internal extension on your behalf, and reads the answer back — the caller never touches the extension.

saying these in an interview costs you the question

  • Thinks a reverse proxy tunnels raw TCP instead of making a new HTTP request
  • Believes the Host header is rewritten to the target by default
  • Says you must copy the upstream status, headers and body yourself
  • Constructs a new proxy value inside every handler invocation
  • Assumes the mux prefix is stripped from the forwarded path
open as a page

In httputil.ReverseProxy, what does the Rewrite hook give you that a Director function does not?

level: middleimportance: should knowfreq 42%

basics

~10 s

Rewrite receives a *httputil.ProxyRequest holding both In, the request the client sent, and Out, the one going upstream, plus the SetURL and SetXForwarded helpers. Director sees only the outbound request, and never the original.

open as a page

What does httputil.ReverseProxy send the client when the upstream is unreachable, and how do you change it?

level: seniorimportance: should knowfreq 46%

basics

~10 s

By default it logs a proxy error through ErrorLog and writes 502 Bad Gateway with an empty body. Set ErrorHandler to own that response instead; it also receives any error returned by ModifyResponse.

open as a page

An httputil.ReverseProxy moved from Director to Rewrite now sends the upstream only one X-Forwarded-For address. Why?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The Director path appends the client's address to whatever chain arrived. The Rewrite path deletes the inbound forwarding headers first and adds nothing unless the hook calls ProxyRequest.SetXForwarded, which writes only the address the proxy itself observed.

open as a page

Which headers does httputil.ReverseProxy remove before sending a request upstream?

level: middleimportance: nice to knowfreq 32%

basics

~10 s

httputil.ReverseProxy removes the per-connection headers - Connection, Proxy-Connection, Keep-Alive, Proxy-Authenticate, Proxy-Authorization, Te, Trailer, Transfer-Encoding and Upgrade - plus any header the Connection header names, on request and response alike.

open as a page