koala73/worldmonitor · error · BothSourcesFailedError
Airspace data unavailable: both civilian and military source
Error message
Airspace data unavailable: both civilian and military sources failed
What it means
Thrown by the get_airspace MCP tool as a BothSourcesFailedError when BOTH the civilian (ADS-B) and military flight data upstream fetches rejected. Before this, the code checks each result for BillingDenialError and re-throws that first (preserving the billing contract). BothSourcesFailedError carries classified failure summaries (civilianFailure, militaryFailure) and full details so dispatch can distinguish a shared-host outage (same failure on both sides) from two independent provider failures.
Source
Thrown at api/mcp/registry/rpc-tools.ts:1366
return r.ok ? r.json() as Promise<MilResp> : Promise.reject(new Error(`HTTP ${r.status}`));
}),
]);
// A billing denial is user-level, not a data-source outage: never serve
// partial data or a generic both-failed error over it — rethrow so
// dispatch re-emits the full billing contract (status, Retry-After,
// X-Billing-Verification, data.code).
for (const result of [civResult, milResult]) {
if (result.status === 'rejected' && result.reason instanceof BillingDenialError) {
throw result.reason;
}
}
const civOk = type === 'military' || civResult.status === 'fulfilled';
const milOk = type === 'civilian' || milResult.status === 'fulfilled';
// Both sources down — total outage, don't return misleading empty data
if (!civOk && !milOk) throw new BothSourcesFailedError(civResult.reason, milResult.reason);
const civ = civResult.status === 'fulfilled' ? civResult.value : null;
const mil = milResult.status === 'fulfilled' ? milResult.value : null;
const warnings: string[] = [];
if (!civOk) warnings.push('civilian ADS-B data unavailable');
if (!milOk) warnings.push('military flight data unavailable');
const civilianFlights = (civ?.positions ?? []).slice(0, 100).map(p => ({
callsign: p.callsign, icao24: p.icao24,
lat: p.lat, lon: p.lon,
altitude_m: p.altitude_m, speed_kts: p.ground_speed_kts,
heading_deg: p.track_deg, on_ground: p.on_ground,
}));
const militaryFlights = (mil?.flights ?? []).slice(0, 100).map(f => ({
callsign: f.callsign, hex_code: f.hex_code,
aircraft_type: f.aircraft_type, aircraft_model: f.aircraft_model,
operator: f.operator, operator_country: f.operator_country,
lat: f.location?.latitude, lon: f.location?.longitude,View on GitHub (pinned to ffec79ac33)
Solutions
- Narrow the request: call get_airspace with type='civilian' or type='military' to fetch just one source — the tool returns partial data with a warning instead of throwing.
- Retry after a short delay — simultaneous transient failures (timeouts, rate limits) often resolve independently.
- Check the civilianFailure and militaryFailure fields on the error — if they match, suspect a shared egress/network issue; if different, suspect independent provider outages.
- Verify both provider API keys/tokens are valid and not expired (ADS-B Exchange, military feed credentials).
Example fix
// before — fetches both sources, throws if both fail
tool: 'get_airspace', params: { country_code: 'US' }
// after — fetches only civilian, returns partial data with warning
tool: 'get_airspace', params: { country_code: 'US', type: 'civilian' } Defensive patterns
Strategy: fallback
Validate before calling
// Before calling get_airspace with both sources, check provider health
const health = await fetch('/api/health').then(r => r.json());
const civUp = health.providerStatus?.['adsb-exchange'] !== 'down';
const milUp = health.providerStatus?.['military-feed'] !== 'down';
if (!civUp && !milUp) {
// Both providers flagged down — call with type filter to get partial data
throw new Error('Both airspace providers down; request civilian or military only for partial data');
} Type guard
function isBothSourcesFailedError(e: unknown): e is { civilianFailure: string; militaryFailure: string } & Error {
return e instanceof Error && (e as any).name === 'BothSourcesFailedError';
} Try / catch
try {
const airspace = await callMcpTool('get_airspace', { country_code: 'US' });
} catch (e) {
if (isBothSourcesFailedError(e)) {
// Fall back to a single source — returns partial data with a warning
if (e.civilianFailure === 'timeout' && e.militaryFailure !== 'timeout') {
const partial = await callMcpTool('get_airspace', { country_code: 'US', type: 'military' });
} else {
const partial = await callMcpTool('get_airspace', { country_code: 'US', type: 'civilian' });
}
} else throw e;
} Prevention
- Use type='civilian' or type='military' to get partial data when one provider is known to be down.
- Monitor both provider endpoints independently so you can proactively narrow the request type.
- Check civilianFailure vs militaryFailure — matching values suggest a shared egress issue.
When it happens
Trigger: Calling get_airspace (default type or without type filter) when both the civilian ADS-B provider AND the military flight provider are simultaneously unreachable — e.g. both time out (8s budget), both return 5xx, or both are rate-limited. If type='civilian' or type='military', only one source is fetched and this error cannot fire (the single failing source is reported as a warning, not a throw).
Common situations: A shared network egress issue from the edge function affecting both providers; both flight-data providers (ADS-B Exchange, military feed) experiencing simultaneous outages; both providers rate-limiting the edge IP at the same time; a config issue where both provider API keys expired.
Related errors
- get-country-intel-brief HTTP ${res.status}${code ? `: ${code
- get-vessel-snapshot HTTP ${res.status}${detail ? ` — ${detai
- serverUrl host is not allowed
- serverUrl hostname is blocked: ${hostname}
- serverUrl DNS resolution failed: ${message}
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/8be75d37b12415bb.
Report an issue: GitHub.