koala73/worldmonitor · error · ToolFetchError
HTTP
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.
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
AI-assisted analysis of koala73/worldmonitor@e586b8b4b8 (2026-08-21).
Data as JSON: /api/errors/ad1e5e3ed25b8957.
Report an issue: GitHub.
Appendix: source
Thrown at api/mcp/downstream.ts:296
emitTelemetry('mcp.downstream', {
tool,
auth_kind: auth.kind,
inbound_host_class: execution.inboundHostClass,
downstream_origin: execution.downstreamOriginTag,
downstream_operation: operation,
status: response.status,
ok: response.ok,
error_code: errorCode,
response_marker: responseMarker,
});
}
/**
* Validate one MCP sibling fetch while recording only bounded routing/auth
* diagnostics. Error response bodies are consumed solely to map a closed set
* of gateway codes; raw text, unknown values, headers, URLs, and credentials
* never leave this module.
*/
export async function assertMcpToolFetchOk(
response: ToolFetchResponse,
observation: DownstreamObservation,
): Promise<void> {
const { operation, tool, auth, execution } = observation;
if (response.ok) {
emitDownstreamTelemetry(
tool,
operation,
auth,
execution,
response,
null,
successMarker(response),
);
return;
}
View on GitHub (pinned to e586b8b4b8)