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.
Problem
When both upstream sources fail,
get_airspacethrows a message that discards why either one failed:api/mcp/registry/rpc-tools.ts:971civResult.reasonandmilResult.reasonare both in scope at that point and both are dropped. The alert that reaches Sentry (WORLDMONITOR-WV) carriesextra: {}and no distinguishing tags — verified against the latest event, whose only useful tags areenvironmentandrelease.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):They share one
basehost, onebuildAuthHeaderspath, and oneAbortSignal.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/TimeoutErrorfrom the 8sAbortSignal.timeout(upstream slow)HTTP <status>from the existingPromise.reject(new Error(\HTTP ${r.status}`))` rejections (upstream erroring)buildAuthHeadersfailure (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.BillingDenialErroris 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.