From 2e84a12fbd0a07c7dc0752e2248f30e55185459e Mon Sep 17 00:00:00 2001 From: c4ch3c4d3 Date: Wed, 19 Aug 2026 00:13:03 -0500 Subject: [PATCH] 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. --- .../remote_access/pager-webui/www/index.html | 2 +- .../remote_access/pager-webui/www/js/api.js | 21 ++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/payload/user/remote_access/pager-webui/www/index.html b/payload/user/remote_access/pager-webui/www/index.html index 8d9e1d6..6bb5066 100644 --- a/payload/user/remote_access/pager-webui/www/index.html +++ b/payload/user/remote_access/pager-webui/www/index.html @@ -262,7 +262,7 @@ - + diff --git a/payload/user/remote_access/pager-webui/www/js/api.js b/payload/user/remote_access/pager-webui/www/js/api.js index a7f8df4..c3c2abb 100644 --- a/payload/user/remote_access/pager-webui/www/js/api.js +++ b/payload/user/remote_access/pager-webui/www/js/api.js @@ -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');