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

  1. 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.
  2. Retry after a short delay — simultaneous transient failures (timeouts, rate limits) often resolve independently.
  3. Check the civilianFailure and militaryFailure fields on the error — if they match, suspect a shared egress/network issue; if different, suspect independent provider outages.
  4. 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

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


AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12). Data as JSON: /api/errors/8be75d37b12415bb. Report an issue: GitHub.