diff --git a/CHANGELOG.md b/CHANGELOG.md index 419c03e45..8527900d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +- Aircraft track backfill (`/api/opensky-track`, `/api/adsblol/trace`) now + answers 502 when the upstream body exceeds the 5 MB cap, instead of a 200 + whose error body the client read as an empty track. The failure is cached + like other upstream errors, so retries inside the 60 s window do not spend + OpenSky credits (Raushankumar0720, #720, #722). + - Saving a key from Provider Settings works again on Macs where Nix or Homebrew coreutils sit ahead of `/bin` on `PATH`. The credential hardener now spawns Apple's `/bin/chmod -N` by absolute path; GNU `chmod` has no `-N`, diff --git a/server/providers/aircraft/tracks.js b/server/providers/aircraft/tracks.js index 7d9501628..2c7b78a78 100644 --- a/server/providers/aircraft/tracks.js +++ b/server/providers/aircraft/tracks.js @@ -45,7 +45,9 @@ export function trackBackfillProxies() { RESPONSE_CAP_BYTES, ); let body; + let status = upstream.status; if (tooLarge) { + status = 502; body = JSON.stringify({ error: 'Upstream track response too large' }); } else if (!upstream.ok) { // Sanitize upstream error surface; status code is signal enough @@ -53,8 +55,10 @@ export function trackBackfillProxies() { } else { body = text; } - cachePut(key, { at: Date.now(), status: upstream.status, body }); - res.statusCode = upstream.status; + // Cache the 502 like any upstream error: /tracks/all costs OpenSky + // credits, so a retry inside the window must not re-download the body. + cachePut(key, { at: Date.now(), status, body }); + res.statusCode = status; res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.setHeader('Cache-Control', 'no-store'); res.end(body); diff --git a/src/tooling/liveProviders.test.mjs b/src/tooling/liveProviders.test.mjs index 250924b42..38953eff9 100644 --- a/src/tooling/liveProviders.test.mjs +++ b/src/tooling/liveProviders.test.mjs @@ -353,3 +353,31 @@ test('military cooldown bounds untrusted Retry-After and defaults server errors' assert.equal(Number(result.headers['retry-after']), seconds); } }); + +test('track backfill proxy returns 502 on an oversized upstream body and caches it as an error', async (t) => { + const tracks = install(providers.trackBackfillProxies(), true); + let callCount = 0; + t.mock.method(globalThis, 'fetch', async () => { + callCount++; + return { + ok: true, + status: 200, + headers: new Map(), + body: (async function* () { + yield Buffer.alloc(6 * 1024 * 1024, 'x'); + })(), + }; + }); + + const res1 = await tracks('/api/opensky-track', '?icao24=def456'); + assert.equal(res1.statusCode, 502); + assert.deepEqual(JSON.parse(res1.body), { + error: 'Upstream track response too large', + }); + + // Cached as a 502 (never a 200): a retry inside the window neither + // reads as an empty track nor spends OpenSky credits on another download. + const res2 = await tracks('/api/opensky-track', '?icao24=def456'); + assert.equal(res2.statusCode, 502); + assert.equal(callCount, 1); +});