{"record":{"id":"aa8d3ef08491a681","repo":"koala73/worldmonitor","slug":"label-http-400","errorCode":null,"errorMessage":"${label} HTTP 400","messagePattern":"(.+?) HTTP 400","errorType":"validation","errorClass":"RpcValidationError","httpStatus":400,"severity":"error","filePath":"api/mcp/billing-denial.ts","lineNumber":182,"sourceCode":"}\n\n/**\n * 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":164,"sourceCodeEnd":187,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/api/mcp/billing-denial.ts#L164-L187","documentation":"RpcValidationError is thrown by assertToolFetchOk in api/mcp/billing-denial.ts when a tool _execute gateway fetch returns HTTP 400 AND the response body parses as JSON containing a 'violations' array of safe {field, description} pairs. These violations are proto/sebuf ValidationError output: the MCP tool call's arguments failed server-side schema validation. The message keeps the '<label> HTTP 400' shape so dispatch's log-severity downgrade and mcpErrorFingerprint grouping still match, while the typed 'violations' array carries the actionable detail (max 8 entries, field regex ^[A-Za-z_][A-Za-z0-9_.]{0,63}$, description capped at 200 chars, HTML/credential-like text dropped).","triggerScenarios":"Calling any MCP tool whose params violate the generated proto schema: wrong type for a field (string where number expected), out-of-range limit, unknown enum value, or a missing required property. The gateway returns 400 with a JSON body {violations: [{field, description}...]} and extractSafeRpcViolations finds at least one sanitizable pair. Triggered from tools/call on paths that route through assertToolFetchOk (e.g. nlp-tools list-feed-digest with an invalid category/variant parameter shape).","commonSituations":"Client generated from an older tool inputSchema after the server proto evolved (new required field, retyped property). Hand-written tool calls with ad-hoc params. Local dev against a deployed gateway with a newer proto than the local registry. Bodies that are HTML or contain unsafe text fall through to the generic HTTP 400 error instead, so seeing RpcValidationError specifically means a real proto validation reply arrived.","solutions":["Read err.violations — each entry names the exact offending field and the server's description; fix those params in the tools/call arguments","Re-fetch the tool's inputSchema via tools/list and align client param construction with the current schema","If the schema looks correct on both sides, curl the underlying /api endpoint directly to inspect the raw 400 body and confirm the violations list","If violations reference fields your client never sends, suspect proto drift: run make generate and redeploy/rebuild so registry and gateway agree"],"exampleFix":"// before\nconst res = await client.callTool('list-feed-digest', {\n  category: 'energy',\n  limit: 'ten',            // wrong type: proto expects number\n});\n// throws RpcValidationError: [ { field: 'limit', description: 'expected int32' } ]\n\n// after\nconst res = await client.callTool('list-feed-digest', {\n  category: 'energy',\n  limit: 10,\n});","handlingStrategy":"validation","validationCode":"// Validate params against the tool's declared inputSchema before tools/call\nfunction validateAgainstSchema(params, schema) {\n  const errs = [];\n  for (const [name, prop] of Object.entries(schema.properties ?? {})) {\n    const v = params[name];\n    if (schema.required?.includes(name) && (v === undefined || v === null)) {\n      errs.push(`${name}: required`); continue;\n    }\n    if (v === undefined) continue;\n    if (prop.type === 'number' && (typeof v !== 'number' || !Number.isFinite(v))) errs.push(`${name}: expected number`);\n    if (prop.type === 'string' && typeof v !== 'string') errs.push(`${name}: expected string`);\n    if (prop.enum && !prop.enum.includes(v)) errs.push(`${name}: must be one of ${prop.enum.join('|')}`);\n    if (prop.type === 'integer' && !Number.isInteger(v)) errs.push(`${name}: expected integer`);\n  }\n  return errs;\n}\nconst errs = validateAgainstSchema(args, toolSchema);\nif (errs.length) throw new Error('Bad params: ' + errs.join('; '));","typeGuard":"function isRpcValidationError(e) {\n  return typeof e === 'object' && e !== null\n    && (e.name === 'RpcValidationError' || Array.isArray(e.violations));\n}","tryCatchPattern":"try {\n  await client.callTool('list-feed-digest', args);\n} catch (e) {\n  if (isRpcValidationError(e)) {\n    // e.violations: [{field, description}] — map to user-facing field errors\n    for (const v of e.violations) console.error(`${v.field}: ${v.description}`);\n  } else throw e;\n}","preventionTips":["Fetch tools/list and generate/validate params from the live inputSchema instead of hardcoded shapes","Re-fetch the schema after gateway deploys — proto changes are the usual source of new 400 violations","Treat e.violations as the source of truth; never string-parse the message for field names"],"tags":["mcp","rpc","proto","validation","http-400"],"backgroundTag":"schema-validation-failed","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-08-23T13:39:53.451Z"}