fix: all data fetches use cache:'no-store' — browser-cache poisoning guard

During the nginx outage the proxy URLs briefly served the landing page
WITH cacheable headers; browsers cached that HTML for the proxy URLs and
kept reading it after the origin recovered (satellites 'no satellites',
empty aircraft). no-store on every feed fetch means the browser cache can
never serve (or store) a poisoned body for data endpoints again. The
proxies are no-store server-side anyway — this closes the outage-window
loophole.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-30 09:59:59 +10:00
parent 30b1381a8b
commit fcdcef760a
9 changed files with 11 additions and 11 deletions

View File

@ -52,7 +52,7 @@ export function createAdsbLayer(ctx, spec) {
const byHex = new Map(); const byHex = new Map();
let anyOk = false; let anyOk = false;
const results = await Promise.allSettled( const results = await Promise.allSettled(
spec.endpoints.map((ep) => fetch(`proxy/adsb/${ep}`).then((r) => { spec.endpoints.map((ep) => fetch(`proxy/adsb/${ep}`, { cache: 'no-store' }).then((r) => {
if (!r.ok) throw new Error(String(r.status)); if (!r.ok) throw new Error(String(r.status));
return r.json().then((d) => ({ ep, d })); return r.json().then((d) => ({ ep, d }));
})) }))

View File

@ -146,7 +146,7 @@ export default function create(ctx) {
async function poll() { async function poll() {
let res; let res;
try { try {
res = await fetch(buildUrl()); res = await fetch(buildUrl(), { cache: 'no-store' });
} catch (err) { } catch (err) {
ui.setStatus('aircraft', 'network error — retrying', 'warn'); ui.setStatus('aircraft', 'network error — retrying', 'warn');
return; return;
@ -217,7 +217,7 @@ export default function create(ctx) {
replayInFlight = true; replayInFlight = true;
lastReplayT = tSec; lastReplayT = tSec;
try { try {
const res = await fetch(`history/aircraft?t=${tSec}`); const res = await fetch(`history/aircraft?t=${tSec}`, { cache: 'no-store' });
if (isLive) return; // clock snapped back to live mid-fetch — discard if (isLive) return; // clock snapped back to live mid-fetch — discard
if (res.status === 404) { if (res.status === 404) {
bc.removeAll(); bc.removeAll();

View File

@ -34,7 +34,7 @@ export default function create(ctx) {
if (!loaded) ui.setStatus('firms', 'fetching detections…', 'warn'); if (!loaded) ui.setStatus('firms', 'fetching detections…', 'warn');
let res; let res;
try { try {
res = await fetch('proxy/firms'); res = await fetch('proxy/firms', { cache: 'no-store' });
} catch { } catch {
ui.setStatus('firms', 'network error', 'err'); return; ui.setStatus('firms', 'network error', 'err'); return;
} }

View File

@ -120,7 +120,7 @@ export function createGeoJsonLayer(ctx, spec) {
async function poll() { async function poll() {
let res; let res;
try { try {
res = await fetch(spec.url); res = await fetch(spec.url, { cache: 'no-store' });
} catch { } catch {
ui.setStatus(spec.id, 'network error — retrying', 'warn'); ui.setStatus(spec.id, 'network error — retrying', 'warn');
return; return;

View File

@ -47,7 +47,7 @@ export default function create(ctx) {
async function poll() { async function poll() {
let res; let res;
try { try {
res = await fetch(A.proxyMil); res = await fetch(A.proxyMil, { cache: 'no-store' });
} catch (err) { } catch (err) {
ui.setStatus('military', 'network error — retrying', 'warn'); ui.setStatus('military', 'network error — retrying', 'warn');
return; return;

View File

@ -26,7 +26,7 @@ export default function create(ctx) {
ui.setStatus('radio', 'loading stations…', 'warn'); ui.setStatus('radio', 'loading stations…', 'warn');
let res; let res;
try { try {
res = await fetch('proxy/rg/ara/content/places'); res = await fetch('proxy/rg/ara/content/places', { cache: 'no-store' });
} catch { } catch {
ui.setStatus('radio', 'network error', 'err'); return; ui.setStatus('radio', 'network error', 'err'); return;
} }
@ -89,7 +89,7 @@ export default function create(ctx) {
p.hidden = false; p.hidden = false;
let items = []; let items = [];
try { try {
const res = await fetch(`proxy/rg/ara/content/page/${encodeURIComponent(meta.placeId)}/channels`); const res = await fetch(`proxy/rg/ara/content/page/${encodeURIComponent(meta.placeId)}/channels`, { cache: 'no-store' });
const j = await res.json(); const j = await res.json();
for (const block of (j.data && j.data.content) || []) { for (const block of (j.data && j.data.content) || []) {
for (const it of block.items || []) { for (const it of block.items || []) {

View File

@ -53,7 +53,7 @@ export default function create(ctx) {
lastKey = key; lastKey = key;
inFlight = true; inFlight = true;
try { try {
const res = await fetch(`proxy/adsb/lat/${lat}/lon/${lon}/dist/${DIST_NM}`); const res = await fetch(`proxy/adsb/lat/${lat}/lon/${lon}/dist/${DIST_NM}`, { cache: 'no-store' });
if (key !== lastKey) return; // camera moved again mid-fetch — stale if (key !== lastKey) return; // camera moved again mid-fetch — stale
if (!res.ok) { ui.setStatus('radius', `HTTP ${res.status}`, 'warn'); return; } if (!res.ok) { ui.setStatus('radius', `HTTP ${res.status}`, 'warn'); return; }
const data = await res.json(); const data = await res.json();

View File

@ -38,7 +38,7 @@ export default function create(ctx) {
async function load() { async function load() {
ui.setStatus('roadcams', 'loading cameras…', 'warn'); ui.setStatus('roadcams', 'loading cameras…', 'warn');
const results = await Promise.allSettled(REGIONS.map(async (r) => { const results = await Promise.allSettled(REGIONS.map(async (r) => {
const res = await fetch(`proxy/cams/${r.id}`); const res = await fetch(`proxy/cams/${r.id}`, { cache: 'no-store' });
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
return { region: r, cams: await res.json() }; return { region: r, cams: await res.json() };
})); }));

View File

@ -120,7 +120,7 @@ export default function create(ctx) {
async function run() { async function run() {
const sources = CONFIG.satellites.sources; const sources = CONFIG.satellites.sources;
const settled = await Promise.allSettled( const settled = await Promise.allSettled(
sources.map((s) => fetch(`${CONFIG.proxy.celestrak}?${s.q}`).then((res) => { sources.map((s) => fetch(`${CONFIG.proxy.celestrak}?${s.q}`, { cache: 'no-store' }).then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.text(); return res.text();
})) }))