From 969595b9b4c02c7154e75dbb657c1f4088421e9c Mon Sep 17 00:00:00 2001 From: Rene Fichtmueller Date: Thu, 9 Apr 2026 15:22:50 +0200 Subject: [PATCH] fix: eliminate 40-72s hangs from fetchJSONWithRetry + add frontend timeouts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Server: - aspath: announced-prefixes 15s→5s, looking-glass 20s→6s (no retry) - rpki-history: routing-history 20s→6s (no retry) - looking-glass endpoint: 20s→6s (no retry) - communities: bgp-state 12s→6s (no retry) - checkHijacksForAsn: announced-prefixes 15s→6s (no retry) - bgp-updates (pfxLoad): 25s→8s All previously used fetchJSONWithRetry which silently retried on timeout: timeout + 1s wait + timeout = up to 72s cold. Now single attempt, 5-6s cap. Frontend: - loadCommunities: add 8s AbortController - loadIrrAudit: add 8s AbortController - loadRpkiHistory: add 8s AbortController - loadAspath: add 10s AbortController - loadHijackMonitor: add 8s AbortController --- CHANGELOG_PENDING.md | 2 ++ public/index.html | 15 ++++++++++----- server.js | 14 +++++++------- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 0c07a43..054794b 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -21,3 +21,5 @@ {"d":"2026-04-09","t":"FIX","m":"lookup: remove WithRetry on Prefixes+Neighbours (was 8s+8s=16s, now 8s max), add 9s timedFetch hard cap per source"} {"d":"2026-04-09","t":"FIX","m":"validate Phase1: reduce timeout 8s→5s; Phase2 per-check cap 10s→5s; rdns sample 20→3; total cold ≤10s vs 16s before"} {"d":"2026-04-09","t":"FIX","m":"doLookup: add 15s AbortController on initial fetch — skeleton no longer spins indefinitely on slow/failed lookups"} +{"d":"2026-04-09","t":"FIX","m":"aspath/rpki-history/looking-glass/communities: fetchJSONWithRetry with 15-20s timeouts replaced by fetchJSON 5-6s — was causing 40-72s hangs"} +{"d":"2026-04-09","t":"FIX","m":"loadCommunities/loadIrrAudit/loadRpkiHistory/loadAspath/loadHijackMonitor: add AbortController 8-10s — cards no longer spin forever"} diff --git a/public/index.html b/public/index.html index e3ebb66..df88332 100644 --- a/public/index.html +++ b/public/index.html @@ -3674,8 +3674,9 @@ async function loadCommunities(asn) { const content = document.getElementById('commContent'); card.classList.remove('hidden'); content.innerHTML = 'Decoding communities…'; + const ctrl = new AbortController(); setTimeout(() => ctrl.abort(), 8000); try { - const r = await fetch('/api/communities?asn=' + asn); + const r = await fetch('/api/communities?asn=' + asn, { signal: ctrl.signal }); const d = await r.json(); if (!d.communities || !d.communities.length) { content.innerHTML = 'No communities found for this ASN.'; @@ -3712,8 +3713,9 @@ async function loadIrrAudit(asn) { const content = document.getElementById('irrContent'); card.classList.remove('hidden'); content.innerHTML = 'Checking IRR registration via NLNOG IRR Explorer…'; + const ctrl = new AbortController(); setTimeout(() => ctrl.abort(), 8000); try { - const r = await fetch('/api/irr-audit?asn=' + asn); + const r = await fetch('/api/irr-audit?asn=' + asn, { signal: ctrl.signal }); const d = await r.json(); const pct = d.score || 0; const color = pct >= 80 ? 'var(--green)' : pct >= 50 ? 'var(--orange)' : 'var(--red)'; @@ -3761,8 +3763,9 @@ async function loadRpkiHistory(asn) { const content = document.getElementById('rpkiHistContent'); card.classList.remove('hidden'); content.innerHTML = 'Loading routing history…'; + const ctrl = new AbortController(); setTimeout(() => ctrl.abort(), 8000); try { - const r = await fetch('/api/rpki-history?asn=' + asn); + const r = await fetch('/api/rpki-history?asn=' + asn, { signal: ctrl.signal }); const d = await r.json(); if (!d.prefixes || !d.prefixes.length) { content.innerHTML = 'No routing history data available for this ASN.'; @@ -3789,8 +3792,9 @@ async function loadAspath(asn) { const content = document.getElementById('aspathContent'); card.classList.remove('hidden'); content.innerHTML = 'Loading AS-PATH data…'; + const ctrl = new AbortController(); setTimeout(() => ctrl.abort(), 10000); try { - const r = await fetch('/api/aspath?asn=' + asn); + const r = await fetch('/api/aspath?asn=' + asn, { signal: ctrl.signal }); const d = await r.json(); const paths = d && d.paths || []; if (!paths.length) { content.innerHTML = 'No AS-PATH data available for this ASN.'; return; } @@ -3911,8 +3915,9 @@ async function loadHijackMonitor(asn) { const content = document.getElementById('hijackContent'); card.classList.remove('hidden'); content.innerHTML = 'Checking hijack status…'; + const ctrl = new AbortController(); setTimeout(() => ctrl.abort(), 8000); try { - const r = await fetch('/api/hijack-alerts?asn=' + asn); + const r = await fetch('/api/hijack-alerts?asn=' + asn, { signal: ctrl.signal }); const d = await r.json(); let html = ''; if (!d.monitoring) { diff --git a/server.js b/server.js index ffeb5f8..70aba62 100644 --- a/server.js +++ b/server.js @@ -369,7 +369,7 @@ function loadHijackAlerts() { try { return JSON.parse(fs.readFileSync(HIJACK_ALE async function checkHijacksForAsn(asn) { try { const url = `https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS${asn}&${UA}`; - const data = await fetchJSONWithRetry(url, { timeout: 15000 }); + const data = await fetchJSON(url, { timeout: 6000 }); const prefixes = (data && data.data && data.data.prefixes || []).map(p => p.prefix); return prefixes; } catch (_) { return []; } @@ -4734,7 +4734,7 @@ ${html} res.setHeader('Cache-Control', 'public, max-age=3600'); try { const url = `https://stat.ripe.net/data/bgp-state/data.json?resource=AS${asn.replace('AS','')}`; - const data = await fetchJSONWithRetry(url, { timeout: 12000 }); + const data = await fetchJSON(url, { timeout: 6000 }); const rawComms = []; if (data && data.data && data.data.bgp_state) { for (const entry of data.data.bgp_state.slice(0, 50)) { @@ -4862,7 +4862,7 @@ ${html} if (!asn) { res.writeHead(400); return res.end(JSON.stringify({error:'asn required'})); } try { const url = 'https://stat.ripe.net/data/routing-history/data.json?resource=AS' + asn + '&max_rows=100'; - const data = await fetchJSONWithRetry(url, { timeout: 20000 }); + const data = await fetchJSON(url, { timeout: 6000 }); const byOrigin = data && data.data && data.data.by_origin || []; // Flatten: each origin entry has prefixes[] var prefixes = []; @@ -4892,7 +4892,7 @@ ${html} try { // Use RIPE Stat announced-prefixes to get prefixes, then looking-glass for paths var annUrl = 'https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS' + asn; - var annData = await fetchJSONWithRetry(annUrl, { timeout: 15000 }); + var annData = await fetchJSON(annUrl, { timeout: 5000 }); var announced = annData && annData.data && annData.data.prefixes || []; var prefix = announced.length > 0 ? announced[0].prefix : null; if (!prefix) { @@ -4901,7 +4901,7 @@ ${html} } // Get looking-glass data for the first announced prefix var lgUrl = 'https://stat.ripe.net/data/looking-glass/data.json?resource=' + encodeURIComponent(prefix); - var lgData = await fetchJSONWithRetry(lgUrl, { timeout: 20000 }); + var lgData = await fetchJSON(lgUrl, { timeout: 6000 }); var rrcs = lgData && lgData.data && lgData.data.rrcs || []; var paths = []; var seen = new Set(); @@ -4939,7 +4939,7 @@ ${html} if (!resource) { res.writeHead(400); return res.end(JSON.stringify({error:'prefix or asn required'})); } try { const url = `https://stat.ripe.net/data/looking-glass/data.json?resource=${encodeURIComponent(resource)}`; - const data = await fetchJSONWithRetry(url, { timeout: 20000 }); + const data = await fetchJSON(url, { timeout: 6000 }); const rrcs = data && data.data && data.data.rrcs || []; const results = rrcs.slice(0, 15).map(rrc => ({ rrc: rrc.rrc, @@ -5164,7 +5164,7 @@ ${html} } try { const updUrl = `https://stat.ripe.net/data/bgp-updates/data.json?resource=AS${rawAsn}&starttime=${encodeURIComponent(starttime)}&endtime=${encodeURIComponent(endtime)}&limit=1000`; - const raw = await fetchJSON(updUrl, { timeout: 25000 }); + const raw = await fetchJSON(updUrl, { timeout: 8000 }); const updates = (raw && raw.data && raw.data.updates && raw.data.updates.updates) || []; const announcements = [], withdrawals = [], originChanges = [], rpkiIssues = [];