Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Client Ip Laravel Package

middlewares/client-ip

A Laravel middleware for reliably resolving the client's real IP address, including requests passing through proxies, load balancers, and CDNs. Supports forwarded headers and configurable trusted proxies for accurate IP detection.

Deep Wiki
Context7

Getting Started

middlewares/client-ip is a single-class PSR-15 middleware. It reads REMOTE_ADDR from the request's server params, optionally consults X-Forwarded-For when the peer is a trusted proxy, and attaches the resolved address to the request as clientIp (configurable). That's the whole package — no service provider, no config file, no facade.

composer require middlewares/client-ip

Where to look first

  • vendor/middlewares/client-ip/src/ClientIp.php — ~40 lines, the entire public API (proxy(), attribute(), process()).
  • vendor/middlewares/utils/src/Ip.php — the actual resolution logic, including CIDR matching and the X-Forwarded-For walk. This is where 90% of behavior lives.
  • README for the two builder methods and the default attribute name.

Minimal first use case (PSR-15 stack)

use Middlewares\ClientIp;

$stack = new Dispatcher([
    (new ClientIp())->proxy('10.0.0.0/8', '172.16.0.0/12'),
    $router,
]);

// Downstream handler:
$ip = $request->getAttribute('clientIp'); // ?string

Put it first in the pipeline so every subsequent middleware (rate limiter, geo lookup, audit logger, allowlist) can read the attribute.

For Laravel specifically: Laravel is not a PSR-15 host. If you only need $request->ip(), you already have it — TrustProxies does the same job. Reach for this package when (a) you're writing a framework-agnostic package that must run on both Laravel and PSR-15 runtimes, or (b) you're embedding a PSR-15 dispatcher (League\Route, a Slim/Mezzio stack) inside a Laravel app. In both cases, wrap it in a thin Laravel middleware that runs ClientIp::process() over a PSR-7 copy of the request and copies the attribute back — see below.

Implementation Patterns

Pattern 1 — Framework-agnostic package with a Laravel adapter

If your package ships PSR-15 middleware, keep ClientIp in the PSR-15 path and add a Laravel bridge only in src/Laravel/:

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Middlewares\ClientIp;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

class ResolveClientIp
{
    public function handle(Request $request, Closure $next)
    {
        $factory = new Psr17Factory();

        $psr = $factory->createServerRequest(
            $request->getMethod(),
            $request->fullUrl(),
            ['REMOTE_ADDR' => $request->server('REMOTE_ADDR')]
        );

        foreach ($request->headers->all() as $name => $values) {
            $psr = $psr->withHeader($name, $values);
        }

        $captured = null;
        $terminal = new class($captured) implements RequestHandlerInterface {
            public function __construct(public ?string &$captured) {}
            public function handle(ServerRequestInterface $request): ResponseInterface
            {
                $this->captured = $request->getAttribute('clientIp');
                return new Response();
            }
        };

        (new ClientIp())
            ->proxy(...config('app.trusted_proxies', []))
            ->attribute('clientIp')
            ->process($psr, $terminal);

        if ($captured !== null) {
            $request->attributes->set('clientIp', $captured);
        }

        return $next($request);
    }
}

The terminal handler is a stub whose only purpose is to let the middleware mutate the request before you read the attribute. Because PSR-7 requests are immutable, you cannot read the attribute off $psr after process() — you must read it inside the handler.

Pattern 2 — Shared trusted-proxy config

Keep one source of truth. Parse TRUSTED_PROXIES from env once and feed it to both ClientIp::proxy() (PSR-15 path) and Laravel's TrustProxies (native path) so a request never gets two different answers:

// config/app.php
'trusted_proxies' => array_filter(array_map('trim', explode(',', env('TRUSTED_PROXIES', '')))),

proxy() accepts both bare IPs and CIDR ranges (10.0.0.0/8, 2001:db8::/32).

Pattern 3 — Custom attribute name to avoid collisions

(new ClientIp())
    ->proxy(...$proxies)
    ->attribute('resolvedClientIp');

Useful when your stack already has a clientIp attribute populated by another middleware (e.g., a CDN-aware resolver), or when you want the PSR-15 result to sit alongside Laravel's own $request->ip() rather than overwrite it.

