refactor: extract lia.js and health.js routes, fix /api/health closure bug

Bug fix (agreed pre-existing bug #3 of 3): the .catch() branch of
localDb.getLocalDbStats() referenced status/roaAge, both only declared
inside the sibling .then() callback's closure -- out of scope in .catch(),
so any real DB-stats failure threw ReferenceError with no further catch,
hanging the response until client timeout instead of degrading gracefully.
Fixed: roaAge computed once before the promise chain (same pattern as the
existing aspaAge); the .catch() branch gets its own locally-scoped
`status = "degraded"` (a DB-stats fetch failure can't confirm "ok",
matching the honest-degradation pattern already used throughout this
codebase). Also deduplicated the identical delta_last computation that
was repeated verbatim in both branches into one aspaAdoptionDelta() helper.

Verified via node --check, eslint no-undef (2 known bugs remain: compare,
webhooks), and smoke-test harness (28/28 match). server.js: 3556 -> 3346 lines.
This commit is contained in:
Rene Fichtmueller 2026-07-17 07:36:10 +02:00
parent 3399392f1f
commit b82c61e03c
3 changed files with 210 additions and 169 deletions

177
server.js
View File

@ -220,6 +220,8 @@ function proxyToApiServer(req, res, targetUrl) {
const staticRoutes = require("./server/routes/static"); const staticRoutes = require("./server/routes/static");
const searchRoutes = require("./server/routes/search"); const searchRoutes = require("./server/routes/search");
const feedbackRoutes = require("./server/routes/feedback"); const feedbackRoutes = require("./server/routes/feedback");
const liaRoutes = require("./server/routes/lia");
const healthRoute = require("./server/routes/health");
const server = http.createServer(async (req, res) => { const server = http.createServer(async (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Origin", "*");
@ -282,197 +284,34 @@ const server = http.createServer(async (req, res) => {
return staticRoutes.handleFavicon(req, res); return staticRoutes.handleFavicon(req, res);
} }
// Lia'''s Atlas Paradise - Easter egg page // Lia's Atlas Paradise - Easter egg page
if (reqPath === "/lia" || reqPath === "/lia/") { if (reqPath === "/lia" || reqPath === "/lia/") {
return staticRoutes.handleLia(req, res); return staticRoutes.handleLia(req, res);
} }
// ============================================================
// Lia's Atlas Paradise: Atlas probe coverage endpoint // Lia's Atlas Paradise: Atlas probe coverage endpoint
// ============================================================
if (reqPath === "/api/atlas/coverage") { if (reqPath === "/api/atlas/coverage") {
res.setHeader("Content-Type", "application/json"); return liaRoutes.handleAtlasCoverage(req, res);
if (!atlasState.cache) {
res.writeHead(503);
return res.end(JSON.stringify({ error: "Atlas probe data is still loading. Please try again in a minute." }));
}
return res.end(JSON.stringify(atlasState.cache, null, 2));
} }
// ============================================================
// Lia's Paradise: File parsing endpoint (for binary uploads) // Lia's Paradise: File parsing endpoint (for binary uploads)
// ============================================================
if (reqPath === "/api/lia/parse-file" && req.method === "POST") { if (reqPath === "/api/lia/parse-file" && req.method === "POST") {
res.setHeader("Content-Type", "application/json"); return liaRoutes.handleParseFile(req, res);
let body = "";
req.on("data", function(chunk) { body += chunk; });
req.on("end", function() {
try {
var parsed = JSON.parse(body);
var filename = parsed.filename || "";
var ext = filename.split(".").pop().toLowerCase();
// For text-based formats, decode base64 and extract text
if (ext === "csv" || ext === "txt") {
var text = Buffer.from(parsed.data, "base64").toString("utf8");
return res.end(JSON.stringify({ text: text }));
}
// For binary formats (PDF, XLS, DOC), we can't parse server-side without
// heavy dependencies. Return helpful error.
return res.end(JSON.stringify({
error: "Binary file parsing (" + ext.toUpperCase() + ") requires client-side extraction. Please use CSV or TXT format, or copy-paste the content.",
suggestion: "Export your spreadsheet as CSV first, then upload the CSV file."
}));
} catch(e) {
return res.end(JSON.stringify({ error: "Parse error: " + e.message }));
}
});
return;
} }
// ============================================================
// Lia's Paradise: Combined PeeringDB + Atlas coverage data // Lia's Paradise: Combined PeeringDB + Atlas coverage data
// ============================================================
if (reqPath === "/api/lia/coverage") { if (reqPath === "/api/lia/coverage") {
res.setHeader("Content-Type", "application/json"); return liaRoutes.handleLiaCoverage(req, res);
if (!atlasState.cache) {
res.writeHead(503);
return res.end(JSON.stringify({ error: "Atlas probe data is still loading. Please try again in a minute." }));
}
// Cache this expensive response for 30 min
var liaCacheKey = "lia_coverage";
var liaCached = cacheGet(liaCacheKey);
if (liaCached) return res.end(liaCached);
// Fetch PeeringDB network list (all networks with status "ok")
// Use pre-cached org→country map (loaded at startup, 16MB response cached in memory)
fetchPeeringDB("/net?status=ok&depth=0").then(function(pdbData) {
if (!pdbData || !pdbData.data) {
return res.end(JSON.stringify({ error: "Could not fetch PeeringDB networks" }));
}
var probeAsns = new Set(atlasState.cache.asns_with_probes || []);
var enriched = pdbData.data.map(function(n) {
var org = pdbOrgState.map.get(n.org_id) || {};
var cc = org.country || "";
return {
asn: n.asn,
name: n.name || "",
org_name: org.name || "",
country: cc,
country_name: cc,
info_type: n.info_type || "",
has_probe: probeAsns.has(n.asn),
};
}).filter(function(n) { return n.asn > 0 && n.country; });
var result = JSON.stringify({
networks: enriched,
total: enriched.length,
with_probes: enriched.filter(function(n) { return n.has_probe; }).length,
without_probes: enriched.filter(function(n) { return !n.has_probe; }).length,
atlas_unique_asns: probeAsns.size,
org_countries_loaded: pdbOrgState.map.size,
fetched_at: new Date().toISOString(),
});
cacheSet(liaCacheKey, result, 30 * 60 * 1000);
res.end(result);
}).catch(function(e) {
res.end(JSON.stringify({ error: "PeeringDB fetch failed: " + e.message }));
});
return;
} }
res.setHeader("Content-Type", "application/json"); res.setHeader("Content-Type", "application/json");
// Health endpoint — extended with cache status, ASPA metrics, and local DB stats // Health endpoint — extended with cache status, ASPA metrics, and local DB stats
if (reqPath === "/api/health") { if (reqPath === "/api/health") {
const mem = process.memoryUsage(); return healthRoute.handleHealth(req, res);
const rpkiAspaLastFetch = getRpkiAspaLastFetch();
const aspaAge = rpkiAspaLastFetch ? Math.floor((Date.now() - rpkiAspaLastFetch) / 60000) : -1;
const pdbTotal = pdbSourceCache.hits + pdbSourceCache.misses;
// Query local DB stats (async, but return partial if needed)
localDb.getLocalDbStats().then(function(dbStats) {
// Determine health status based on local DB data availability
const hasLocalBgp = dbStats && dbStats.bgp_routes > 100000; // should have >2M rows normally
const hasLocalRpki = dbStats && dbStats.rpki_roas > 100000; // should have >500k rows normally
const status = (hasLocalBgp && hasLocalRpki && aspaAge < 300) ? "ok" : "degraded";
const healthResponse = {
status,
service: "PeerCortex",
version: "0.6.9",
timestamp: new Date().toISOString(),
uptime_seconds: Math.floor(process.uptime()),
memory_mb: Math.round(mem.heapUsed / 1024 / 1024),
bgproutes_configured: !!BGPROUTES_API_KEY,
caches: {
aspa_map: { entries: rpkiAspaMap.size, age_minutes: aspaAge },
pdb_net: { entries: pdbSourceCache.net.size, hit_rate_pct: pdbTotal > 0 ? Math.round(pdbSourceCache.hits / pdbTotal * 100) : 0 },
pdb_netixlan: { entries: pdbSourceCache.netixlan.size },
pdb_netfac: { entries: pdbSourceCache.netfac.size },
ripe_stat: { entries: ripeStatCache.size },
response_cache: { entries: responseCache.size },
},
local_db: dbStats ? {
bgp_routes: dbStats.bgp_routes,
rpki_roas: dbStats.rpki_roas,
threat_intel: dbStats.threat_intel,
rdap_cache_entries: dbStats.rdap_cache_entries,
source: "PostgreSQL (local)",
healthy: hasLocalBgp && hasLocalRpki,
} : null,
aspa_adoption: {
total_objects: rpkiAspaMap.size,
roa_count: roaStore.count,
history_samples: aspaAdoptionDailyHistory.length,
delta_last: aspaAdoptionDailyHistory.length >= 2
? aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 1].aspa_objects - aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 2].aspa_objects
: 0,
},
};
return res.end(JSON.stringify(healthResponse, null, 2));
}).catch(function(e) {
console.error('[/api/health] Local DB stats error:', e.message);
// Return health without local DB stats on error
return res.end(
JSON.stringify({
status,
service: "PeerCortex",
version: "0.6.9",
timestamp: new Date().toISOString(),
uptime_seconds: Math.floor(process.uptime()),
memory_mb: Math.round(mem.heapUsed / 1024 / 1024),
bgproutes_configured: !!BGPROUTES_API_KEY,
caches: {
roa_store: { entries: roaStore.count, age_minutes: roaAge, ready: roaStore.ready },
aspa_map: { entries: rpkiAspaMap.size, age_minutes: aspaAge },
pdb_net: { entries: pdbSourceCache.net.size, hit_rate_pct: pdbTotal > 0 ? Math.round(pdbSourceCache.hits / pdbTotal * 100) : 0 },
pdb_netixlan: { entries: pdbSourceCache.netixlan.size },
pdb_netfac: { entries: pdbSourceCache.netfac.size },
ripe_stat: { entries: ripeStatCache.size },
response_cache: { entries: responseCache.size },
},
local_db: { error: "Could not fetch local DB stats", message: e.message },
aspa_adoption: {
total_objects: rpkiAspaMap.size,
roa_count: roaStore.count,
history_samples: aspaAdoptionDailyHistory.length,
delta_last: aspaAdoptionDailyHistory.length >= 2
? aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 1].aspa_objects - aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 2].aspa_objects
: 0,
},
}, null, 2)
);
});
return;
} }
// ============================================================ // ============================================================
// ASPA Deep Verification endpoint: /api/aspa/verify?asn=X // ASPA Deep Verification endpoint: /api/aspa/verify?asn=X
// ============================================================ // ============================================================

104
server/routes/health.js Normal file
View File

@ -0,0 +1,104 @@
const localDb = require("../../local-db-client");
const { getRpkiAspaLastFetch, rpkiAspaMap } = require("../services/rpki");
const { pdbSourceCache } = require("../caches/pdb-source-cache");
const { roaStore } = require("../caches/roa-store");
const { ripeStatCache } = require("../services/ripe-stat");
const { responseCache } = require("../caches/response-caches");
const { aspaAdoptionDailyHistory } = require("../aspa-adoption/tracker");
const BGPROUTES_API_KEY = process.env.BGPROUTES_API_KEY || "";
function aspaAdoptionDelta() {
return aspaAdoptionDailyHistory.length >= 2
? aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 1].aspa_objects - aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 2].aspa_objects
: 0;
}
// Health endpoint — extended with cache status, ASPA metrics, and local DB stats
function handleHealth(req, res) {
const mem = process.memoryUsage();
const rpkiAspaLastFetch = getRpkiAspaLastFetch();
const aspaAge = rpkiAspaLastFetch ? Math.floor((Date.now() - rpkiAspaLastFetch) / 60000) : -1;
// Bug fix 2026-07-17: previously only declared inside the .then() callback
// below, so the .catch() branch's reference to it threw ReferenceError
// (never caught -- the response just hung until client timeout).
const roaAge = roaStore.lastBuild ? Math.floor((Date.now() - roaStore.lastBuild) / 60000) : -1;
const pdbTotal = pdbSourceCache.hits + pdbSourceCache.misses;
// Query local DB stats (async, but return partial if needed)
localDb.getLocalDbStats().then(function(dbStats) {
// Determine health status based on local DB data availability
const hasLocalBgp = dbStats && dbStats.bgp_routes > 100000; // should have >2M rows normally
const hasLocalRpki = dbStats && dbStats.rpki_roas > 100000; // should have >500k rows normally
const status = (hasLocalBgp && hasLocalRpki && aspaAge < 300) ? "ok" : "degraded";
const healthResponse = {
status,
service: "PeerCortex",
version: "0.6.9",
timestamp: new Date().toISOString(),
uptime_seconds: Math.floor(process.uptime()),
memory_mb: Math.round(mem.heapUsed / 1024 / 1024),
bgproutes_configured: !!BGPROUTES_API_KEY,
caches: {
aspa_map: { entries: rpkiAspaMap.size, age_minutes: aspaAge },
pdb_net: { entries: pdbSourceCache.net.size, hit_rate_pct: pdbTotal > 0 ? Math.round(pdbSourceCache.hits / pdbTotal * 100) : 0 },
pdb_netixlan: { entries: pdbSourceCache.netixlan.size },
pdb_netfac: { entries: pdbSourceCache.netfac.size },
ripe_stat: { entries: ripeStatCache.size },
response_cache: { entries: responseCache.size },
},
local_db: dbStats ? {
bgp_routes: dbStats.bgp_routes,
rpki_roas: dbStats.rpki_roas,
threat_intel: dbStats.threat_intel,
rdap_cache_entries: dbStats.rdap_cache_entries,
source: "PostgreSQL (local)",
healthy: hasLocalBgp && hasLocalRpki,
} : null,
aspa_adoption: {
total_objects: rpkiAspaMap.size,
roa_count: roaStore.count,
history_samples: aspaAdoptionDailyHistory.length,
delta_last: aspaAdoptionDelta(),
},
};
return res.end(JSON.stringify(healthResponse, null, 2));
}).catch(function(e) {
console.error('[/api/health] Local DB stats error:', e.message);
// Return health without local DB stats on error. Local DB stats failed
// to fetch, so "ok" can't be confirmed -- report degraded, matching the
// honest-degradation pattern used throughout this codebase.
const status = "degraded";
return res.end(
JSON.stringify({
status,
service: "PeerCortex",
version: "0.6.9",
timestamp: new Date().toISOString(),
uptime_seconds: Math.floor(process.uptime()),
memory_mb: Math.round(mem.heapUsed / 1024 / 1024),
bgproutes_configured: !!BGPROUTES_API_KEY,
caches: {
roa_store: { entries: roaStore.count, age_minutes: roaAge, ready: roaStore.ready },
aspa_map: { entries: rpkiAspaMap.size, age_minutes: aspaAge },
pdb_net: { entries: pdbSourceCache.net.size, hit_rate_pct: pdbTotal > 0 ? Math.round(pdbSourceCache.hits / pdbTotal * 100) : 0 },
pdb_netixlan: { entries: pdbSourceCache.netixlan.size },
pdb_netfac: { entries: pdbSourceCache.netfac.size },
ripe_stat: { entries: ripeStatCache.size },
response_cache: { entries: responseCache.size },
},
local_db: { error: "Could not fetch local DB stats", message: e.message },
aspa_adoption: {
total_objects: rpkiAspaMap.size,
roa_count: roaStore.count,
history_samples: aspaAdoptionDailyHistory.length,
delta_last: aspaAdoptionDelta(),
},
}, null, 2)
);
});
return;
}
module.exports = { handleHealth };

