{"record":{"id":"c05c54be005c196f","repo":"koala73/worldmonitor","slug":"label-http-response-status","errorCode":null,"errorMessage":"${label} HTTP ${response.status}","messagePattern":"\\$\\{label\\} HTTP \\$\\{response\\.status\\}","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"api/mcp/billing-denial.ts","lineNumber":185,"sourceCode":" * Standard non-ok handling for tool `_execute` gateway fetches: billing\n * denials become typed errors dispatch can re-emit faithfully; proto 400\n * bodies with safe field violations become RpcValidationError; everything\n * else keeps the existing `<label> HTTP <status>` Error contract.\n *\n * HTTP 400 response bodies are consumed only to classify violations. Callers\n * must await this helper — a forgotten await would let execution continue\n * and treat the 400 as success.\n */\nexport async function assertToolFetchOk(response: ToolFetchResponse, label: string): Promise<void> {\n  if (response.ok) return;\n  throwIfBillingDenial(response, label);\n  if (response.status === 400) {\n    const violations = await extractSafeRpcViolations(response);\n    if (violations.length > 0) {\n      throw new RpcValidationError(label, violations);\n    }\n  }\n  throw new Error(`${label} HTTP ${response.status}`);\n}\n","sourceCodeStart":167,"sourceCodeEnd":187,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/api/mcp/billing-denial.ts#L167-L187","documentation":"This is the generic non-ok fallback of assertToolFetchOk in api/mcp/billing-denial.ts: the tool's gateway fetch returned a non-ok status that is neither a billing denial (no X-Billing-Verification marker header with a known code) nor a 400 carrying safe proto violations. The message preserves the '<label> HTTP <status>' contract that dispatch's catch-all and log-severity downgrade key on. Every other classification (BillingDenialError, RpcValidationError) has already been ruled out by the time this throws.","triggerScenarios":"Any tools/call whose _execute fetches a /api/... route and gets 401/403/404/409/429/500/502/503 back: an expired or mis-signed HMAC auth header (401), a lapsed entitlement without the billing marker (403), a route that does not exist in this deployment (404), or an upstream 5xx. Also fires for 400s whose body is HTML, malformed JSON, or has no sanitizable violations array.","commonSituations":"Signature canonicalization drift between buildAuthHeaders and the gateway (method, path, or body hash mismatch). Deploying the MCP registry and the API routes out of sync so a tool POSTs to a path that is not yet deployed. Upstream dependency outage surfacing as 502/503. Env var misconfiguration (wrong secret) producing consistent 401s.","solutions":["Parse the status from the message (/ HTTP (\\d+)$/) — 401/403 means auth/entitlement, 404 means route drift, 5xx means retry later","For 401/403, verify the auth headers: method, full path with query string, and body must match exactly what buildAuthHeaders signed","For 404, confirm the route named in the label exists in the target deployment (check docs/api/ OpenAPI or hit the path directly)","For 5xx/429, retry with backoff honoring any Retry-After header"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isToolHttpError(e) {\n  return e instanceof Error && / HTTP \\d+$/.test(e.message)\n    && !(e instanceof RpcValidationError) && e.name !== 'BillingDenialError';\n}\nfunction httpStatusOf(e) { const m = e.message.match(/HTTP (\\d+)$/); return m ? Number(m[1]) : null; }","tryCatchPattern":"try {\n  const r = await client.callTool(name, args);\n} catch (e) {\n  if (isToolHttpError(e)) {\n    const s = httpStatusOf(e);\n    if (s >= 500 || s === 429) return retryWithBackoff(() => client.callTool(name, args), 3);\n    if (s === 401 || s === 403) throw new Error('Auth/entitlement failure — refresh credentials', { cause: e });\n    throw e; // 4xx other than 401/403: not retryable\n  }\n  throw e;\n}","preventionTips":["Centralize the '<label> HTTP <status>' parse in one helper so status-based routing cannot drift per call site","Smoke-test tool auth (one cheap tools/call) after rotating secrets or changing buildAuthHeaders","Pin registry and API route deployments to the same release so 404 drift cannot reach production callers"],"tags":["mcp","http","gateway","fetch","upstream"],"backgroundTag":"upstream-http-error","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-08-23T11:17:13.642Z"}