{"record":{"id":"4ca722721e64ed87","repo":"koala73/worldmonitor","slug":"get-country-intel-brief-http-res-status-code","errorCode":null,"errorMessage":"get-country-intel-brief HTTP ${res.status}${code ? `: ${code}` : ''}","messagePattern":"get-country-intel-brief HTTP (.+?)(.+?)` : ''\\}","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"api/mcp/registry/rpc-tools.ts","lineNumber":1630,"sourceCode":"        body: briefBody,\n        signal: AbortSignal.timeout(22_000),\n      }, execution);\n      if (!res.ok) {\n        throwIfBillingDenial(res, 'get-country-intel-brief');\n        // Surface the gateway's error code in the thrown message so Sentry\n        // groups the failure by root cause, not just status. Body reads are\n        // best-effort; a read failure must not mask the HTTP status.\n        const detail = await res.text().catch(() => '');\n        let code = '';\n        // `error` is usually a string (for example,\n        // `invalid_internal_mcp_signature`), but stringify non-string shapes so\n        // object envelopes remain readable. Bound both paths so Sentry titles\n        // cannot bloat on a long body.\n        try {\n          const error = (JSON.parse(detail) as { error?: unknown }).error ?? '';\n          code = (typeof error === 'string' ? error : JSON.stringify(error)).slice(0, 120);\n        } catch {\n          code = detail.replace(/<[^>]*>/g, ' ').replace(/\\s+/g, ' ').trim().slice(0, 120);\n        }\n        throw new Error(`get-country-intel-brief HTTP ${res.status}${code ? `: ${code}` : ''}`);\n      }\n      const result = await res.json() as Record<string, unknown>;\n      const resultSources = collectMcpBriefSources(Array.isArray(result.sources) ? result.sources as DigestItemForBrief[] : [], 6);\n      // groundingStories stays [] when the 2 s digest fetch failed above, which\n      // is the honest signal: the brief was written without that grounding.\n      return {\n        ...result,\n        sources: resultSources.length > 0 ? resultSources : sources,\n        groundingStories,\n        ...(digestCoverage ? { digestCoverage } : {}),\n      };\n    },\n    // METHOD DRIFT: _execute POSTs above but OpenAPI declares only GET on this\n    // path (verified against docs/api/IntelligenceService.openapi.json). The\n    // gateway routes by path, not method, so POST works at runtime. We declare\n    // GET here because OpenAPI is the parity test's source-of-truth — fixing","sourceCodeStart":1612,"sourceCodeEnd":1648,"githubUrl":"https://github.com/koala73/worldmonitor/blob/e586b8b4b80f595aa7ece295eec10d76f2921240/api/mcp/registry/rpc-tools.ts#L1612-L1648","documentation":"Plain Error thrown in get_country_intel_brief (api/mcp/registry/rpc-tools.ts:1152): the country intel brief endpoint returned non-ok. Before throwing, the tool parses the body for an 'error' field (usually a string like 'invalid_internal_mcp_signature'; non-string shapes are JSON-stringified) and bounds it to 120 chars — non-JSON bodies are HTML-stripped, whitespace-collapsed, and sliced — so the message carries a safe, Sentry-friendly code without body bloat. Note the documented METHOD DRIFT: this tool POSTs while OpenAPI declares only GET; the gateway routes by path so it works, but proxies enforcing method parity can break it.","triggerScenarios":"Calling get_country_intel_brief when the internal MCP signature validation fails (401 with 'invalid_internal_mcp_signature' — signature computed over the wrong method/path/body), an invalid country code produces a 4xx, the handler 5xxs, or a method-enforcing intermediary returns 405.","commonSituations":"buildAuthHeaders signing GET-shape while the request POSTs (or omitting the body from the signature). Country code not ISO alpha-2. OpenAPI/registry method drift tripping strict gateways. Upstream brief-generation dependency outage.","solutions":["Read the trailing : <code> — 'invalid_internal_mcp_signature' means the auth signature mismatch; verify method (POST), path, and body match what was signed","Validate country_code as ISO 3166-1 alpha-2 before calling","For 5xx, retry with backoff; for 405, check whether an intermediary enforces the OpenAPI-declared GET on this path","Reproduce with curl POST to the path with the same auth headers to isolate signature vs handler failure"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Validate the country param client-side before the call\nif (!/^[A-Za-z]{2}$/.test(countryCode ?? '')) throw new Error('country_code must be ISO 3166-1 alpha-2');","typeGuard":"function isCountryBriefHttpError(e) {\n  return e instanceof Error && /^get-country-intel-brief HTTP \\d+/.test(e.message);\n}\nfunction briefStatusOf(e) { return Number(e.message.match(/HTTP (\\d+)/)?.[1] ?? 0); }\nfunction briefCodeOf(e) { return e.message.match(/: (.+)$/)?.[1] ?? ''; }","tryCatchPattern":"try {\n  const brief = await client.callTool('get_country_intel_brief', { country: 'DE' });\n} catch (e) {\n  if (isCountryBriefHttpError(e)) {\n    const s = briefStatusOf(e), code = briefCodeOf(e);\n    if (s >= 500) return retryWithBackoff(call, 3);\n    if (code === 'invalid_internal_mcp_signature') throw new Error('Signature bug: sign the POST method+path+body', { cause: e });\n    throw e;\n  }\n  throw e;\n}","preventionTips":["Sign POST requests with the POST method and the exact request body — GET-shape signatures cause the 401 here","Validate country_code format before calling","Be aware of the documented GET/POST method drift on this path when behind method-enforcing proxies"],"tags":["mcp","rpc","country-brief","http","auth-signature"],"backgroundTag":"upstream-http-error","analyzedSha":"e586b8b4b80f595aa7ece295eec10d76f2921240","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}