Determining the real client IP address in a Go HTTP service is deceptively tricky. When your server sits behind one or more reverse proxies, load balancers, or CDN edge nodes, the TCP-levelDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/go-chi/chi/llms.txt
Use this file to discover all available pages before exploring further.
r.RemoteAddr is the IP of the last proxy — not the browser or mobile app making the request. The industry standard workaround — trusting X-Forwarded-For — introduces IP spoofing vulnerabilities unless handled carefully.
Chi ships four ClientIPFrom* middlewares that replace the deprecated RealIP handler. Each targets a specific network topology and stores a canonical netip.Addr in the request context rather than mutating r.RemoteAddr.
Why RealIP is deprecated
The legacy RealIP middleware (still present but marked deprecated) checks True-Client-IP first, then X-Real-IP, and finally takes the leftmost entry from X-Forwarded-For, writing the result to r.RemoteAddr. This approach is vulnerable to IP spoofing:
- An attacker can add a fake
X-Forwarded-Forheader to any request. - If your proxy appends rather than overwrites, the “leftmost” IP is entirely attacker-controlled.
True-Client-IPand similar headers are passed through by default in some CDN products, making them trivially forgeable unless your edge strips them.
ClientIPFrom* middlewares address the threat model directly.
Choosing the right middleware
| Deployment setup | Middleware to use |
|---|---|
| Directly on the public internet, no proxy | middleware.ClientIPFromRemoteAddr |
Behind nginx (X-Real-IP), Cloudflare (CF-Connecting-IP), Apache (X-Client-IP) | middleware.ClientIPFromHeader("<your-trusted-header>") |
| Behind one or more proxies whose IP ranges you can list as CIDRs | middleware.ClientIPFromXFF("10.0.0.0/8", ...) |
| Behind a known, fixed number of proxies with dynamic IPs | middleware.ClientIPFromXFFTrustedProxies(2) |
GetClientIP and GetClientIPAddr work identically regardless of which middleware you chose.
ClientIPFromRemoteAddr
Use this when your server has a direct TCP connection to the public internet with no proxy between it and the client. The middleware reads r.RemoteAddr, strips the port, and stores the parsed netip.Addr in context.
signature
remote_addr_usage.go
::ffff:a.b.c.d; the middleware folds them to plain IPv4 before storage so one logical client maps to one canonical key for logs and rate limits.
ClientIPFromHeader
Use this when a single trusted reverse proxy sits in front of your server and unconditionally overwrites a known header with the real client IP. Examples of headers that are safe to use (assuming the proxy strips inbound values):
X-Real-IP— nginx withngx_http_realip_moduleCF-Connecting-IP— CloudflareX-Client-IP— Apache withmod_remoteip
signature
header_usage.go
ClientIPFromXFF
Use this when your service sits behind one or more reverse proxies and you can enumerate their IP address ranges as CIDR prefixes. The middleware walks the merged X-Forwarded-For chain right-to-left, skipping every IP that falls within a trusted prefix. The first untrusted IP it encounters is the client.
signature
xff_usage.go
ClientIPFromXFF with no arguments returns the rightmost XFF entry without any trust-list filtering, which is safe only if you have exactly one trusted hop directly in front of the server.
The middleware panics at startup if any CIDR prefix string is invalid, catching misconfiguration at boot rather than silently at runtime.
ClientIPFromXFFTrustedProxies
Use this when you know the exact number of trusted reverse proxies in your chain but cannot enumerate their IP ranges — for example, an autoscaling CDN edge with dynamic IPs.
signature
xff_trusted_proxies_usage.go
len(xff) - numTrustedProxies in the merged XFF list. If the chain is shorter than numTrustedProxies, no client IP is set. The middleware panics at startup if numTrustedProxies < 1.
Reading the client IP in handlers
Both retrieval functions read from the same context key set by any of the four middlewares above.signature
get_client_ip.go
GetClientIP returns an empty string if no middleware has set the value. GetClientIPAddr returns the zero netip.Addr; call .IsValid() to check.
Normalization guarantees
All four middlewares apply the same normalization before storage:- IPv4-mapped IPv6 (
::ffff:a.b.c.d) is folded to plain IPv4, so one logical client maps to exactly one canonical address for logs, rate limits, and ACLs. - IPv6 zone identifiers (e.g.,
%eth0) carried in header values are stripped, defending against prefix-check bypass via aliasing.ClientIPFromRemoteAddrpreserves zones for legitimate link-local connections.