get-client-ip
Extract the real client IP from HTTP requests, safely behind proxies.
Works standalone or as Express/NestJS middleware.
Highlights ✨
- Spoof-resistant — walks
X-Forwarded-Forfrom the right and stops at the first untrusted hop, like Expresstrust proxy - Works without a framework — raw
node:http, Koactx.req, Next.js pages API, Express v4/v5, Fastify, NestJS - Opt-in vendor headers —
cf-connecting-ip,true-client-ip, and similar are read only when you list them - RFC 7239
Forwarded— quoted strings, IPv6, ports,unknown, and obfuscated identifiers - CIDR trust lists — IPv4 and IPv6, compared as parsed addresses
- Normalized output —
::ffff:1.2.3.4becomes1.2.3.4; IPv6 is lowercase and compressed
Installation 📦
Requires Node.js >= 22.12.
npm install get-client-ip
# or
pnpm add get-client-ip
Quick Start 🚀
import express from "express";
import { getClientIp } from "get-client-ip";
const app = express();
// Middleware — populates req.clientIp and req.clientIps
app.use(getClientIp);
app.get("/me", (req, res) => {
res.json({ ip: req.clientIp, ips: req.clientIps });
});
// Or standalone — call directly in a route
app.get("/ip", (req, res) => {
res.json({ ip: getClientIp(req) });
});
app.listen(3000);
Without a framework:
import http from "node:http";
import { getClientIp } from "get-client-ip";
http.createServer((req, res) => res.end(getClientIp(req))).listen(3000);
API Reference 📖
getClientIp(req, options?): string | undefined
Returns the client IP, or undefined when no address is known. Also sets req.clientIp and req.clientIps. Throws if req is missing or options are invalid.
Passing it straight to app.use(getClientIp) runs it as middleware with default options.
getClientIpMiddleware(options?)
Returns (req, res, next) middleware that uses options. It validates options once, at creation.
import { DEFAULT_TRUSTED_PROXIES, getClientIpMiddleware } from "get-client-ip";
app.use(getClientIpMiddleware({ trustedProxies: [...DEFAULT_TRUSTED_PROXIES, "203.0.113.0/24"] }));
Options
| Option | Type | Default | Description |
|---|---|---|---|
trustedProxies | string[] | DEFAULT_TRUSTED_PROXIES | IPs or CIDRs of proxies whose forwarding headers you trust. [] trusts none. |
forwardedHeader | "x-forwarded-for" | "forwarded" | "x-forwarded-for" | The header your proxies append to. The other one is ignored. |
trustedHeaders | string[] | [] | Single-IP headers your edge always overwrites, e.g. ["cf-connecting-ip"]. Checked in order. |
DEFAULT_TRUSTED_PROXIES is 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, ::1/128, fc00::/7, and fe80::/10. CGNAT (100.64.0.0/10) is not trusted.
req.clientIp and req.clientIps
req.clientIp; // string | undefined — the client IP
req.clientIps; // [string, ...string[]] | undefined — the client, then each trusted hop toward your server
The package augments the Express Request type, so no manual declarations are needed.
How the IP Is Resolved 📋
trustedHeaders— the first valid IP from a listed header, only when the socket peer is a trusted proxy.req.ip— when it is a valid IP outsidetrustedProxies. Express and Fastify set it from their own trust settings.- If
req.ipis itself a trusted proxy (e.g. Express withouttrust proxybehind nginx), it is the proxy, not the client. The chain is walked as in step 4.req.ipis kept only when no untrusted hop is found.
- If
- Socket address — when the socket peer is not a trusted proxy, headers are ignored.
- Forwarding chain — read
X-Forwarded-For(orForwarded) from the right. Skip each trusted proxy. Return the first untrusted address.- If every hop is trusted, return the leftmost one.
- An
unknown, obfuscated (_hidden), or invalid hop stops the walk. The nearest trusted hop is returned.
Example: nginx on 10.0.0.5 appends the peer it saw. A client sends X-Forwarded-For: 6.6.6.6, so your app receives 6.6.6.6, 203.0.113.9. The result is 203.0.113.9, not the spoofed 6.6.6.6.
Deployment Recipes 🧭
| Setup | Configuration |
|---|---|
| nginx, HAProxy, or a load balancer on a private IP | Defaults |
| Express behind a proxy | Defaults work; setting Express trust proxy also works |
| Cloudflare in front of a private load balancer | { trustedHeaders: ["cf-connecting-ip"] } |
Proxy that writes RFC 7239 Forwarded | { forwardedHeader: "forwarded" } |
| Proxy on a public IP | { trustedProxies: [...DEFAULT_TRUSTED_PROXIES, "<proxy CIDR>"] } |
| Directly on the internet | { trustedProxies: [] } |
NestJS 🧩
import { Controller, Get, Req } from "@nestjs/common";
import type { Request } from "express";
import { getClientIp } from "get-client-ip";
@Controller()
export class AppController {
@Get("ip")
getIp(@Req() req: Request) {
return { ip: getClientIp(req) };
}
}
Security 🛡️
- List only headers your edge overwrites. A
trustedHeadersentry is client-controlled unless every request passes through that edge. Make your origin reachable only through it. - Pick the header your proxy writes. If your proxy appends
X-Forwarded-For, a client-sentForwardedheader is ignored, and vice versa. - Clients inside a trusted range can spoof. A host on a trusted network can send any
X-Forwarded-For. NarrowtrustedProxiesto your proxies' addresses when that matters. - An untrusted
req.ipis trusted as-is. If your framework's proxy trust is looser thantrustedProxies,req.ipcan come from a client-supplied header.
Breaking Changes in v5 ⚠️
- Requires Node.js >= 22.12.
X-Forwarded-Foris read from the right. v4 returned the leftmost entry, which any client can set. Now the first untrusted hop from the right is returned.- Vendor headers need opt-in.
cf-connecting-ip,true-client-ip,fastly-client-ip,x-appengine-user-ip,cf-pseudo-ipv4,x-client-ip,x-real-ip,x-cluster-client-ip,forwarded-for, andx-forwardedare ignored unless listed intrustedHeaders. - A trusted
req.ipno longer wins. Whenreq.ipis insidetrustedProxies(Express withouttrust proxybehind a private or loopback proxy), the forwarding chain is walked. v4 returned the proxy's address. PasstrustedProxies: []to keep the v4 result. Forwardedneeds opt-in. SetforwardedHeader: "forwarded".X-Forwarded-Foris then ignored.- The second argument is now
options.getClientIp(req, res)no longer means middleware; usegetClientIp(req, res, next),app.use(getClientIp), orgetClientIpMiddleware(). req.clientIpschanged meaning. It is the client followed by the trusted hops toward your server, ending with the socket peer. v4 listed every header entry.- Output is normalized. IPv4-mapped IPv6 (
::ffff:1.2.3.4) becomes IPv4, includingreq.ip. IPv6 is lowercase and compressed, and its zone ID is dropped (fe80::1%eth0becomesfe80::1). - A quoted
X-Forwarded-Forentry is an invalid hop. v4 stripped the quotes from"203.0.113.9". Proxies never quote it, so a quoted entry is client-written and stops the walk. - The middleware always sets
req.clientIp. It isundefinedwhen no address resolves. v4 left the property untouched. - IPv6 trust uses real CIDR matching.
fc::1is no longer trusted (it is not infc00::/7).fe80::/10now coversfe80::–febf::. 0.0.0.0and::are invalid hops.
Credits 🙏
Inspired by Petar Bojinov's work on client IP detection.
Contributions 🤝
- Open an issue or feature request
- Submit a PR to improve the package
- Star the repo if you find it useful