Skip to content

Send a User-Agent so adsb.lol stops returning 403 - #18

Merged
Purple10101 merged 1 commit into
masterfrom
fix/adsblol-user-agent-only
Aug 24, 2026
Merged

Send a User-Agent so adsb.lol stops returning 403#18
Purple10101 merged 1 commit into
masterfrom
fix/adsblol-user-agent-only

Conversation

@Purple10101

Copy link
Copy Markdown

18 insertions, 2 deletions. Urgent — this is a live fleet-wide outage.

The bug

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 adsb.lol 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, which is why it went unnoticed.

Combined with the dead adsb_source default (fixed 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

User-Agent Result
(none — what we send today) 403
curl/7.88.1 200
Mozilla/5.0 200
retina-node/1.0 (+github url) 200, 45 aircraft

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're asking for and 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 rather than inventing an ops mailbox.

Also: read the body of a non-200

The reason was sitting in a 52-byte response the whole time and the code discarded it. That's why a hard 403 surfaced as "0 aircraft" — on nodes whose local receiver was also dead, indistinguishable from a quiet sky. That's the part that let this run undetected, so it's worth fixing alongside the header.

Deployed and confirmed on hardware

Hot-patched onto jonathan-node-1 via docker cp (backup at /opt/proxy/server.js.bak-preUA):

Before After
403s 164 in 5 min 0 in 3 min
X-Data-Source local adsb.lol
Aircraft 0 43–45

Positions sanity-checked against the node's configured location: 43/43 within the 120 nm radius, nearest 52 km, farthest 213 km — genuine local traffic, not another region's feed.

Note

Was previously bundled with the response-cache rewrite in #17. Split out so this can be reviewed and merged on its own. The cache work is now #19 — it touches the same fetchUrl, so whichever lands second needs a rebase.

🤖 Generated with Claude Code

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 is 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, 45 aircraft

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 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. That is
what let it run undetected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@claude

claude Bot commented Aug 24, 2026

Copy link
Copy Markdown

Reviewed the diff (proxy/server.js, +18/-2). Small, well-scoped fix and the root-cause analysis in the description (A/B testing the UA header against the live API, before/after numbers from the hot-patched node) is convincing.

Code quality

  • The fix is minimal and correctly scoped — just the header and the error-body read, no unrelated churn. Matches the PR's own note about splitting this out from Fix adsb.lol fallback: send a User-Agent, and stop laundering remote data as local #17 to keep it reviewable.
  • Comments explain genuinely non-obvious context (why a missing UA causes silent full-fleet failure, why the 403 previously looked like "0 aircraft"), which is exactly the kind of thing worth documenting.
  • Minor nit: the local variable err in the new error-body reader (let err = ''; res.on('data', chunk => { if (err.length < 200) err += chunk; })) holds response body text, not an Error. Something like body/detailBuf would avoid confusion with the Error constructed a few lines later.

Potential bugs

  • The 200-char cap is checked before appending, not after, so a single chunk that arrives when err.length is just under 200 can push err past 200 (bounded by one TCP chunk, not truly unbounded). Since the only caller is the fixed api.adsb.lol endpoint, this is low risk, not adsb.lol-controlled by an attacker in any meaningful way — just flagging it as slightly imprecise vs. the apparent intent of a hard cap.
  • Nothing else stood out — the success path, timeout handling, and JSON-parse path are untouched, and the new error-path listener correctly drains the response stream so the socket can close/be reused rather than hanging.

Performance

  • Reading the error body is bounded (~200 bytes buffered, sliced to 120 for the message) and only happens on non-200 responses, so no meaningful overhead on the hot path.

Security

  • No concerns. USER_AGENT is either the env override or a static string pointing at the public repo — no secrets or PII embedded. Supporting ADSBLOL_USER_AGENT as the escape hatch for real contact info instead of hardcoding it is the right call.

Test coverage

  • There's no test harness anywhere in this repo currently (no test files, no test runner in package.json), so this isn't a regression in coverage — but worth flagging since this is a fix for a full-fleet production outage: a couple of unit tests around fetchUrl (asserting the User-Agent header is sent, and that a non-200 response's body ends up in the rejected Error's message) would give this real regression protection going forward, whenever a test setup gets introduced.

Overall: solid, targeted fix for a real bug, backed by good live verification. Nothing blocking.

@Purple10101
Purple10101 merged commit cbf2b8e into master Aug 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant