koala73/worldmonitor · error · ToolFetchError
${operation} HTTP ${status}: ${safeCode}
Error message
${operation} HTTP ${status}: ${safeCode} What it means
ToolFetchError is thrown by assertMcpToolFetchOk in api/mcp/downstream.ts after classifyFailure() has safely extracted a bounded error code from the response body and emitDownstreamTelemetry has recorded the operation. It covers every non-ok downstream response on this path that is not a billing denial (those re-throw as BillingDenialError first). The message shape '<operation> HTTP <status>: <safeCode>' gives the calling tool name, the HTTP status, and a sanitized body-derived code; the typed fields also carry responseMarker (json/html/other/empty_error/method_not_allowed/etc.) for programmatic handling.
Source
Thrown at api/mcp/downstream.ts:282
response,
error.billingCode,
'billing_verification',
);
}
throw error;
}
const failure = await classifyFailure(response);
emitDownstreamTelemetry(
tool,
operation,
auth,
execution,
response,
failure.errorCode,
failure.marker,
);
throw new ToolFetchError(
operation,
response.status,
failure.errorCode,
failure.marker,
);
}
/**
* Classify a PromiseSettledResult rejection reason into a short tag value.
*
* Returns one of:
* `timeout` — AbortSignal.timeout fired (AbortError)
* `http_<status>` — upstream returned a non-ok HTTP status
* `auth_error` — buildAuthHeaders or similar auth-path failure
* `error` — generic Error subclass (message available in detail)
* `unknown` — non-Error rejection (string, undefined, etc.)
*/
export function classifyFailureReason(reason: unknown): string {View on GitHub (pinned to eeab0a219f)
Solutions
- Read err.status and err.safeCode — safeCode often names the exact gateway check that failed (e.g. invalid_internal_mcp_signature)
- For 401/auth codes, audit buildAuthHeaders: the signed method, canonical path including query, and body must match the actual request byte-for-byte
- For 404/405, diff the tool's _apiPaths against the deployed routes (docs/api/ OpenAPI per service is the parity source of truth)
- For 5xx codes, retry with backoff; the telemetry emitted before the throw already recorded the failure for Sentry
Defensive patterns
Strategy: try-catch
Type guard
function isToolFetchError(e) {
return e instanceof Error && e.name === 'ToolFetchError'
&& typeof e.status === 'number' && typeof e.safeCode === 'string';
} Try / catch
try {
const payload = await fetchTool(context, execution);
} catch (e) {
if (isToolFetchError(e)) {
if (e.status >= 500) return retryWithBackoff(() => fetchTool(context, execution), 3);
if (e.safeCode.includes('signature')) throw new Error('Auth signing bug — fix buildAuthHeaders inputs', { cause: e });
throw e;
}
throw e;
} Prevention
- Assert each tool's _apiPaths against the deployed OpenAPI in CI so 404/405 drift is caught pre-release
- Unit-test auth signing with the exact method+path+query+body tuple the tool issues (GET vs POST diverge)
- Use e.responseMarker (json/html/empty_error/method_not_allowed) to detect intermediary interference early
When it happens
Trigger: An MCP tool _execute that goes through assertMcpToolFetchOk fetching e.g. /api/intelligence/v1/get-china-decision-signals and receiving any non-ok status: 401 from a mis-signed internal MCP signature, 404 when the registry's _apiPaths drift from deployed routes, 405 when method routing breaks, or 5xx/timeout-shaped upstream failures.
Common situations: Version skew: the MCP registry declares a path the gateway deployment does not serve. Auth canonicalization bugs (signing GET but issuing POST, or omitting the query string from the signed path). Upstream dependency outages. Method drift like the documented get-country-intel-brief GET/POST OpenAPI mismatch.
Related errors
- ${label} HTTP ${response.status}
- get-country-intel-brief HTTP ${res.status}${code ? `: ${code
- get-vessel-snapshot HTTP ${res.status}${detail ? ` — ${detai
- DNS ${recordType} lookup failed: HTTP ${response.status}
- DNS ${recordType} lookup failed: status ${data.Status}
AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21).
Data as JSON: /api/errors/ad1e5e3ed25b8957.
Report an issue: GitHub.