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

  1. Enable visitors and visitors_geoip, and configure a valid GeoLite2/GeoIP2 City database.
  2. 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.
  3. Observe the POST /visitors/_track request that the tracker JS fires on page load.
  4. 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
  5. 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

Command icon 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

generalredneck created an issue. See original summary.

generalredneck’s picture

Version: 3.0.x-dev » 8.x-2.x-dev

generalredneck’s picture

this 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.