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:
@@ -69,14 +69,25 @@ async function main() {
|
|||||||
const timesInOrder = await page.locator('.times-list').innerText();
|
const timesInOrder = await page.locator('.times-list').innerText();
|
||||||
record('Prayer Times: computes all 6 times for Kuala Lumpur', fajrVisible && /Fajr[\s\S]*Sunrise[\s\S]*Dhuhr[\s\S]*Asr[\s\S]*Maghrib[\s\S]*Isha/.test(timesInOrder), timesInOrder.replace(/\n/g, ' '));
|
record('Prayer Times: computes all 6 times for Kuala Lumpur', fajrVisible && /Fajr[\s\S]*Sunrise[\s\S]*Dhuhr[\s\S]*Asr[\s\S]*Maghrib[\s\S]*Isha/.test(timesInOrder), timesInOrder.replace(/\n/g, ' '));
|
||||||
|
|
||||||
// ── Locate (mosque search via live OpenStreetMap Overpass) ──
|
// ── Locate (mosque + halal search via live OpenStreetMap Overpass) ──
|
||||||
await gotoTab(page, 'Locate');
|
await gotoTab(page, 'Locate');
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
await page.locator('button.btn-primary', { hasText: 'Search nearby' }).click();
|
await page.locator('button.btn-primary', { hasText: 'Search nearby' }).click();
|
||||||
await page.waitForTimeout(8000); // live Overpass API call
|
await page.waitForTimeout(10000); // live Overpass API call, across mirrors if the first is down
|
||||||
const locateResultOrEmpty = await page.locator('.results-list, .empty').isVisible().catch(() => false);
|
const locateResultOrEmpty = await page.locator('.results-list, .empty').isVisible().catch(() => false);
|
||||||
record('Locate: mosque search completes (real results or honest empty state)', locateResultOrEmpty);
|
record('Locate: mosque search completes (real results or honest empty state)', locateResultOrEmpty);
|
||||||
|
|
||||||
|
// Halal food specifically: OSM tagging is sparse for this category
|
||||||
|
// globally, but Kuala Lumpur at this test coordinate is known to have
|
||||||
|
// real diet:halal=yes data (~30 results) — asserting a non-zero count
|
||||||
|
// here catches a mirror silently returning an empty-but-"successful"
|
||||||
|
// response, which looks identical to a legitimate empty search otherwise.
|
||||||
|
await page.locator('.category-toggle button', { hasText: 'Halal food' }).click();
|
||||||
|
await page.locator('button.btn-primary', { hasText: 'Search nearby' }).click();
|
||||||
|
await page.waitForTimeout(10000);
|
||||||
|
const halalResultCount = await page.locator('.result-row').count();
|
||||||
|
record('Locate: halal food search returns real results at a known-good test location', halalResultCount > 0, `${halalResultCount} results`);
|
||||||
|
|
||||||
// ── Quran ──
|
// ── Quran ──
|
||||||
await gotoTab(page, 'Quran');
|
await gotoTab(page, 'Quran');
|
||||||
await page.waitForTimeout(2000);
|
await page.waitForTimeout(2000);
|
||||||
@@ -136,7 +147,12 @@ async function main() {
|
|||||||
record('Neighbourhood: a second account joining by code sees the same posts', secondSeesPost);
|
record('Neighbourhood: a second account joining by code sees the same posts', secondSeesPost);
|
||||||
await secondContext.close();
|
await secondContext.close();
|
||||||
|
|
||||||
record('No uncaught JS console errors during full session', consoleErrors.length === 0, consoleErrors.slice(0, 5).join(' || '));
|
// A 504/429/503 while trying one Overpass mirror before falling through to
|
||||||
|
// the next is the fallback mechanism working as designed, not a bug — the
|
||||||
|
// browser still logs the failed fetch as a console error either way, so
|
||||||
|
// filter those specific expected-noise lines out of this assertion.
|
||||||
|
const realConsoleErrors = consoleErrors.filter(e => !/overpass.*(429|503|504)|Gateway Timeout|Too Many Requests/i.test(e));
|
||||||
|
record('No uncaught JS console errors during full session', realConsoleErrors.length === 0, realConsoleErrors.slice(0, 5).join(' || '));
|
||||||
|
|
||||||
await browser.close();
|
await browser.close();
|
||||||
const passCount = results.filter(r => r.pass).length;
|
const passCount = results.filter(r => r.pass).length;
|
||||||
|
|||||||
+47
-7
@@ -1,7 +1,22 @@
|
|||||||
// Nearby-place search via the public OpenStreetMap Overpass API — free,
|
// Nearby-place search via the public OpenStreetMap Overpass API — free,
|
||||||
// keyless, real community-sourced data. Deliberately never fabricates
|
// keyless, real community-sourced data. Deliberately never fabricates
|
||||||
// listings: an empty result means nothing was found nearby, shown as such.
|
// 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 = {
|
const QUERIES = {
|
||||||
mosque: tags => `node["amenity"="place_of_worship"]["religion"="muslim"](around:${tags.radius},${tags.lat},${tags.lon});
|
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));
|
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) {
|
export async function searchNearby(category, lat, lon, radiusMeters = 5000) {
|
||||||
const q = QUERIES[category];
|
const q = QUERIES[category];
|
||||||
if (!q) throw new Error(`Unknown category: ${category}`);
|
if (!q) throw new Error(`Unknown category: ${category}`);
|
||||||
const query = `[out:json][timeout:20];(${q({ lat, lon, radius: radiusMeters })});out center 30;`;
|
const query = `[out:json][timeout:20];(${q({ lat, lon, radius: radiusMeters })});out center 30;`;
|
||||||
const res = await fetch(OVERPASS_URL, {
|
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',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
body: 'data=' + encodeURIComponent(query)
|
body
|
||||||
});
|
}, 12000);
|
||||||
if (!res.ok) throw new Error(`Overpass API returned ${res.status}`);
|
if (res.status === 429 || res.status === 504 || res.status === 503) {
|
||||||
const data = await res.json();
|
// 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 || [])
|
return (data.elements || [])
|
||||||
.map(el => {
|
.map(el => {
|
||||||
const elLat = el.lat ?? el.center?.lat;
|
const elLat = el.lat ?? el.center?.lat;
|
||||||
|
|||||||
Reference in New Issue
Block a user