fix: eliminate 40-72s hangs from fetchJSONWithRetry + add frontend timeouts

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
This commit is contained in:
Rene Fichtmueller 2026-04-09 15:22:50 +02:00
parent 5b04fc663f
commit 969595b9b4
3 changed files with 19 additions and 12 deletions

View File

@ -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"}

View File

@ -3674,8 +3674,9 @@ async function loadCommunities(asn) {
const content = document.getElementById('commContent');
card.classList.remove('hidden');
content.innerHTML = '<span style="font-family:var(--mono);font-size:.75rem;color:var(--dim)">Decoding communities…</span>';
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 = '<span style="font-family:var(--mono);font-size:.75rem;color:var(--dim)">No communities found for this ASN.</span>';
@ -3712,8 +3713,9 @@ async function loadIrrAudit(asn) {
const content = document.getElementById('irrContent');
card.classList.remove('hidden');
content.innerHTML = '<span style="font-family:var(--mono);font-size:.75rem;color:var(--dim)">Checking IRR registration via NLNOG IRR Explorer…</span>';
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 = '<span style="font-family:var(--mono);font-size:.75rem;color:var(--dim)">Loading routing history…</span>';
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 = '<span style="font-family:var(--mono);font-size:.75rem;color:var(--dim)">No routing history data available for this ASN.</span>';
@ -3789,8 +3792,9 @@ async function loadAspath(asn) {
const content = document.getElementById('aspathContent');
card.classList.remove('hidden');
content.innerHTML = '<span style="font-family:var(--mono);font-size:.75rem;color:var(--dim)">Loading AS-PATH data…</span>';
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 = '<span style="font-family:var(--mono);font-size:.75rem;color:var(--dim)">No AS-PATH data available for this ASN.</span>'; return; }
@ -3911,8 +3915,9 @@ async function loadHijackMonitor(asn) {
const content = document.getElementById('hijackContent');
card.classList.remove('hidden');
content.innerHTML = '<span style="font-family:var(--mono);font-size:.75rem;color:var(--dim)">Checking hijack status…</span>';
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) {

View File

@ -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 = [];