koala73/worldmonitor · error · BothSourcesFailedError
Airspace data unavailable: both civilian and military…
Error message
Airspace data unavailable: both civilian and military sources failed
What it means
BothSourcesFailedError (api/mcp/downstream.ts:324) thrown by get_airspace when the requested scope needs both feeds and neither side is usable: civilian read rejected (or returned a non-redistributable OpenSky provider payload, which counts as unusable) AND military read rejected. The message is fixed; the typed fields carry classifyFailureReason tags for each side (timeout / http_<status> / auth_error / error) plus full detail strings, so dispatch can distinguish a shared-host outage (same failure both sides) from two independent provider failures.
Solutions
- Read err.civilianFailure and err.militaryFailure — matching tags (e.g. both 'timeout') suggest a shared-network/outage cause; differing tags suggest independent provider issues
- Retry with backoff — provider outages are frequently transient; honor any provider quota reset for auth_error/quota codes
- As a workaround, call with type: 'military' or type: 'civilian' to degrade to the single working source (the tool returns warnings instead of throwing when one side is up)
- If civilianFailure is consistently provider-related (OpenSky attribution), the civilian source needs a redistributable provider — that is a data-policy fix, not a retry
Example fix
// before
const res = await client.callTool('get_airspace', { bbox: '55,10,60,20', type: 'both' });
// throws BothSourcesFailedError when both feeds are down
// after — degrade to whichever side is alive, surface the warning
try {
const res = await client.callTool('get_airspace', { bbox: '55,10,60,20', type: 'both' });
} catch (e) {
if (e?.name === 'BothSourcesFailedError') {
const res = await client.callTool('get_airspace', { bbox: '55,10,60,20', type: 'military' });
// res carries warnings: ['civilian ADS-B data unavailable']
}
} Defensive patterns
Strategy: retry
Type guard
function isBothSourcesFailed(e) {
return typeof e === 'object' && e !== null
&& (e.name === 'BothSourcesFailedError'
|| (e.message === 'Airspace data unavailable: both civilian and military sources failed'
&& 'civilianFailure' in e && 'militaryFailure' in e));
} Try / catch
try {
return await client.callTool('get_airspace', { ...args, type: 'both' });
} catch (e) {
if (isBothSourcesFailed(e)) {
if (e.civilianFailure === e.militaryFailure) return retryWithBackoff(call, 3); // shared outage
try {
return await client.callTool('get_airspace', { ...args, type: 'military' }); // degrade one side
} catch { return await client.callTool('get_airspace', { ...args, type: 'civilian' }); }
}
throw e;
} Prevention
- Default workflows to type:'military' or 'civilian' when one feed is enough — single-source calls return warnings instead of throwing
- Compare civilianFailure vs militaryFailure tags: equal tags point to your network/gateway, differing tags to provider issues
- Respect ADS-B provider quotas; auth_error-class civilian failures usually mean quota exhaustion, not an outage to retry
When it happens
Trigger: Calling get_airspace with type 'both' (or default) while the civilian ADS-B fetch times out/fails AND the military flight data fetch also fails. Also triggered with type 'civilian' when the civilian source fails OR returns OpenSky-sourced data (redistribution not allowed — treated as unavailable) and military is not requested but... note: for type 'civilian' only civ matters, so this fires when civ fails AND the other required side (none) — in practice the both/military-independent double failure. With type 'military' a mil failure alone throws only if... it requires both required sides down.
Common situations: Shared upstream host outage taking down both ADS-B providers. Independent failures: OpenSky quota exhausted (civ) plus military feed timeout. Provider returned OpenSky-attributed data that policy forbids redistributing while military also failed. Network egress restrictions on the edge runtime.
Related errors
- body-too-large
- cache_all_null
- cache_all_null
- `call` needs a tool name, e.g. `worldmonitor call…
- Could not resolve to a country.
AI-assisted analysis of koala73/worldmonitor@e586b8b4b8 (2026-08-21).
Data as JSON: /api/errors/8be75d37b12415bb.
Report an issue: GitHub.
Appendix: source
Thrown at api/mcp/registry/rpc-tools.ts:2510
}
return parts.map(part => {
if (part.status === 'rejected') throw part.reason;
return part.value;
});
}
const [civResult, milResult] = await Promise.allSettled([
type === 'military' ? Promise.resolve(null) : fetchParts<TrackAircraftResponse>(
queries.map(query => `${base}/api/aviation/v1/track-aircraft?${query}`), 'get-airspace-civilian'),
type === 'civilian' ? Promise.resolve(null) : fetchParts<ListMilitaryFlightsResponse>(
queries.map(query => `${base}/api/military/v1/list-military-flights?${query}&page_size=100`), 'get-airspace-military'),
]);
// 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 civRaw = civResult.status === 'fulfilled' ? civResult.value : null;
const civProviderAllowed = !civRaw || civRaw.every(part => !isOpenSkyProvider(part?.source));
const civOk = type === 'military' || (civResult.status === 'fulfilled' && civProviderAllowed);
const milOk = type === 'civilian' || milResult.status === 'fulfilled';
// Both sources down — total outage, don't return misleading empty data
if (!civOk && !milOk) {
const civilianFailure = civResult.status === 'rejected'
? civResult.reason
: new Error('Civilian observations are not redistributable');
throw new BothSourcesFailedError(civilianFailure, milResult.reason);
}
View on GitHub (pinned to e586b8b4b8)