Files
partner-tools/partner_geolocalize_usability/models/base_geocoder.py
Stéphan Sainléger e3fa6b8d6e [FIX] partner_geolocalize_usability: avoid Nominatim HTTP 429 rejections
The stock ``base_geocoder._call_openstreetmap`` sends a generic Odoo
``User-Agent`` shared across thousands of instances, which Nominatim
rate-limits and blocks (HTTP 429 "Too many requests"), so mass
geolocation fails regardless of client-side throttling.

Override ``_call_openstreetmap`` to:

- send a deployment-specific ``User-Agent`` read from a new system
  parameter ``partner_geolocalize_usability.user_agent`` (shipped with an
  obvious placeholder so an unconfigured instance fails loudly instead of
  silently reusing the banned generic agent);
- throttle requests to one per second (``time.sleep``) before each call,
  complying with the Nominatim usage policy even when callers loop over
  many partners;
- return ``None`` instead of raising ``IndexError`` when Nominatim
  returns an empty result for an unknown address.

Document the required system parameter in the README and add tests
covering the User-Agent selection, throttling and empty/blank results.
2026-07-21 17:23:43 +02:00

80 lines
3.0 KiB
Python

import logging
import time
import requests
from odoo import api, models
_logger = logging.getLogger(__name__)
# System parameter holding the User-Agent sent to Nominatim.
# Nominatim's usage policy requires a genuine, identifying User-Agent with a
# valid contact. The stock Odoo User-Agent is shared by thousands of instances
# and is rate-limited/blocked (HTTP 429), so we let each deployment set its own.
USER_AGENT_PARAM = "partner_geolocalize_usability.user_agent"
# Placeholder shown until the deployment sets a real value. It intentionally
# does NOT identify any real service so an unconfigured instance fails loudly
# rather than silently reusing a banned generic agent.
USER_AGENT_PLACEHOLDER = (
"SET-ME partner_geolocalize_usability (contact: you@example.invalid)"
)
# Nominatim's public API allows at most 1 request per second. We wait slightly
# more than a second between calls to stay safely under the limit.
NOMINATIM_MIN_INTERVAL = 1.1
class GeoCoder(models.AbstractModel):
_inherit = "base.geocoder"
def _get_nominatim_user_agent(self):
"""Return the configured User-Agent for Nominatim requests.
Falls back to an obvious placeholder so a misconfigured instance is easy
to spot in logs (and so we never silently reuse Odoo's banned agent).
"""
return (
self.env["ir.config_parameter"]
.sudo()
.get_param(USER_AGENT_PARAM, USER_AGENT_PLACEHOLDER)
)
@api.model
def _call_openstreetmap(self, addr, **kw):
"""Query Nominatim with a configurable User-Agent and rate limiting.
Overrides the stock implementation to fix two problems that cause
HTTP 429 ("Too many requests"):
* the shared/generic Odoo User-Agent is blocked by Nominatim -> use a
deployment-specific one from a system parameter;
* bursts of requests exceed the 1 req/s policy -> throttle between calls.
"""
if not addr:
_logger.info("invalid address given")
return None
url = "https://nominatim.openstreetmap.org/search"
try:
headers = {"User-Agent": self._get_nominatim_user_agent()}
# Respect Nominatim's 1 req/s policy. Sleeping before the call keeps
# the throttle effective even when callers loop over many records.
time.sleep(NOMINATIM_MIN_INTERVAL)
response = requests.get(
url, headers=headers, params={"format": "json", "q": addr}
)
_logger.info("openstreetmap nominatim service called")
if response.status_code != 200:
_logger.warning(
"Request to openstreetmap failed.\nCode: %s\nContent: %s",
response.status_code,
response.content,
)
result = response.json()
except Exception as e:
self._raise_query_error(e)
if not result:
return None
geo = result[0]
return float(geo["lat"]), float(geo["lon"])