From e69b0d630c4c74a6a5ad739a5f49cb9dc95d1b2d Mon Sep 17 00:00:00 2001 From: Josh Poole Date: Mon, 24 Aug 2026 09:25:58 +0100 Subject: [PATCH] Send a User-Agent so adsb.lol stops returning 403 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 --- proxy/server.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/proxy/server.js b/proxy/server.js index f6233fef..2891a979 100644 --- a/proxy/server.js +++ b/proxy/server.js @@ -9,6 +9,14 @@ const RECEIVER_LON = parseFloat(process.env.RECEIVER_LON || '0'); const ADSBLOL_RADIUS = parseInt(process.env.ADSBLOL_RADIUS || '40'); const PORT = parseInt(process.env.PROXY_PORT || '3005'); +// adsb.lol refuses requests whose User-Agent is missing or too generic, with +// 403 "User-Agent too generic; include valid contact info." Node's https.get +// sends no User-Agent at all unless one is set, so every request failed closed +// - the fallback was dead fleet-wide and looked like an empty sky. Override +// with a real contact address via ADSBLOL_USER_AGENT where one is available. +const USER_AGENT = process.env.ADSBLOL_USER_AGENT || + 'retina-node/1.0 (+https://github.com/offworldlabs/tar1090-node)'; + const ADSBLOL_API = `https://api.adsb.lol/v2/lat/${RECEIVER_LAT}/lon/${RECEIVER_LON}/dist/${ADSBLOL_RADIUS}`; function fetchUrl(url) { @@ -16,9 +24,17 @@ function fetchUrl(url) { const client = url.startsWith('https') ? https : http; const timeout = 5000; - const req = client.get(url, { timeout }, (res) => { + const req = client.get(url, { timeout, headers: { 'User-Agent': USER_AGENT } }, (res) => { if (res.statusCode !== 200) { - reject(new Error(`HTTP ${res.statusCode}`)); + // Read the body rather than discarding it: adsb.lol states the reason + // for a refusal there, and throwing it away is why a hard 403 was + // indistinguishable from "no aircraft nearby" for as long as it was. + let err = ''; + res.on('data', chunk => { if (err.length < 200) err += chunk; }); + res.on('end', () => { + const detail = err.trim().replace(/\s+/g, ' ').slice(0, 120); + reject(new Error(`HTTP ${res.statusCode}${detail ? ` - ${detail}` : ''}`)); + }); return; }