Pattern 4 — Downstream consumers

Once the attribute is set, downstream code is trivial and framework-independent:

class RateLimiter implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $ip = $request->getAttribute('clientIp', '0.0.0.0');
        if ($this->store->tooMany($ip)) {
            return new Response(429);
        }
        return $handler->handle($request);
    }
}

In the Laravel bridge, expose the same value to controllers via a request macro so call sites read naturally:

// AppServiceProvider::boot()
Request::macro('clientIp', fn () => $this->attributes->get('clientIp'));

Pattern 5 — Testing with synthetic server params

Construct PSR-7 requests with explicit server params; never rely on the SAPI's REMOTE_ADDR:

$request = (new ServerRequest('GET', '/'))
    ->withServerParams(['REMOTE_ADDR' => '203.0.113.7'])
    ->withHeader('X-Forwarded-For', '198.51.100.1, 203.0.113.7');

$result = (new ClientIp())->proxy('203.0.113.7')->process($request, $handler);
// $handler->seen request attribute === '198.51.100.1'

Gotchas and Tips

  • No REMOTE_ADDR → no attribute. The middleware skips withAttribute() entirely when it cannot resolve an address. Downstream getAttribute('clientIp') returns null, which is indistinguishable from "middleware not in the pipeline." Provide a default at the call site, or make the resolver explicit about "unknown."
  • Trusted proxies are opt-in and must be exact. With no proxy() calls, X-Forwarded-For is ignored and REMOTE_ADDR wins. That's safe but wrong behind a load balancer. With proxy('0.0.0.0/0') (a tempting shortcut), anyone can spoof their IP via a header. Pin the actual CIDRs of your LB / ingress.
  • Do not double-trust with Laravel. If TrustProxies already rewrites $request->ip() from forwarded headers, running ClientIp over $request->server('REMOTE_ADDR') on the bridged request is usually fine, but feeding it already-resolved headers is not. Pick one layer as authoritative, ideally TrustProxies, and use ClientIp only for its attribute shape.
  • Header-specific. Middlewares\Utils\Ip walks X-Forwarded-For; it does not read CF-Connecting-IP, True-Client-IP, Forwarded, or X-Real-IP. Behind Cloudflare or a proxy that only sets X-Real-IP, you'll get the edge address, not the client. Wrap or replace the resolver rather than assuming.
  • proxy() is append-only. There is no unproxy() / reset. If you reuse a ClientIp instance across requests (e.g., in a long-running worker), configure it once in the container and treat it as immutable.
  • Immutability bites bridges. process() returns a new ServerRequestInterface; the $psr variable you passed in does not gain the attribute. In Laravel land, always read the attribute inside the terminal handler (or inside the next PSR-15 middleware), never after process() returns.
  • Header casing and arrays. PSR-7 header names are case-insensitive, but when bridging from Illuminate\Http\Request, pass $request->headers->all() as arrays so multi-valued X-Forwarded-For lines survive.
  • IPv6. ::1 and 2001:db8::/32 are handled, but a proxy() entry that mixes an IPv4 CIDR with an IPv6 peer will simply fail the in_array/CIDR check. If your app runs behind a dual-stack balancer, register both families.
  • Silent failure is the debugging story. If the attribute is missing, check in this order: (1) is the middleware actually first in the pipeline? (2) does the request have REMOTE_ADDR? (3) is REMOTE_ADDR inside a declared proxy CIDR? (4) is X-Forwarded-For present and parseable? There are no logs and no exceptions.
  • Attribute name is a string, not a constant. There's no ClientIp::ATTRIBUTE; typos in ->attribute('clientIP') fail silently. Keep the name in a single constant in your own codebase.
  • Package is stable and minimal. Last release 2025-03-23, 16 stars, one class, MIT. Treat the surface as frozen: if you need CDN headers, GeoIP, or ASN data, that's a wrapper around Middlewares\Utils\Ip (or your own resolver), not a feature request.
  • Escape hatch. For a Laravel-only app with no PSR-15 ambitions, the honest recommendation is $request->ip() plus TrustProxies. Adding this package there means adding nyholm/psr7 + a bridge middleware to reimplement what the framework already does.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator