fix: Locate returning 429/504 and silently-empty halal results

The Locate tab (mosque/halal/cemetery) hardcoded a single public
Overpass API endpoint. Public Overpass instances are individually
flaky under load — confirmed live: the same query returned 429 from
one mirror and 504 from the primary a few minutes apart, with no
retry or fallback, surfacing as a hard failure to the user.

Added a short fallback chain (overpass-api.de, then
overpass.kumi.systems) that retries on 429/503/504 and only reports
failure once every mirror has failed, with an honest 'try again
shortly' message distinct from a genuine empty-results state.

A third mirror (overpass.osm.ch) was tried too but dropped after
finding something worse than a failure: it returned a 'successful'
200 with 0 results for a query the other two mirrors correctly answer
with 30 (diet:halal=yes near Kuala Lumpur) — a stale/incomplete
regional replica that would have short-circuited the fallback loop
and silently told users nothing was nearby when it actually was.

e2e-khairat-lifestyle.cjs gains a tightened assertion (halal search
must return >0 results at a known-good test coordinate, not just
'results or empty') to catch this exact silently-wrong-mirror class of
bug in the future, plus a filter so expected fallback-path 429/504s
don't fail the 'no console errors' check — those are the retry
mechanism working, not a bug. Verified live against the deployed app:
30 real results returned.
This commit is contained in:
2026-08-14 15:14:48 +08:00
parent 33a821e61d
commit 23f32872a5
2 changed files with 68 additions and 12 deletions
+49 -9
View File
@@ -1,7 +1,22 @@
// Nearby-place search via the public OpenStreetMap Overpass API — free,
// keyless, real community-sourced data. Deliberately never fabricates
// listings: an empty result means nothing was found nearby, shown as such.
const OVERPASS_URL = 'https://overpass-api.de/api/interpreter';
//
// The public Overpass instances are individually flaky under load — the
// same query can return 429 (rate-limited) from one mirror and 200 from
// another a moment later. A single hardcoded endpoint means any one
// mirror having a bad minute breaks the feature outright, so this tries
// a short list of independent public mirrors in order and only reports
// failure once all of them have failed.
// overpass.osm.ch was tried here too, but confirmed to return a "successful"
// 200 with an empty/incomplete result set for queries the other two mirrors
// answer correctly (e.g. diet:halal=yes near Kuala Lumpur: 0 results vs 30) —
// worse than an outright failure, since a 200 short-circuits the fallback
// loop and looks like a legitimate empty search to the user. Dropped.
const OVERPASS_MIRRORS = [
'https://overpass-api.de/api/interpreter',
'https://overpass.kumi.systems/api/interpreter'
];
const QUERIES = {
mosque: tags => `node["amenity"="place_of_worship"]["religion"="muslim"](around:${tags.radius},${tags.lat},${tags.lon});
@@ -25,18 +40,43 @@ function distanceKm(lat1, lon1, lat2, lon2) {
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
/** Searches OpenStreetMap for real nearby places in the given category. Returns [] on no results, throws on network/API failure. */
function fetchWithTimeout(url, options, timeoutMs) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
return fetch(url, { ...options, signal: controller.signal }).finally(() => clearTimeout(timer));
}
/** Searches OpenStreetMap for real nearby places in the given category. Returns [] on no results, throws only if every mirror fails. */
export async function searchNearby(category, lat, lon, radiusMeters = 5000) {
const q = QUERIES[category];
if (!q) throw new Error(`Unknown category: ${category}`);
const query = `[out:json][timeout:20];(${q({ lat, lon, radius: radiusMeters })});out center 30;`;
const res = await fetch(OVERPASS_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'data=' + encodeURIComponent(query)
});
if (!res.ok) throw new Error(`Overpass API returned ${res.status}`);
const data = await res.json();
const body = 'data=' + encodeURIComponent(query);
let lastError = null;
let data = null;
for (const mirror of OVERPASS_MIRRORS) {
try {
const res = await fetchWithTimeout(mirror, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body
}, 12000);
if (res.status === 429 || res.status === 504 || res.status === 503) {
// This mirror is overloaded/rate-limited right now — a different
// one may not be, so keep trying rather than failing immediately.
lastError = new Error(`${mirror} returned ${res.status}`);
continue;
}
if (!res.ok) { lastError = new Error(`${mirror} returned ${res.status}`); continue; }
data = await res.json();
break;
} catch (e) {
lastError = e;
}
}
if (!data) throw new Error(`All location-search mirrors are unavailable right now (${lastError?.message || 'unknown error'}). Try again shortly.`);
return (data.elements || [])
.map(el => {
const elLat = el.lat ?? el.center?.lat;