Problem/Motivation
The visitors_geoip submodule's GeoIpService::city() calls MaxMind's GeoIp2\Database\Reader::city() without a try/catch. That method throws GeoIp2\Exception\AddressNotFoundException by design whenever the queried IP address is not present in the GeoIP database, which is expected, documented behavior for any private/reserved IP (RFC 1918 ranges, loopback, Docker/container networks, etc.), and can also occur for other reasons (corrupt database file, IPs not yet allocated/mapped, etc.).
Because the exception is never caught, it propagates all the way up through Visitors::doLocation() and out of the /visitors/_track route controller, producing an uncaught PHP exception and a 500 response for every tracked page view whenever the visitor's IP isn't resolvable in the database. In practice this means:
- Every page view fails to log in local/dev environments (Docker-based stacks, Lando, DDEV, etc.), since container-internal IPs are always private and never in the GeoIP database.
- Any real-world edge case where a visitor's IP isn't found (bogon ranges, certain proxy/VPN exit IPs, a stale or incomplete database) will also silently break tracking in production, rather than degrading gracefully.
Steps to reproduce
- Enable
visitorsandvisitors_geoip, and configure a valid GeoLite2/GeoIP2 City database. - Make a request to the site from an IP address that is not present in the GeoIP database — the simplest way to trigger this is any private/reserved address (e.g.
10.x.x.x,172.16-31.x.x,192.168.x.x), which is exactly what most local/containerized development environments present as the client IP. - Observe the
POST /visitors/_trackrequest that the tracker JS fires on page load. - The request returns HTTP 500. The PHP error log shows:
Uncaught PHP Exception GeoIp2\Exception\AddressNotFoundException: "The address X.X.X.X is not in the database." at .../vendor/geoip2/geoip2/src/Database/Reader.php line 253
- No hit is recorded for the page view.
Proposed resolution
Wrap the reader call in GeoIpService::city() in a try/catch for GeoIp2\Exception\GeoIp2Exception (the common base class for AddressNotFoundException, AuthenticationException, HttpException, and OutOfQueriesException), returning NULL on failure — matching the NULL-safe contract the interface and its caller already expect (Visitors::doLocation() already checks if (!$location) { return NULL; } immediately after calling city()).
public function city($ip_address) { if (is_null($this->reader)) { return NULL; } try { $record = $this->reader->city($ip_address); } catch (GeoIp2Exception $e) { // Private/reserved IPs (common in local dev) and other lookup // failures (corrupt database, etc.) aren't in the GeoIP database. return NULL; } return $record; }
A patch implementing this is attached / linked in the comments.
Remaining tasks
- Review and merge the patch/MR.
- Confirm whether the same unguarded call pattern exists elsewhere in
visitors_geoip(e.g. region/city rebuild services) and needs the same treatment. - Consider a test case covering a lookup against a known-absent (e.g. private-range) IP address.
User interface changes
None.
API changes
None. VisitorsGeoIpInterface::city()'s documented contract (nullable return) is unchanged — this fix only makes the implementation actually honor that contract instead of throwing.
Data model changes
None.
Issue fork visitors-3618588
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
generalredneckComment #5
generalredneckthis branch targets 8.x-2.x, not the default 3.0.x branch that gets auto-checked-out for new issue forks. On 3.0.x, the visitors module no longer does its own GeoIP lookups. That responsibility has moved to a separate contrib project, drupal/maxmind (via the maxmind_geoip.lookup service). So this patch only applies to 8.x-2.x, where visitors_geoip/src/Service/GeoIpService.php still owns the lookup directly.
For what it's worth, I checked drupal/maxmind's maxmind_geoip/src/Service/GeoIpService.php and it has the exact same bug, verbatim ($this->reader->city($ip_address) called with no try/catch around AddressNotFoundException). If 3.0.x is the branch you want fixed, this same patch would need to be filed as a separate issue against drupal/maxmind instead, since that's the project that actually owns the affected code now.