Fix adsb.lol fallback: send a User-Agent, and stop laundering remote data as local - #17
Fix adsb.lol fallback: send a User-Agent, and stop laundering remote data as local#17Purple10101 wants to merge 2 commits into
Conversation
The proxy wrote every adsb.lol response into LOCAL_DATA_PATH
(/run/readsb/aircraft.json) "so tar1090's backend process can read it". That
file is readsb's own output, rewritten at 1 Hz, so the two writers raced for
it - and because getAircraftData() reads the local file first, a response that
had just been fetched from adsb.lol would come back on the next request
labelled source: 'local'. Remote data was laundered as local data, and the
X-Data-Source header could not be trusted to say where a fix came from.
Serve from an in-process cache instead. Nothing writes to the local file any
more; readsb owns it exclusively.
Alongside that:
- Concurrent requests collapse onto one upstream fetch via a shared inFlight
promise, rather than each issuing its own call.
- A failed refresh leaves the previous cache in place and never rejects, so
a flaky upstream degrades into stale-but-served instead of an error.
- Responses carry X-Data-Age-Ms and X-Data-Stale, and payload.now is stamped
at fetch time and never restamped on serve, so consumers can tell how old
a position actually is. blah2-api's extrapolation refuses to project more
than 5 s, which is what CACHE_TTL_MS (3 s) is sized against.
- MAX_STALE_MS bounds how long a dead upstream keeps being served; past it
the response reports source: 'none' rather than passing off an empty sky
as a successful read.
- Non-200 responses call res.resume() so the socket is drained and freed.
- /health reports cache age and cached aircraft count.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
adsb.lol refuses requests whose User-Agent is missing or too generic:
HTTP 403 "User-Agent too generic; include valid contact info."
Node's https.get sends no User-Agent at all unless one is set, which puts
us in the most-blocked category possible. Every fallback request has been
failing, on every node, since adsb.lol introduced the rule - nothing changed
on our side, and there was no deploy to correlate it with.
Verified A/B against the live API, same URL and same second:
no User-Agent -> 403
curl/7.88.1 -> 200
retina-node/1.0 (+github url) -> 200, 38 aircraft
Note the rule is a blocklist of generic tokens rather than a real contact-info
check - Mozilla/5.0 passes - but a descriptive agent is what they are asking
for and is least likely to be caught when they tighten it. ADSBLOL_USER_AGENT
overrides it, which is the hook for a real contact address; the default points
at the repo because inventing an ops mailbox here would be worse than useless.
Also read the body of a non-200 instead of discarding it. The reason was
sitting in a 52-byte response the whole time, and throwing it away is why this
presented as "no aircraft nearby" rather than "we are being refused" - on
nodes whose local receiver was also dead, it looked exactly like a quiet sky.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ReviewSolid, well-diagnosed fix — the writeup nails a subtle failure mode (403 silently read as "empty sky") and the caching rewrite closes an actual data-integrity bug (adsb.lol responses laundered as Bugs / correctness
No backoff when adsb.lol is persistently down. Nits
What's good
Test coverageThere's no test infrastructure in this repo (no |
|
Split into two PRs so the urgent fix isn't gated on reviewing the cache work:
Bundling them made a PR titled 'fix the 403' that was 94% unrelated caching changes. They're independent and deserve separate review. Both touch |
Two commits, reviewable separately.
1. adsb.lol has been 403-ing every node
Node's
https.getsends noUser-Agentat all unless one is set, which is the most-blocked category possible. Every fallback request has been failing on every node since adsb.lol introduced the rule — nothing changed on our side, and there's no deploy to correlate it with.Combined with the dead
adsb_sourcedefault (fixed separately in retina-node#32), nodes have had no ADS-B ground truth from either source.Verified A/B against the live API, same URL and same second:
curl/7.88.1Mozilla/5.0retina-node/1.0 (+github url)The rule is a blocklist of generic tokens rather than a real contact-info check —
Mozilla/5.0passes — but a descriptive agent is what they're asking for and is least likely to be caught when they tighten it.ADSBLOL_USER_AGENToverrides it, which is the hook for a real contact address; the default points at the repo rather than inventing an ops mailbox.Also: read the body of a non-200 instead of discarding it. The reason was sitting in a 52-byte response the whole time. Throwing it away is why this presented as "no aircraft nearby" rather than "we are being refused" — on nodes whose local receiver was also dead, a hard 403 looked exactly like a quiet sky. That's the part that let it run undetected.
2. Stop writing adsb.lol responses back into readsb's file
Pre-existing work, committed separately. The proxy wrote every adsb.lol response into
/run/readsb/aircraft.json— readsb's own output, rewritten at 1 Hz — so the two raced for the same file. And sincegetAircraftData()reads the local file first, a response just fetched from adsb.lol came back on the next request labelledsource: 'local'. Remote data was laundered as local, andX-Data-Sourcecouldn't be trusted.Now served from an in-process cache; nothing writes to the local file and readsb owns it exclusively. Alongside: concurrent requests collapse onto one upstream fetch, a failed refresh leaves the previous cache in place rather than erroring,
X-Data-Age-Ms/X-Data-Staleexpose staleness,MAX_STALE_MSbounds how long a dead upstream is served, and non-200s drain the socket.Verification
Ran the patched proxy end-to-end against the live API:
Confirmed a 0-aircraft result at 40 nm was a genuinely quiet sky, not dropped data — a raw query with the same UA also returned 0, while 120 nm (what nodes are actually configured for) returned 38.
Not covered
Not yet deployed to any node — the two nodes carrying the tmpfs hot-patch still run the
v0.2.0image. Wants a new image build and a node soak before fleet rollout.🤖 Generated with Claude Code