Fix(asn): report unloaded local data as an error, not a cacheable empty

- PeeringDB is 'disabled' only without a Cloudflare key; configured but not
  loaded (still downloading, or the download failed) is an 'error', so the
  ASN Profile isn't cached for a week without it.
- Connectivity without an as-rel snapshot is an 'error' too, not an empty
  graph; a missing as2org still only blocks while it downloads, as RIPEstat
  names the nodes.
- A CAIDA as2org / as-rel file that parses to nothing (corrupt) no longer
  counts as loaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jason5ng32andClaude Opus 5.5 committed 2026-10-05 02:35:23 +08:00
1 parent 02476aec7e
commit d84fefbbea
4 files changed
+60 -17

No files matched your search

+23 -10
View File
@@ -20,12 +20,27 @@ import {
} from '../common/asn-profile.js';
import { getAsnConnectivity } from './asn-connectivity.js';
// A section whose local dataset is still downloading at boot fails (status
// 'error'): left out of the answer and keeping it uncached, while every
// other section answers as usual.
const unlessLoading = (checks, label, fn) => (...args) => {
if (isStillLoading(...checks)) throw new Error(`${label} still loading`);
return fn(...args);
// A section whose local data isn't there fails (status 'error'): left out
// of the answer and keeping it uncached, while every other section answers
// as usual — never an 'empty' / 'disabled' that would be cached for a week.
// Connectivity needs the as-rel snapshot (without it every graph is empty);
// as2org only names the nodes, RIPEstat covers a failed download, so it
// counts only while still downloading. Exported for tests.
export const connectivityReadiness = ({ relLoaded = isAsRelLoaded, orgLoaded = isAsOrgLoaded, fetch = getAsnConnectivity } = {}) =>
(...args) => {
if (!relLoaded()) throw new Error('CAIDA as-rel snapshot not loaded');
if (isStillLoading(orgLoaded)) throw new Error('CAIDA as2org snapshot still loading');
return fetch(...args);
};
// PeeringDB is 'disabled' only where it isn't configured (no Cloudflare key,
// so never downloaded). Configured but not loaded — still downloading, or
// the download failed — is an error. Exported for tests.
export const peeringdbReadiness = ({ configured = hasRadarApiKey, loaded = isPeeringdbLoaded } = {}) => () => {
if (!configured()) return false;
if (!loaded()) throw new Error('PeeringDB index not loaded');
return true;
};
export default async (req, res) => {
@@ -36,15 +51,13 @@ export default async (req, res) => {
fetchRadarAsn: (n) => loadAsnSummary(String(n)),
fetchRadarPrefixes: (n) => RADAR_VIEWS['bgp-prefixes'].fetch({ asn: String(n) }),
isPartialPrefixes: (body) => !isCompleteRadarAnswer(body),
getConnectivity: unlessLoading([isAsRelLoaded, isAsOrgLoaded], 'CAIDA snapshot', getAsnConnectivity),
getConnectivity: connectivityReadiness(),
rdapAutnum: (n) => rdapAutnum(n, { timeoutMs: SOURCE_TIMEOUTS.autnum }),
isAutnumMissing,
queryAsRank: (n) => queryAsRank(n, { timeoutMs: SOURCE_TIMEOUTS.rank }),
// Private-API pass-through: the caller's headers go upstream.
requestReputation: (n) => requestAsnReputation(n, req.headers, { timeoutMs: SOURCE_TIMEOUTS.reputation }),
// The index only downloads with the Cloudflare key; without it the
// section is 'disabled', never loading.
isPeeringdbLoaded: unlessLoading([() => !hasRadarApiKey() || isPeeringdbLoaded()], 'PeeringDB index', isPeeringdbLoaded),
isPeeringdbLoaded: peeringdbReadiness(),
lookupPeeringdb,
});
const body = await composeAsnProfile(asn, loaders);
+7
View File
@@ -72,6 +72,13 @@ function loadDatabase() {
const text = fs.readFileSync(filePath, 'utf8');
asnToOrgName.clear();
parsePipeText(text);
// The parser skips what it can't read, so a corrupt file parses to
// nothing rather than throwing: that is not a loaded snapshot.
if (asnToOrgName.size === 0) {
loadedFrom = null;
logger.warn({ path: filePath }, '⚠️ CAIDA as2org snapshot holds no ASNs; treating it as missing');
return;
}
loadedFrom = path.basename(filePath);
logger.info(`📦 CAIDA as2org loaded (${loadedFrom}) — ${asnToOrgName.size} ASNs in ${Date.now() - start}ms`);
} catch (error) {
+7
View File
@@ -114,6 +114,13 @@ function loadDatabase() {
customerCount.clear();
parsePipeText(text);
rebuildTier1Set();
// A corrupt file parses to nothing rather than throwing: that is not
// a loaded snapshot.
if (providersIndex.size === 0 && peersIndex.size === 0) {
loadedFrom = null;
logger.warn({ path: filePath }, '⚠️ CAIDA as-rel snapshot holds no relationships; treating it as missing');
return;
}
loadedFrom = path.basename(filePath);
logger.info(`📦 CAIDA as-rel loaded (${loadedFrom}) — ${providersIndex.size} customers, ${customerCount.size} providers, ${peersIndex.size} peering ASes, ${tier1Set.size} Tier 1s in ${Date.now() - start}ms`);
} catch (error) {
+23 -7
View File
@@ -22,8 +22,8 @@ import invisibilityHandler from '../api/invisibility-test.js';
import macCheckerHandler from '../api/mac-checker.js';
import githubStarsHandler from '../api/github-stars.js';
import personaEvaluateHandler from '../api/persona.js';
import asnProfileHandler from '../api/asn-profile.js';
import { isPeeringdbLoaded } from '../common/peeringdb-db.js';
import asnProfileHandler, { peeringdbReadiness, connectivityReadiness } from '../api/asn-profile.js';
import { isAsRelLoaded } from '../common/as-rel-db.js';
import { isCompleteProfile } from '../common/asn-profile.js';
import updateAchievementHandler from '../api/update-user-achievement.js';
import ipcheckIngHandler from '../api/ipcheck-ing.js';
@@ -741,13 +741,29 @@ describe('asn-profile handler', () => {
assert.equal(res.body.status.reputation, 'disabled');
assert.equal(res.body.status.whois, 'error');
assert.equal(res.body.status.rank, 'error');
assert.ok(['ok', 'empty'].includes(res.body.status.connectivity));
// Local: ok / empty with an as-rel snapshot, an error (never a
// cacheable empty) without one — a fresh checkout.
if (isAsRelLoaded()) assert.ok(['ok', 'empty'].includes(res.body.status.connectivity));
else assert.equal(res.body.status.connectivity, 'error');
assert.equal(res.body.whois, null);
assert.equal(res.body.rank, null);
// Local and never an error: disabled without an index file (the
// checkout default), ok / empty once the updater has built one.
if (isPeeringdbLoaded()) assert.ok(['ok', 'empty'].includes(res.body.status.peeringdb));
else assert.equal(res.body.status.peeringdb, 'disabled');
// No Cloudflare key: PeeringDB isn't configured here.
assert.equal(res.body.status.peeringdb, 'disabled');
});
it('PeeringDB: disabled only when unconfigured; configured but not loaded is an error', () => {
assert.equal(peeringdbReadiness({ configured: () => false, loaded: () => false })(), false);
assert.equal(peeringdbReadiness({ configured: () => true, loaded: () => true })(), true);
assert.throws(() => peeringdbReadiness({ configured: () => true, loaded: () => false })(), /not loaded/);
});
it('connectivity: no as-rel snapshot is an error; as2org matters only while loading', async () => {
const fetch = async () => 'graph';
const missingRel = connectivityReadiness({ relLoaded: () => false, orgLoaded: () => true, fetch });
assert.throws(() => missingRel(13335), /as-rel snapshot not loaded/);
// as2org failed after boot: RIPEstat names the nodes, the graph is served.
const noOrg = connectivityReadiness({ relLoaded: () => true, orgLoaded: () => false, fetch });
assert.equal(await noOrg(13335), 'graph');
});
it('shows a partial Radar summary but flags it incomplete (not cacheable)', async () => {