Table of contents
Part of nurago, a collection of independent Go packages for backend services.
import "github.com/tecnickcom/nurago/pkg/httpreverseproxy"
Package httpreverseproxy provides a reverse-proxy client built on top of net/http/httputil.ReverseProxy.
The package wraps ReverseProxy behind a [Client]:
Newconfigures proxy behavior from an upstream base address.Client.ForwardRequestforwards incoming HTTP requests to the target.
When no custom rewrite function is provided, requests are rewritten to the
configured upstream URL and the wildcard path segment is forwarded as the
proxied path. Standard X-Forwarded-* headers are set automatically. A custom
reverse proxy and HTTP transport can be supplied via WithReverseProxy and
WithHTTPClient; the default error handler logs upstream failures and returns
HTTP 502 Bad Gateway.
Defaults and Behavior
The default upstream client never follows redirects (3xx responses are forwarded verbatim, avoiding an SSRF vector) and uses a private, tuned transport that raises the per-host idle-connection pool and bounds only the wait for response headers (via ResponseHeaderTimeout). Because there is no whole-request timeout, streaming responses (Server-Sent Events, long downloads) and slow uploads are forwarded without being truncated; a disconnecting client still cancels the upstream request.
The default rewrite sets the outbound Host header to the upstream host and appends
to any inbound X-Forwarded-* headers, so those should be trusted only behind a
trusted hop. Only the scheme, host, and base path of the configured upstream address
are used; any userinfo or query string in it is dropped. Percent-encoded reserved
characters in the path (notably %2F) are decoded before forwarding, because routing
operates on the decoded path.
When the address carries a base path, that base path acts as a boundary by default:
a request whose path resolves outside it (via . / ..) is rejected with HTTP 400
before the upstream is contacted. Pass WithLaxBasePath to restore transparent
forwarding of . / .. segments (a pass-through proxy where the upstream is the
authorization boundary). The check is a no-op with a custom rewrite/director or when
the address carries no base path.
The check resolves . / .. (via path.Clean) only to make the accept/reject
decision; a request that is accepted is still forwarded verbatim (its trailing slash
and any in-bounds . / .. segments are preserved for the upstream to normalize).
The boundary therefore assumes the upstream resolves paths the same way, and it does
not defend against multiply percent-encoded traversal (e.g. %252e%252e), which
survives a single decode as a literal segment, so put untrusted-input defenses at
the upstream as well.
Observability Behavior
The default error handler logs request method/path/query, trace ID, response
code/status, request/response timing, and the underlying proxy error. The logged
path and query are those of the outbound (rewritten) upstream request, since
ReverseProxy passes the outbound request to the handler. The query and the URL
embedded in the error are redacted via the configured WithRedactFn (default
redact.Default().BytesToString) so query-parameter secrets do not leak into logs. If request
start time is present in context (via httputil request-time helpers), response
duration is computed from it.
A genuine upstream failure is logged at Error level and answered with HTTP 502. When the client went away before the upstream responded (the inbound request context is canceled or its deadline elapsed), it is not an upstream fault: it is logged at Info level under a distinct message with the non-standard 499 code and no response is written to the abandoned connection.
Only transport failures and base-path rejections are logged; a successfully forwarded
request (including one where the upstream returns a 4xx or 5xx response) produces no
entry here, because that is a successful round trip. Add access logging by wrapping
Client.ForwardRequest in middleware, or set ModifyResponse on a proxy passed via
WithReverseProxy.
When To Use
- A service fronts an internal upstream and needs to forward requests with consistent logging.
- You want httputil.ReverseProxy behavior without repeating the director and error-handler boilerplate.
Example
// Stand-in upstream service that echoes the path it received.
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// The example intentionally reflects the request path to show how it is
// rewritten; a real upstream would not echo untrusted input.
fmt.Fprintf(w, "upstream received %s", r.URL.Path) //nolint:gosec // G705: illustrative echo
}))
defer upstream.Close()
client, err := httpreverseproxy.New(upstream.URL + "/v2")
if err != nil {
panic(err)
}
// Register the proxy under a catch-all route; httprouter injects the matched
// parameters into the request context that the default rewrite reads.
router := httprouter.New()
router.HandlerFunc(http.MethodGet, "/proxy/*path", client.ForwardRequest)
edge := httptest.NewServer(router)
defer edge.Close()
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, edge.URL+"/proxy/users", nil)
if err != nil {
panic(err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer func() { _ = resp.Body.Close() }()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
// Output: upstream received /v2/users
Full source is in example_httpreverseproxy_test.go. More runnable examples are on pkg.go.dev.
Dependencies
Importing this package pulls 1 external module:
github.com/julienschmidt/httprouter