From 34d258b39464a6bc2d323a6503bb87f98c45adef Mon Sep 17 00:00:00 2001 From: Raushankumar0720 Date: Wed, 23 Sep 2026 15:52:21 +0530 Subject: [PATCH 1/2] fix(live): return 502 and prevent caching on oversized track responses (fixes #720) --- server/providers/aircraft/tracks.js | 8 ++++-- src/tooling/liveProviders.test.mjs | 38 +++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/server/providers/aircraft/tracks.js b/server/providers/aircraft/tracks.js index 7d9501628..d9f2a36d5 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; + if (!tooLarge) { + 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..de8681490 100644 --- a/src/tooling/liveProviders.test.mjs +++ b/src/tooling/liveProviders.test.mjs @@ -353,3 +353,41 @@ 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 and skips cache when upstream response exceeds body cap', async (t) => { + const tracks = install(providers.trackBackfillProxies(), true); + let callCount = 0; + t.mock.method(globalThis, 'fetch', async () => { + callCount++; + if (callCount === 1) { + return { + ok: true, + status: 200, + headers: new Map(), + body: (async function* () { + yield Buffer.alloc(6 * 1024 * 1024, 'x'); + })(), + }; + } + return { + ok: true, + status: 200, + headers: new Map(), + body: (async function* () { + yield Buffer.from(JSON.stringify({ path: [] })); + })(), + }; + }); + + 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', + }); + + // Verify failure was NOT cached: second request invokes upstream again and succeeds + const res2 = await tracks('/api/opensky-track', '?icao24=def456'); + assert.equal(res2.statusCode, 200); + assert.deepEqual(JSON.parse(res2.body), { path: [] }); + assert.equal(callCount, 2); +}); From 3a23dc393da2dec08a7870ab10c55841034f0f0b Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Thu, 24 Sep 2026 22:09:17 -0700 Subject: [PATCH 2/2] fix(live): cache the oversized-track 502 like other upstream errors (#722) Keep the new 502, but store it in the 60 s cache as the proxy already does for 404/429: /tracks/all costs OpenSky credits, so an oversized track must not be re-downloaded on every reselect. Update the regression test and credit the fix in CHANGELOG. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 6 ++++++ server/providers/aircraft/tracks.js | 6 +++--- src/tooling/liveProviders.test.mjs | 22 ++++++---------------- 3 files changed, 15 insertions(+), 19 deletions(-) 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 d9f2a36d5..2c7b78a78 100644 --- a/server/providers/aircraft/tracks.js +++ b/server/providers/aircraft/tracks.js @@ -55,9 +55,9 @@ export function trackBackfillProxies() { } else { body = text; } - if (!tooLarge) { - cachePut(key, { at: Date.now(), status, body }); - } + // 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'); diff --git a/src/tooling/liveProviders.test.mjs b/src/tooling/liveProviders.test.mjs index de8681490..38953eff9 100644 --- a/src/tooling/liveProviders.test.mjs +++ b/src/tooling/liveProviders.test.mjs @@ -354,27 +354,17 @@ test('military cooldown bounds untrusted Retry-After and defaults server errors' } }); -test('track backfill proxy returns 502 and skips cache when upstream response exceeds body cap', async (t) => { +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++; - if (callCount === 1) { - return { - ok: true, - status: 200, - headers: new Map(), - body: (async function* () { - yield Buffer.alloc(6 * 1024 * 1024, 'x'); - })(), - }; - } return { ok: true, status: 200, headers: new Map(), body: (async function* () { - yield Buffer.from(JSON.stringify({ path: [] })); + yield Buffer.alloc(6 * 1024 * 1024, 'x'); })(), }; }); @@ -385,9 +375,9 @@ test('track backfill proxy returns 502 and skips cache when upstream response ex error: 'Upstream track response too large', }); - // Verify failure was NOT cached: second request invokes upstream again and succeeds + // 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, 200); - assert.deepEqual(JSON.parse(res2.body), { path: [] }); - assert.equal(callCount, 2); + assert.equal(res2.statusCode, 502); + assert.equal(callCount, 1); });