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

  1. Read err.status and err.safeCode — safeCode often names the exact gateway check that failed (e.g. invalid_internal_mcp_signature)
  2. For 401/auth codes, audit buildAuthHeaders: the signed method, canonical path including query, and body must match the actual request byte-for-byte
  3. For 404/405, diff the tool's _apiPaths against the deployed routes (docs/api/ OpenAPI per service is the parity source of truth)
  4. 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

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@eeab0a219f (2026-08-21). Data as JSON: /api/errors/ad1e5e3ed25b8957. Report an issue: GitHub.