hardening: 20s client timeout on API GETs

A hung read can no longer stall the recon/pineap poll loops (AbortController).
Writes keep no client abort: radio deploys legitimately take up to 45s
server-side. Cache-bumped api.js.
This commit is contained in:
2026-08-19 00:13:03 -05:00
parent 1be93bc24b
commit 0ecd8c3135
2 changed files with 21 additions and 2 deletions
@@ -262,7 +262,7 @@
<script src="js/config.js"></script>
<script src="js/icons.js?v=20260818-7"></script>
<script src="js/api.js?v=20260817-4"></script>
<script src="js/api.js?v=20260819-2"></script>
<script src="js/chart.js?v=20260819-1"></script>
<script src="js/xterm.min.js"></script>
<script src="js/xterm-addon-fit.min.js"></script>
@@ -3,13 +3,32 @@
const PagerAPI = (() => {
let apiBase = '';
let on401 = null;
// Reads are polled and must never hang a page's refresh loop; writes have
// server-side timeouts up to 45s (radio deploys) so they get no client
// abort.
const GET_TIMEOUT_MS = 20000;
async function request(method, path, body) {
const opts = { method, headers: {}, credentials: 'include' };
if (body !== undefined) {
opts.headers['Content-Type'] = 'application/json';
opts.body = JSON.stringify(body);
}
const res = await fetch(apiBase + path, opts);
const ctl = new AbortController();
const timer = method === 'GET' ? setTimeout(() => ctl.abort(), GET_TIMEOUT_MS) : null;
if (timer) opts.signal = ctl.signal;
let res;
try {
res = await fetch(apiBase + path, opts);
} catch (e) {
if (timer && e && e.name === 'AbortError') {
const error = new Error('Request timed out');
error.status = 0;
throw error;
}
throw e;
} finally {
if (timer) clearTimeout(timer);
}
if (res.status === 401) {
if (on401) on401();
throw new Error('unauthorized');