What does httputil.NewSingleHostReverseProxy do, and how do you serve it in a Go HTTP server?
answer
- one target URL, one handler
- it writes a Director for you
- target's path is joined in front
- the Host header is left alone
- mount it on a mux and reuse it
basics
~10 shttputil.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 sIt 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 linestarget, 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
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.
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.
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.
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