skip to content

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

level: middleimportance: nice to knowfreq 32%

answer

  1. per-connection, not end-to-end
  2. a fixed list plus what Connection names
  3. stripped after your hook runs
  4. the response side is scrubbed too
  5. upgrades and trailers get theirs back

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.

solid answer

~40 s

`httputil.ReverseProxy` drops the hop-by-hop set: `Connection`, `Proxy-Connection`, `Keep-Alive`, `Proxy-Authenticate`, `Proxy-Authorization`, `Te`, `Trailer`, `Transfer-Encoding` and `Upgrade`, together with every header whose name appears as a token in the request's own `Connection` header. The same removal runs over the upstream's response before it is copied back to the client. Two exceptions are added back deliberately: if the inbound request advertised `Te: trailers`, the proxy re-sets it so the upstream knows trailers are acceptable; and if the request was a protocol upgrade, the proxy restores `Connection: Upgrade` and the `Upgrade` token so the upgrade can be negotiated end to end. Removal happens *after* your Director or Rewrite hook, so a `Connection` header you set in the hook never reaches the upstream.

code

go · 8 lines
go
proxy.Director = func(req *http.Request) {
	req.URL.Scheme = upstream.Scheme
	req.URL.Host = upstream.Host
	// dropped: Connection is hop-by-hop, and stripping runs after this hook
	req.Header.Set("Connection", "close")
	// forwarded: an ordinary end-to-end header
	req.Header.Set("X-Tenant", "acme")
}

go deeper

for a junior

Know that a Go reverse proxy does not copy headers blindly: a small set describing the connection itself is removed on the way through, and the rest is forwarded.

for a middle

Name the set, add that the Connection header can designate further headers for removal, and explain that stripping happens after your hook and on both request and response.

for a senior

Show how you debug a missing or unexpected header by comparing the outbound map you logged in the hook with what the upstream handler received, and know why upgrades and trailers are re-added.

for a principal

Decide what the gateway scrubs beyond the protocol's list — internal identity or tenancy headers a caller must never be able to set — and make that policy uniform rather than per-service.

## The rule the proxy is implementing Some HTTP header fields describe the message; others describe the single TCP connection the message travelled on. The second kind belongs to one hop only. A proxy that copies them onward is telling the next hop something about a connection it is not part of, and the results range from harmless to genuinely broken — a forwarded `Transfer-Encoding` next to a re-framed body is the classic smuggling shape. `httputil.ReverseProxy` handles this for you and does not ask. ## What is removed The fixed set the package strips is: `Connection`, `Proxy-Connection`, `Keep-Alive`, `Proxy-Authenticate`, `Proxy-Authorization`, `Te`, `Trailer`, `Transfer-Encoding`, `Upgrade`. On top of that, the `Connection` header may itself *name* further headers as hop-by-hop, as a comma-separated token list. A request carrying `Connection: X-Internal-Token` is asking that `X-Internal-Token` not be forwarded, and the proxy honours it by deleting that header too — on both the request and the response. This is the part candidates usually miss: it is not a fixed blacklist, it is a fixed list plus whatever the message declares. The same removal runs over the upstream's response headers before they are copied to the `http.ResponseWriter`, so a `Connection` or `Keep-Alive` that governed the proxy-to-upstream connection does not leak out to the browser. ## Where in the pipeline it happens The removal runs **after** your `Director` or `Rewrite` hook, over the outbound header map. That ordering has a visible consequence: anything hop-by-hop you set inside the hook is thrown away moments later. Setting `Connection: close` in a Director to try to stop the proxy from reusing the upstream connection does nothing at all — connection behaviour towards the upstream is a property of the round tripper, not a header you inject. End-to-end headers you set in the same hook, such as a tenant or trace identifier, survive untouched. ## The two things put back Stripping the whole set unconditionally would break two legitimate cases, so the proxy restores them. **Trailers.** If the inbound request's `Te` field contained the `trailers` token, the proxy re-sets `Te: trailers` on the outbound request. Without it, an upstream that would have sent trailing headers has been told, by the proxy's own scrubbing, that the client cannot accept them. **Protocol upgrades.** The proxy reads the inbound `Upgrade` token before stripping. If there was one, it puts `Connection: Upgrade` and `Upgrade: <token>` back on the outbound request. That is what allows an upgrade handshake to traverse a `ReverseProxy` at all: the upstream must see the upgrade request to answer `101 Switching Protocols`, at which point the proxy stops being an HTTP relay and splices the two connections together. ## Diagnosing it The symptom is almost always "a header I set is not arriving" or "a header I did not expect is arriving". Put the proxy's view and the upstream's view side by side: log the outbound header map at the end of your hook, and log what the upstream handler actually received. If a name is present in the first and absent in the second, check whether it is on the hop-by-hop list or named in `Connection`. If it survived and you wished it had not — a stale `X-Internal-*` from a caller, say — the fix is a `Header.Del` in your hook, because the proxy only strips what the protocol calls per-connection, not what your organisation considers internal. ## Why this is the proxy's job and not yours Once you have seen the list, the temptation is to reproduce it in a hook. Don't. The package applies it after your hook, on both directions, and handles the upgrade and trailer exceptions in the right order. Your hook's job is the routing and the end-to-end headers your services agreed on.

  • If Upgrade is hop-by-hop, how does an upgrade request get through a ReverseProxy at all?
    The proxy reads the inbound `Upgrade` token before stripping, and after stripping puts `Connection: Upgrade` and that token back on the outbound request. The upstream therefore sees a real upgrade request and can answer `101 Switching Protocols`, which the proxy recognises and handles.
  • You set Connection: close in your Director and the upstream never sees it. Why?
    Hop-by-hop removal runs over the outbound header map after the hook, and `Connection` is on the fixed list. Connection reuse towards the upstream is decided by the round tripper the proxy dials through, not by a header you write in the hook.
  • Does the same stripping apply to the upstream's response?
    Yes. Before the response headers are copied to the client, the proxy removes the same hop-by-hop set plus whatever the response's own `Connection` header names, so headers describing the proxy-to-upstream connection do not reach the browser.

A hop-by-hop header is like a note for the courier currently holding the parcel. The next courier gets their own note; passing the old one along tells them about a leg of the journey they were never on.

saying these in an interview costs you the question

  • Thinks the proxy forwards every request header verbatim
  • Believes only Connection itself is removed, not the headers it names
  • Sets a Connection header in a hook and expects the upstream to see it
  • Assumes protocol upgrades cannot pass through a ReverseProxy
  • Thinks the stripping applies to the request but not the response