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:
parent
5b04fc663f
commit
969595b9b4
@ -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":"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":"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":"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"}
|
||||||
|
|||||||
@ -3674,8 +3674,9 @@ async function loadCommunities(asn) {
|
|||||||
const content = document.getElementById('commContent');
|
const content = document.getElementById('commContent');
|
||||||
card.classList.remove('hidden');
|
card.classList.remove('hidden');
|
||||||
content.innerHTML = '<span style="font-family:var(--mono);font-size:.75rem;color:var(--dim)">Decoding communities…</span>';
|
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 {
|
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();
|
const d = await r.json();
|
||||||
if (!d.communities || !d.communities.length) {
|
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>';
|
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');
|
const content = document.getElementById('irrContent');
|
||||||
card.classList.remove('hidden');
|
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>';
|
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 {
|
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 d = await r.json();
|
||||||
const pct = d.score || 0;
|
const pct = d.score || 0;
|
||||||
const color = pct >= 80 ? 'var(--green)' : pct >= 50 ? 'var(--orange)' : 'var(--red)';
|
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');
|
const content = document.getElementById('rpkiHistContent');
|
||||||
card.classList.remove('hidden');
|
card.classList.remove('hidden');
|
||||||
content.innerHTML = '<span style="font-family:var(--mono);font-size:.75rem;color:var(--dim)">Loading routing history…</span>';
|
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 {
|
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();
|
const d = await r.json();
|
||||||
if (!d.prefixes || !d.prefixes.length) {
|
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>';
|
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');
|
const content = document.getElementById('aspathContent');
|
||||||
card.classList.remove('hidden');
|
card.classList.remove('hidden');
|
||||||
content.innerHTML = '<span style="font-family:var(--mono);font-size:.75rem;color:var(--dim)">Loading AS-PATH data…</span>';
|
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 {
|
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 d = await r.json();
|
||||||
const paths = d && d.paths || [];
|
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; }
|
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');
|
const content = document.getElementById('hijackContent');
|
||||||
card.classList.remove('hidden');
|
card.classList.remove('hidden');
|
||||||
content.innerHTML = '<span style="font-family:var(--mono);font-size:.75rem;color:var(--dim)">Checking hijack status…</span>';
|
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 {
|
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();
|
const d = await r.json();
|
||||||
let html = '';
|
let html = '';
|
||||||
if (!d.monitoring) {
|
if (!d.monitoring) {
|
||||||
|
|||||||
14
server.js
14
server.js
@ -369,7 +369,7 @@ function loadHijackAlerts() { try { return JSON.parse(fs.readFileSync(HIJACK_ALE
|
|||||||
async function checkHijacksForAsn(asn) {
|
async function checkHijacksForAsn(asn) {
|
||||||
try {
|
try {
|
||||||
const url = `https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS${asn}&${UA}`;
|
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);
|
const prefixes = (data && data.data && data.data.prefixes || []).map(p => p.prefix);
|
||||||
return prefixes;
|
return prefixes;
|
||||||
} catch (_) { return []; }
|
} catch (_) { return []; }
|
||||||
@ -4734,7 +4734,7 @@ ${html}
|
|||||||
res.setHeader('Cache-Control', 'public, max-age=3600');
|
res.setHeader('Cache-Control', 'public, max-age=3600');
|
||||||
try {
|
try {
|
||||||
const url = `https://stat.ripe.net/data/bgp-state/data.json?resource=AS${asn.replace('AS','')}`;
|
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 = [];
|
const rawComms = [];
|
||||||
if (data && data.data && data.data.bgp_state) {
|
if (data && data.data && data.data.bgp_state) {
|
||||||
for (const entry of data.data.bgp_state.slice(0, 50)) {
|
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'})); }
|
if (!asn) { res.writeHead(400); return res.end(JSON.stringify({error:'asn required'})); }
|
||||||
try {
|
try {
|
||||||
const url = 'https://stat.ripe.net/data/routing-history/data.json?resource=AS' + asn + '&max_rows=100';
|
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 || [];
|
const byOrigin = data && data.data && data.data.by_origin || [];
|
||||||
// Flatten: each origin entry has prefixes[]
|
// Flatten: each origin entry has prefixes[]
|
||||||
var prefixes = [];
|
var prefixes = [];
|
||||||
@ -4892,7 +4892,7 @@ ${html}
|
|||||||
try {
|
try {
|
||||||
// Use RIPE Stat announced-prefixes to get prefixes, then looking-glass for paths
|
// 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 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 announced = annData && annData.data && annData.data.prefixes || [];
|
||||||
var prefix = announced.length > 0 ? announced[0].prefix : null;
|
var prefix = announced.length > 0 ? announced[0].prefix : null;
|
||||||
if (!prefix) {
|
if (!prefix) {
|
||||||
@ -4901,7 +4901,7 @@ ${html}
|
|||||||
}
|
}
|
||||||
// Get looking-glass data for the first announced prefix
|
// Get looking-glass data for the first announced prefix
|
||||||
var lgUrl = 'https://stat.ripe.net/data/looking-glass/data.json?resource=' + encodeURIComponent(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 rrcs = lgData && lgData.data && lgData.data.rrcs || [];
|
||||||
var paths = [];
|
var paths = [];
|
||||||
var seen = new Set();
|
var seen = new Set();
|
||||||
@ -4939,7 +4939,7 @@ ${html}
|
|||||||
if (!resource) { res.writeHead(400); return res.end(JSON.stringify({error:'prefix or asn required'})); }
|
if (!resource) { res.writeHead(400); return res.end(JSON.stringify({error:'prefix or asn required'})); }
|
||||||
try {
|
try {
|
||||||
const url = `https://stat.ripe.net/data/looking-glass/data.json?resource=${encodeURIComponent(resource)}`;
|
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 rrcs = data && data.data && data.data.rrcs || [];
|
||||||
const results = rrcs.slice(0, 15).map(rrc => ({
|
const results = rrcs.slice(0, 15).map(rrc => ({
|
||||||
rrc: rrc.rrc,
|
rrc: rrc.rrc,
|
||||||
@ -5164,7 +5164,7 @@ ${html}
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const updUrl = `https://stat.ripe.net/data/bgp-updates/data.json?resource=AS${rawAsn}&starttime=${encodeURIComponent(starttime)}&endtime=${encodeURIComponent(endtime)}&limit=1000`;
|
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 updates = (raw && raw.data && raw.data.updates && raw.data.updates.updates) || [];
|
||||||
|
|
||||||
const announcements = [], withdrawals = [], originChanges = [], rpkiIssues = [];
|
const announcements = [], withdrawals = [], originChanges = [], rpkiIssues = [];
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user