{"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":2510,"sourceCode":"        }\n        return parts.map(part => {\n          if (part.status === 'rejected') throw part.reason;\n          return part.value;\n        });\n      }\n\n      const [civResult, milResult] = await Promise.allSettled([\n        type === 'military' ? Promise.resolve(null) : fetchParts<TrackAircraftResponse>(\n          queries.map(query => `${base}/api/aviation/v1/track-aircraft?${query}`), 'get-airspace-civilian'),\n        type === 'civilian' ? Promise.resolve(null) : fetchParts<ListMilitaryFlightsResponse>(\n          queries.map(query => `${base}/api/military/v1/list-military-flights?${query}&page_size=100`), 'get-airspace-military'),\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 civRaw = civResult.status === 'fulfilled' ? civResult.value : null;\n      const civProviderAllowed = !civRaw || civRaw.every(part => !isOpenSkyProvider(part?.source));\n      const civOk = type === 'military' || (civResult.status === 'fulfilled' && civProviderAllowed);\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) {\n        const civilianFailure = civResult.status === 'rejected'\n          ? civResult.reason\n          : new Error('Civilian observations are not redistributable');\n        throw new BothSourcesFailedError(civilianFailure, milResult.reason);\n      }\n","sourceCodeStart":2492,"sourceCodeEnd":2528,"githubUrl":"https://github.com/koala73/worldmonitor/blob/e586b8b4b80f595aa7ece295eec10d76f2921240/api/mcp/registry/rpc-tools.ts#L2492-L2528","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nconst res = await client.callTool('get_airspace', { bbox: '55,10,60,20', type: 'both' });\n// throws BothSourcesFailedError when both feeds are down\n\n// after — degrade to whichever side is alive, surface the warning\ntry {\n  const res = await client.callTool('get_airspace', { bbox: '55,10,60,20', type: 'both' });\n} catch (e) {\n  if (e?.name === 'BothSourcesFailedError') {\n    const res = await client.callTool('get_airspace', { bbox: '55,10,60,20', type: 'military' });\n    // res carries warnings: ['civilian ADS-B data unavailable']\n  }\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":"function isBothSourcesFailed(e) {\n  return typeof e === 'object' && e !== null\n    && (e.name === 'BothSourcesFailedError'\n      || (e.message === 'Airspace data unavailable: both civilian and military sources failed'\n        && 'civilianFailure' in e && 'militaryFailure' in e));\n}","tryCatchPattern":"try {\n  return await client.callTool('get_airspace', { ...args, type: 'both' });\n} catch (e) {\n  if (isBothSourcesFailed(e)) {\n    if (e.civilianFailure === e.militaryFailure) return retryWithBackoff(call, 3); // shared outage\n    try {\n      return await client.callTool('get_airspace', { ...args, type: 'military' }); // degrade one side\n    } catch { return await client.callTool('get_airspace', { ...args, type: 'civilian' }); }\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["mcp","airspace","adsb","multi-provider","outage"],"backgroundTag":"upstream-provider-error","analyzedSha":"e586b8b4b80f595aa7ece295eec10d76f2921240","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}