Skip to content

fix(mcp): get_airspace both-sources-failed error discards both rejection reasons (WORLDMONITOR-WV) #6085

Description

@koala73

Problem

When both upstream sources fail, get_airspace throws a message that discards why either one failed:

api/mcp/registry/rpc-tools.ts:971

if (!civOk && !milOk) throw new Error('Airspace data unavailable: both civilian and military sources failed');

civResult.reason and milResult.reason are both in scope at that point and both are dropped. The alert that reaches Sentry (WORLDMONITOR-WV) carries extra: {} and no distinguishing tags — verified against the latest event, whose only useful tags are environment and release.

So a timeout, an HTTP 500, and an auth failure all produce a byte-identical report, and none of them can be told apart after the fact.

Why the sources failing together is worth diagnosing

Both "sources" are our own routes, not third parties (rpc-tools.ts:935-936):

const civUrl = `${base}/api/aviation/v1/track-aircraft?${bboxQ}`;
const milUrl = `${base}/api/military/v1/list-military-flights?${bboxQ}&page_size=100`;

They share one base host, one buildAuthHeaders path, and one AbortSignal.timeout(8_000). A correlated double failure therefore points at our API host, our auth, or both upstream data providers at once — never at a single flaky vendor. Those have completely different responses, and right now the alert cannot indicate which one happened.

Evidence

WORLDMONITOR-WV: 3 events since 2026-07-19, most recent 2026-08-02T14:38Z. Each one was an unrecoverable hard error returned to an MCP caller of get_airspace, and each produced no actionable detail.

The low volume is the point — this is a rare, high-signal error. When it fires we should learn something from it, and today we don't.

Suggested direction

Attach both rejection reasons to the throw and to the Sentry extra, distinguishing at minimum:

  • AbortError / TimeoutError from the 8s AbortSignal.timeout (upstream slow)
  • HTTP <status> from the existing Promise.reject(new Error(\HTTP ${r.status}`))` rejections (upstream erroring)
  • a buildAuthHeaders failure (our auth path)

Tag each side separately (civilianFailure / militaryFailure) so the two can be compared in one glance — the interesting case is whether they failed the same way, which is what distinguishes a shared-host outage from two independent provider failures.

BillingDenialError is already rethrown ahead of this branch (rpc-tools.ts:961-965) and should stay excluded.

Non-goal

Not proposing to suppress or downgrade this error. It is a deliberate fail-loud that correctly refuses to serve misleading empty data, and it should keep firing — it just needs to say something when it does.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium priority, schedule when capacity allowsarea: APIBackend API, sidecar, keysbugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions