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:
parent
30b1381a8b
commit
fcdcef760a
@ -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 }));
|
||||||
}))
|
}))
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 || []) {
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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() };
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -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();
|
||||||
}))
|
}))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user