You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Right now, getting the IP address a request came from means digging into $_SERVER yourself. The PSR request has it in REMOTE_ADDR, but the mapper to GenericRequest throws the server params away.
Behind a reverse proxy you get the proxy's address, and the client's real one sits in a header like X-Forwarded-For. Anyone can set that header, so it's only read if the request came through a proxy you've declared. Nothing is trusted by default:
useTempest\Http\Ip\TrustedProxiesConfig;
returnnewTrustedProxiesConfig(
proxies: ['10.0.0.0/8'], // or PRIVATE_RANGES, or ANY
);
Addresses and CIDR ranges both work, IPv4 and IPv6. In a chain of hops, the closest one that isn't a trusted proxy is the client. Header names are configurable too, for the likes of Cloudflare. Docs included, under routing.
Notes
ip is now a reserved request parameter name, since it's a property on the Request interface. If a payload has a field called ip and gets mapped onto a custom request object, it'll throw RequestParametersIncludedReservedNames, same as it already does for path, query and so on. Probably worth a line in the release notes.
Went with ip rather than clientIp to keep it short, like Laravel's $request->ip().
The range matching lives in Tempest\Support\Ip rather than the HTTP package, since it seemed generally useful: matches(), matches_any(), is_private().
Included changes
Request::$ip on the interface and in IsRequest
TrustedProxiesConfig and ClientIpResolver in Tempest\Http\Ip, Tempest\Support\Ip functions and PRIVATE_RANGES
PsrRequestToGenericRequestMapper resolves the address, RequestToPsrRequestMapper writes it back for the tester's dispatch, and RequestToObjectMapper carries it onto custom request objects
HttpRouterTester::fromIp(), across all verb methods and makePsrRequest
Perhaps we could make this a bit more exhaustive if we're adding it. SERVER_ADDRREMOTE_ADDR doesn't always contain the client's real IP address, especially behind Cloudflare, Docker and other services. What do you think?
osbre
changed the title
feat(http): expose client IP address on the request
feat(http): resolve the client IP address behind trusted proxies
Aug 1, 2026
@xHeaven You mean REMOTE_ADDR, yes. Updated to include trusted proxies and relevant IP helpers under support package, hopefully that's not too many changes for one PR.
osbre
changed the title
feat(http): resolve the client IP address behind trusted proxies
feat(http): client IP address behind trusted proxies
Aug 1, 2026
Sorry, accidentally did a force-push by clicking GitHub's "Update branch via Rebase" button... wasn't expecting my commit history to go up the list again 👀 makes it look as if I did new changes ugh
That said, I renamed those functions back as requested.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Right now, getting the IP address a request came from means digging into
$_SERVERyourself. The PSR request has it inREMOTE_ADDR, but the mapper toGenericRequestthrows the server params away.This PR adds
Request::$ip:It's
nullwhen the server doesn't report an address. In tests,fromIpsets it:Trusted proxies
Behind a reverse proxy you get the proxy's address, and the client's real one sits in a header like
X-Forwarded-For. Anyone can set that header, so it's only read if the request came through a proxy you've declared. Nothing is trusted by default:Addresses and CIDR ranges both work, IPv4 and IPv6. In a chain of hops, the closest one that isn't a trusted proxy is the client. Header names are configurable too, for the likes of Cloudflare. Docs included, under routing.
Notes
ipis now a reserved request parameter name, since it's a property on theRequestinterface. If a payload has a field calledipand gets mapped onto a custom request object, it'll throwRequestParametersIncludedReservedNames, same as it already does forpath,queryand so on. Probably worth a line in the release notes.iprather thanclientIpto keep it short, like Laravel's$request->ip().Tempest\Support\Iprather than the HTTP package, since it seemed generally useful:matches(),matches_any(),is_private().Included changes
Request::$ipon the interface and inIsRequestTrustedProxiesConfigandClientIpResolverinTempest\Http\Ip,Tempest\Support\Ipfunctions andPRIVATE_RANGESPsrRequestToGenericRequestMapperresolves the address,RequestToPsrRequestMapperwrites it back for the tester's dispatch, andRequestToObjectMappercarries it onto custom request objectsHttpRouterTester::fromIp(), across all verb methods andmakePsrRequest