{"record":{"id":"8be75d37b12415bb","repo":"koala73/worldmonitor","slug":"airspace-data-unavailable-both-civilian-and-milit","errorCode":null,"errorMessage":"Airspace data unavailable: both civilian and military sources failed","messagePattern":"Airspace data unavailable: both civilian and military sources failed","errorType":"exception","errorClass":"BothSourcesFailedError","httpStatus":null,"severity":"error","filePath":"api/mcp/registry/rpc-tools.ts","lineNumber":1366,"sourceCode":"                return r.ok ? r.json() as Promise<MilResp> : Promise.reject(new Error(`HTTP ${r.status}`));\n              }),\n      ]);\n\n      // A billing denial is user-level, not a data-source outage: never serve\n      // partial data or a generic both-failed error over it — rethrow so\n      // dispatch re-emits the full billing contract (status, Retry-After,\n      // X-Billing-Verification, data.code).\n      for (const result of [civResult, milResult]) {\n        if (result.status === 'rejected' && result.reason instanceof BillingDenialError) {\n          throw result.reason;\n        }\n      }\n\n      const civOk = type === 'military' || civResult.status === 'fulfilled';\n      const milOk = type === 'civilian' || milResult.status === 'fulfilled';\n\n      // Both sources down — total outage, don't return misleading empty data\n      if (!civOk && !milOk) throw new BothSourcesFailedError(civResult.reason, milResult.reason);\n\n      const civ = civResult.status === 'fulfilled' ? civResult.value : null;\n      const mil = milResult.status === 'fulfilled' ? milResult.value : null;\n      const warnings: string[] = [];\n      if (!civOk) warnings.push('civilian ADS-B data unavailable');\n      if (!milOk) warnings.push('military flight data unavailable');\n\n      const civilianFlights = (civ?.positions ?? []).slice(0, 100).map(p => ({\n        callsign: p.callsign, icao24: p.icao24,\n        lat: p.lat, lon: p.lon,\n        altitude_m: p.altitude_m, speed_kts: p.ground_speed_kts,\n        heading_deg: p.track_deg, on_ground: p.on_ground,\n      }));\n      const militaryFlights = (mil?.flights ?? []).slice(0, 100).map(f => ({\n        callsign: f.callsign, hex_code: f.hex_code,\n        aircraft_type: f.aircraft_type, aircraft_model: f.aircraft_model,\n        operator: f.operator, operator_country: f.operator_country,\n        lat: f.location?.latitude, lon: f.location?.longitude,","sourceCodeStart":1348,"sourceCodeEnd":1384,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/api/mcp/registry/rpc-tools.ts#L1348-L1384","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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)."],"exampleFix":"// before — fetches both sources, throws if both fail\ntool: 'get_airspace', params: { country_code: 'US' }\n// after — fetches only civilian, returns partial data with warning\ntool: 'get_airspace', params: { country_code: 'US', type: 'civilian' }","handlingStrategy":"fallback","validationCode":"// Before calling get_airspace with both sources, check provider health\nconst health = await fetch('/api/health').then(r => r.json());\nconst civUp = health.providerStatus?.['adsb-exchange'] !== 'down';\nconst milUp = health.providerStatus?.['military-feed'] !== 'down';\nif (!civUp && !milUp) {\n  // Both providers flagged down — call with type filter to get partial data\n  throw new Error('Both airspace providers down; request civilian or military only for partial data');\n}","typeGuard":"function isBothSourcesFailedError(e: unknown): e is { civilianFailure: string; militaryFailure: string } & Error {\n  return e instanceof Error && (e as any).name === 'BothSourcesFailedError';\n}","tryCatchPattern":"try {\n  const airspace = await callMcpTool('get_airspace', { country_code: 'US' });\n} catch (e) {\n  if (isBothSourcesFailedError(e)) {\n    // Fall back to a single source — returns partial data with a warning\n    if (e.civilianFailure === 'timeout' && e.militaryFailure !== 'timeout') {\n      const partial = await callMcpTool('get_airspace', { country_code: 'US', type: 'military' });\n    } else {\n      const partial = await callMcpTool('get_airspace', { country_code: 'US', type: 'civilian' });\n    }\n  } else throw e;\n}","preventionTips":["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."],"tags":["mcp","upstream-failure","airspace","aviation","both-sources-failed"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}