httpreverseproxy

httpreverseproxy forwards requests to an upstream with no redirect following, no whole-request timeout, and a base path that acts as a traversal boundary.

Part of nurago, a collection of independent Go packages for backend services.

import "github.com/tecnickcom/nurago/pkg/httpreverseproxy"

Forwards an inbound request to a configured upstream, on top of net/http/httputil.ReverseProxy.

c, err := httpreverseproxy.New(upstreamURL)

router.Handle("GET", "/api/*path", c.ForwardRequest)

Redirects are forwarded, never followed

The default upstream client does not follow redirects. A 3xx is passed back to the client verbatim.

This closes an SSRF vector. A proxy that follows redirects will fetch whatever the upstream points it at, and the upstream can point it at 169.254.169.254, at an internal service, or at a host the proxy can reach and the original client cannot. The proxy’s network position is the privilege being borrowed.

Forwarding the 3xx leaves the client to resolve it, from its own network position rather than yours.

No whole-request timeout, on purpose

The default transport bounds only the wait for response headers, through ResponseHeaderTimeout. There is no deadline on the whole request.

A single overall timeout would truncate the cases a proxy most often carries: Server-Sent Events, long downloads, streaming responses, slow uploads. Cutting a stream at sixty seconds is not a safety measure, it is a bug.

Bounding the header wait keeps the protection that matters, since an upstream that never responds still fails quickly. And a client that disconnects cancels the upstream request, so an abandoned transfer does not continue consuming resources.

The transport also raises the per-host idle connection pool, which the standard default sizes for a client rather than a proxy.

The base path is a boundary

When the configured upstream address carries a base path, that path is enforced. A request whose path resolves outside it through . or .. is rejected with HTTP 400 before the upstream is contacted.

That makes the base path a security boundary rather than a prefix convention. Proxying /api/ to an internal service means a client cannot reach /admin/ by sending ../admin/.

WithLaxBasePath restores transparent forwarding for cases where the upstream is expected to handle traversal itself.

Percent-encoded reserved characters, %2F in particular, are decoded before forwarding, because routing operates on the decoded path. That is what stops an encoded slash from slipping past the check.

X-Forwarded headers need a trusted hop

The default rewrite sets the outbound Host to the upstream host and appends to any inbound X-Forwarded-* headers.

Appending preserves the chain, which is right when your proxy sits behind another trusted one. It also means a client can send an X-Forwarded-For of its own choosing and have it preserved. Rate limiting, geolocation and audit logging built on those headers are only as trustworthy as the hop in front of you. Behind a trusted load balancer, fine. Directly exposed, the inbound values must be stripped.

Only the scheme, host and base path of the configured address are used. Userinfo and query string are dropped.

Errors

The default error handler logs the upstream failure and returns 502 Bad Gateway, with one exception: a client that has gone away does not produce a 502, because there is nobody left to receive it and the event is not an upstream fault.

WithReverseProxy and WithHTTPClient replace the proxy and transport entirely.

Dependencies

This package reaches no external module: it uses only the Go standard library, plus logutil.