fyliu Claude Opus 4.6 commited on
Commit
a67b60b
·
1 Parent(s): d8c3cfa

Fix geolocation to resolve public IP for localhost/LAN clients

Browse files

When the client IP is private (127.0.0.1, 192.168.x.x, etc.), the
geolocation endpoint now discovers the server's public IP via
api.ipify.org before querying ip-api.com. This fixes the issue where
local development always fell back to New York.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. backend/api/geolocation.py +29 -2
backend/api/geolocation.py CHANGED
@@ -2,6 +2,7 @@
2
 
3
  from __future__ import annotations
4
 
 
5
  import math
6
 
7
  import httpx
@@ -12,7 +13,7 @@ from ..data_loader import get_route_graph
12
 
13
  router = APIRouter(prefix="/api/geolocation", tags=["geolocation"])
14
 
15
- # Fallback for private / unresolvable IPs
16
  _FALLBACK = {
17
  "country_code": "US",
18
  "city": "New York",
@@ -22,6 +23,14 @@ _FALLBACK = {
22
  }
23
 
24
 
 
 
 
 
 
 
 
 
25
  def _haversine_km(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
26
  """Great-circle distance in km between two lat/lon points."""
27
  r = 6371.0
@@ -52,6 +61,21 @@ def _nearest_airports(lat: float, lon: float, limit: int = 3) -> list[dict]:
52
  ]
53
 
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  @router.get("")
56
  async def geolocate(request: Request):
57
  """Resolve client IP to location, currency, and nearest airports."""
@@ -60,10 +84,13 @@ async def geolocate(request: Request):
60
  if not client_ip:
61
  client_ip = request.client.host if request.client else "127.0.0.1"
62
 
 
 
 
63
  # Try ip-api.com (free, no key, 45 req/min)
64
  try:
65
  async with httpx.AsyncClient(timeout=3.0) as client:
66
- resp = await client.get(f"http://ip-api.com/json/{client_ip}?fields=status,country,countryCode,city,lat,lon")
67
  data = resp.json()
68
  if data.get("status") == "success":
69
  country_code = data.get("countryCode", "US")
 
2
 
3
  from __future__ import annotations
4
 
5
+ import ipaddress
6
  import math
7
 
8
  import httpx
 
13
 
14
  router = APIRouter(prefix="/api/geolocation", tags=["geolocation"])
15
 
16
+ # Fallback for truly unresolvable cases
17
  _FALLBACK = {
18
  "country_code": "US",
19
  "city": "New York",
 
23
  }
24
 
25
 
26
+ def _is_private_ip(ip_str: str) -> bool:
27
+ """Check if an IP address is private/loopback/reserved."""
28
+ try:
29
+ return ipaddress.ip_address(ip_str).is_private
30
+ except ValueError:
31
+ return True
32
+
33
+
34
  def _haversine_km(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
35
  """Great-circle distance in km between two lat/lon points."""
36
  r = 6371.0
 
61
  ]
62
 
63
 
64
+ async def _resolve_ip(client_ip: str) -> str:
65
+ """If client_ip is private (localhost, LAN), discover the public IP."""
66
+ if not _is_private_ip(client_ip):
67
+ return client_ip
68
+ # Ask ip-api.com without an IP — it returns the server's public IP info
69
+ # Or use a lightweight service to get the public IP first
70
+ try:
71
+ async with httpx.AsyncClient(timeout=3.0) as client:
72
+ resp = await client.get("https://api.ipify.org?format=json")
73
+ data = resp.json()
74
+ return data.get("ip", client_ip)
75
+ except Exception:
76
+ return client_ip
77
+
78
+
79
  @router.get("")
80
  async def geolocate(request: Request):
81
  """Resolve client IP to location, currency, and nearest airports."""
 
84
  if not client_ip:
85
  client_ip = request.client.host if request.client else "127.0.0.1"
86
 
87
+ # If IP is private (localhost/LAN), resolve to public IP
88
+ lookup_ip = await _resolve_ip(client_ip)
89
+
90
  # Try ip-api.com (free, no key, 45 req/min)
91
  try:
92
  async with httpx.AsyncClient(timeout=3.0) as client:
93
+ resp = await client.get(f"http://ip-api.com/json/{lookup_ip}?fields=status,country,countryCode,city,lat,lon")
94
  data = resp.json()
95
  if data.get("status") == "success":
96
  country_code = data.get("countryCode", "US")