98
server/routes/lia.js Normal file
View File

@ -0,0 +1,98 @@
const { atlasState } = require("../atlas-probes");
const { pdbOrgState } = require("../pdb-org-countries");
const { fetchPeeringDB } = require("../services/peeringdb");
const { cacheGet, cacheSet } = require("../caches/response-caches");
// Lia's Atlas Paradise: Atlas probe coverage endpoint
function handleAtlasCoverage(req, res) {
res.setHeader("Content-Type", "application/json");
if (!atlasState.cache) {
res.writeHead(503);
return res.end(JSON.stringify({ error: "Atlas probe data is still loading. Please try again in a minute." }));
}
return res.end(JSON.stringify(atlasState.cache, null, 2));
}
// Lia's Paradise: File parsing endpoint (for binary uploads)
function handleParseFile(req, res) {
res.setHeader("Content-Type", "application/json");
let body = "";
req.on("data", function(chunk) { body += chunk; });
req.on("end", function() {
try {
var parsed = JSON.parse(body);
var filename = parsed.filename || "";
var ext = filename.split(".").pop().toLowerCase();
// For text-based formats, decode base64 and extract text
if (ext === "csv" || ext === "txt") {
var text = Buffer.from(parsed.data, "base64").toString("utf8");
return res.end(JSON.stringify({ text: text }));
}
// For binary formats (PDF, XLS, DOC), we can't parse server-side without
// heavy dependencies. Return helpful error.
return res.end(JSON.stringify({
error: "Binary file parsing (" + ext.toUpperCase() + ") requires client-side extraction. Please use CSV or TXT format, or copy-paste the content.",
suggestion: "Export your spreadsheet as CSV first, then upload the CSV file."
}));
} catch(e) {
return res.end(JSON.stringify({ error: "Parse error: " + e.message }));
}
});
return;
}
// Lia's Paradise: Combined PeeringDB + Atlas coverage data
function handleLiaCoverage(req, res) {
res.setHeader("Content-Type", "application/json");
if (!atlasState.cache) {
res.writeHead(503);
return res.end(JSON.stringify({ error: "Atlas probe data is still loading. Please try again in a minute." }));
}
// Cache this expensive response for 30 min
var liaCacheKey = "lia_coverage";
var liaCached = cacheGet(liaCacheKey);
if (liaCached) return res.end(liaCached);
// Fetch PeeringDB network list (all networks with status "ok")
// Use pre-cached org→country map (loaded at startup, 16MB response cached in memory)
fetchPeeringDB("/net?status=ok&depth=0").then(function(pdbData) {
if (!pdbData || !pdbData.data) {
return res.end(JSON.stringify({ error: "Could not fetch PeeringDB networks" }));
}
var probeAsns = new Set(atlasState.cache.asns_with_probes || []);
var enriched = pdbData.data.map(function(n) {
var org = pdbOrgState.map.get(n.org_id) || {};
var cc = org.country || "";
return {
asn: n.asn,
name: n.name || "",
org_name: org.name || "",
country: cc,
country_name: cc,
info_type: n.info_type || "",
has_probe: probeAsns.has(n.asn),
};
}).filter(function(n) { return n.asn > 0 && n.country; });
var result = JSON.stringify({
networks: enriched,
total: enriched.length,
with_probes: enriched.filter(function(n) { return n.has_probe; }).length,
without_probes: enriched.filter(function(n) { return !n.has_probe; }).length,
atlas_unique_asns: probeAsns.size,
org_countries_loaded: pdbOrgState.map.size,
fetched_at: new Date().toISOString(),
});
cacheSet(liaCacheKey, result, 30 * 60 * 1000);
res.end(result);
}).catch(function(e) {
res.end(JSON.stringify({ error: "PeeringDB fetch failed: " + e.message }));
});
return;
}
module.exports = { handleAtlasCoverage, handleParseFile, handleLiaCoverage };