From 31224d8bd255222612792e713e3bcf141ea1d70e Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Thu, 24 Sep 2026 22:36:53 -0700 Subject: [PATCH] fix(traffic): name the proxy's own 502/503 accurately (#665) 502 (every mirror unreachable) and 503 (the local limiter, before any mirror is asked) come from our proxy, so they no longer read as a mirror refusing the query. Credit the fix in CHANGELOG and note the mapping in CURRENT-STATE. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 4 +++- docs/CURRENT-STATE.md | 2 +- src/layers/traffic/source.js | 21 ++++++++++++++------- src/layers/traffic/source.test.mjs | 9 +++++++++ 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05f9936a7..527e1a29a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,9 @@ `Overpass refused the road query (HTTP 406)` instead of a general "Road data temporarily unavailable", so a reader is not sent to check a TomTom key when the public OpenStreetMap mirrors are the side that failed. - Failures the layer cannot classify keep the general line. + The proxy's own 502 (every mirror unreachable) and 503 (local limiter busy) + read `Overpass mirrors unreachable` and `Overpass temporarily unavailable`. + Failures the layer cannot classify keep the general line (daikaginza, #665). - Bound the client terrain-height cache at 20 000 entries with least-recently-used eviction, so a long session no longer retains every coordinate it ever resolved. Consumer reads promote their entry and a batch still reports every point it resolved (Pedro Lobato, #594). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 237e0dc1a..59f021aba 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -3483,7 +3483,7 @@ silently demoting every later lookup for the session. - HTTP refusals such as 406 now rotate alongside the existing network, rate-limit, and runtime-error cases. A refusal from one mirror no longer prevents reaching healthy alternatives or persists under the seven-day road/month-long boundary cache TTLs. Concurrent identical queries share one mirror sequence; if it fails, both the initiating and joined callers can use the same last-good data. - A refusal every mirror agrees on is still reported with the first mirror's status and body, so a genuinely malformed query says what upstream said — but only after every mirror has had the chance to answer it. `fetchOverpassPayload` takes injectable endpoints and fetch so the rotation is tested without a live mirror (`src/overpassProxy.test.mjs`). - Every mirror is asked with a User-Agent that names the application, its version and the project address (`OVERPASS_USER_AGENT` in `server/providers/overpass/constants.js`), which is what the OSM API usage policy asks for. Mirrors may refuse a client they cannot identify; such a refusal is never treated as data and costs the fan-out that mirror, so the header is what keeps the list at full strength. -- When the fan-out has nothing left to offer, the traffic row says which upstream declined and how: `Overpass rate-limited`, `Overpass timed out`, or `Overpass refused the road query (HTTP 406)`. Street Traffic draws on two upstreams — OpenStreetMap for geometry, TomTom for flow — so a row that only said "road data unavailable" sent readers to check a TomTom key that was never the problem (#661). `roadRequestError` in `src/layers/traffic/source.js` owns the mapping and tags the error with its status. A refusal whose code is unreadable — the layer takes its source by injection, so an adapter may not supply one — reports `Overpass temporarily unavailable` rather than printing `HTTP undefined`, and anything the layer did not classify at all (a dropped connection, a malformed snapshot) still reads `Road data temporarily unavailable`, because a platform exception is not a sentence to put on a panel. +- When the fan-out has nothing left to offer, the traffic row says which upstream declined and how: `Overpass rate-limited`, `Overpass timed out`, or `Overpass refused the road query (HTTP 406)`; the proxy's own 502 (every mirror unreachable) and 503 (local limiter busy) read `Overpass mirrors unreachable` and `Overpass temporarily unavailable`. Street Traffic draws on two upstreams — OpenStreetMap for geometry, TomTom for flow — so a row that only said "road data unavailable" sent readers to check a TomTom key that was never the problem (#661). `roadRequestError` in `src/layers/traffic/source.js` owns the mapping and tags the error with its status. A refusal whose code is unreadable — the layer takes its source by injection, so an adapter may not supply one — reports `Overpass temporarily unavailable` rather than printing `HTTP undefined`, and anything the layer did not classify at all (a dropped connection, a malformed snapshot) still reads `Road data temporarily unavailable`, because a platform exception is not a sentence to put on a panel. ### Share-link v2 layer state (August 2026) diff --git a/src/layers/traffic/source.js b/src/layers/traffic/source.js index dbcbb67c1..b2c992b12 100644 --- a/src/layers/traffic/source.js +++ b/src/layers/traffic/source.js @@ -60,13 +60,20 @@ export function roadRequestError(status) { ? 'Overpass rate-limited' : code === 504 ? 'Overpass timed out' - : // A source is injected, so a caller's adapter may hand back a - // refusal with no status on it. "HTTP undefined" on a panel row is - // worse than not naming a number, so an unreadable code falls back - // to what `alpr` says when it cannot be more specific either. - code === null - ? 'Overpass temporarily unavailable' - : `Overpass refused the road query (HTTP ${code})`; + : // Our own proxy answers these, not a mirror: 502 when every mirror + // failed at the network level, 503 from its local concurrency + // limiter before any mirror was asked. Neither is a refusal. + code === 502 + ? 'Overpass mirrors unreachable' + : code === 503 + ? 'Overpass temporarily unavailable' + : // A source is injected, so a caller's adapter may hand back a + // refusal with no status on it. "HTTP undefined" on a panel row is + // worse than not naming a number, so an unreadable code falls back + // to what `alpr` says when it cannot be more specific either. + code === null + ? 'Overpass temporarily unavailable' + : `Overpass refused the road query (HTTP ${code})`; return new RoadRequestError(message, { status: code }); } diff --git a/src/layers/traffic/source.test.mjs b/src/layers/traffic/source.test.mjs index f7024d81c..b492223eb 100644 --- a/src/layers/traffic/source.test.mjs +++ b/src/layers/traffic/source.test.mjs @@ -207,6 +207,15 @@ test('an Overpass refusal is named by status, and keeps the status beside the wo 'Overpass refused the road query (HTTP 500)', ); + // 502 and 503 come from the local proxy (mirrors unreachable, local + // limiter busy), so they must not read as a mirror refusing the query. + assert.equal(roadRequestError(502).message, 'Overpass mirrors unreachable'); + assert.equal( + roadRequestError(503).message, + 'Overpass temporarily unavailable', + ); + assert.equal(roadRequestError(503).status, 503); + // An injected source may refuse without a readable code; a row must never // print "HTTP undefined" at a reader. for (const missing of [undefined, null, NaN, 'four-oh-six']) {