fix(traffic): say which upstream declined a road load

Street Traffic draws on OpenStreetMap for geometry and TomTom for flow,
and the row said "Road data temporarily unavailable" whichever one
failed. The reporter of #661 went and verified a TomTom key while every
public Overpass mirror was refusing their network with HTTP 406 — a
status fetchRoads already had and dropped on the floor.

Classify the refusal where the layer talks to Overpass: 429 is
"Overpass rate-limited", 504 is "Overpass timed out", anything else is
"Overpass refused the road query (HTTP <status>)". The first two are the
words alpr's Overpass source already uses; the status is what says which
upstream declined, which a single-source layer never has to.

The error carries its status as a RoadRequestError, following
LiveSourceError in src/sources/live/contract.js, so a caller can branch
on the code without parsing English. The catch promotes a classified
refusal and keeps the general line for everything else — the branch is
on the error type, not on its text, because a dropped socket throws
whatever the platform felt like saying and "Failed to fetch" on a panel
row helps nobody.

Retries, backoff, caching and the mirror rotation are untouched.
This commit is contained in:
daikaginza committed 2026-09-21 10:49:37 +07:00
1 parent 0dbde1e36c
commit c24399e99d
6 files changed
+149 -4

No files matched your search

+1
View File
@@ -3242,6 +3242,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.
### Share-link v2 layer state (August 2026)