{"record":{"id":"bd0960b35559d903","repo":"koala73/worldmonitor","slug":"http-d-s","errorCode":null,"errorMessage":"HTTP %d: %s","messagePattern":"HTTP (.+?): (.+?)","errorType":"exception","errorClass":"APIError","httpStatus":null,"severity":"error","filePath":"sdk/python/src/worldmonitor_sdk/__init__.py","lineNumber":291,"sourceCode":"        headers = self._headers(accept=\"application/json, text/event-stream\")\n        headers[\"content-type\"] = \"application/json\"\n        status, content_type, text = self._transport(\n            {\n                \"url\": self.mcp_url,\n                \"method\": \"POST\",\n                \"headers\": headers,\n                \"body\": json.dumps(rpc).encode(\"utf-8\"),\n            },\n            self.timeout,\n        )\n        value = parse_body(text, content_type)\n        # A JSON-RPC error object wins over the HTTP status (the server pairs\n        # auth errors with a 200 on some transports).\n        if isinstance(value, dict) and isinstance(value.get(\"error\"), dict):\n            err = value[\"error\"]\n            raise MCPError(err.get(\"code\", 0), err.get(\"message\", \"\"), err.get(\"data\"))\n        if status < 200 or status >= 300:\n            raise APIError(status, value)\n        if isinstance(value, dict) and \"result\" in value:\n            return value[\"result\"]\n        return value\n\n\ndef _stringify(value):\n    if value is True:\n        return \"true\"\n    if value is False:\n        return \"false\"\n    return str(value)\n\n\n__all__ = [\n    \"API_KEY_HEADER\",\n    \"AUTH_HINT\",\n    \"APIError\",\n    \"Client\",","sourceCodeStart":273,"sourceCodeEnd":309,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/sdk/python/src/worldmonitor_sdk/__init__.py#L273-L309","documentation":"Raised as APIError by _rpc() (line 290-291) when the MCP transport — a POST to mcp_url — returns a non-2xx HTTP status AND the parsed body was not a JSON-RPC error object (a JSON-RPC error would have raised MCPError first at line 287-289). This is a transport/gateway-level failure of the MCP endpoint itself, not an application-level JSON-RPC error. The body is whatever the server/proxy returned (often HTML), truncated to 300 chars.","triggerScenarios":"WORLDMONITOR_MCP_URL overridden to a host that does not accept POST or has no /mcp route (404, 405). A reverse proxy or Cloudflare WAF returns an HTML challenge/block page with a 403/503 status (non-JSON, so no JSON-RPC error object to match). The MCP endpoint is down or returning 502/504 from an upstream gateway. mcp_url accidentally set to the REST base_url (https://api.worldmonitor.app instead of https://worldmonitor.app/mcp).","commonSituations":"Self-hosted relay without the /mcp route mounted. Corporate egress proxy that strips or rewrites the POST. A DNS or TLS failure that surfaces as a proxy 502. Mismatched mcp_url and base_url after an environment migration.","solutions":["Verify client.mcp_url — it must be the MCP endpoint (default https://worldmonitor.app/mcp), not the REST API host.","If self-hosting, confirm the relay accepts JSON POSTs to /mcp and returns JSON, not an HTML error page.","For 502/503/504, retry with exponential backoff — these are transient gateway failures.","If a WAF/CDN is blocking, ensure the User-Agent header (set automatically by the SDK) is preserved end-to-end."],"exampleFix":"# before\nclient = Client(mcp_url='https://api.worldmonitor.app/mcp')  # wrong host\n# after\nclient = Client()  # DEFAULT_MCP_URL = https://worldmonitor.app/mcp","handlingStrategy":"retry","validationCode":"# Confirm the MCP endpoint accepts a POST before relying on it in a hot path.\nimport urllib.request, json\nreq = urllib.request.Request(client.mcp_url, data=json.dumps({'jsonrpc':'2.0','id':1,'method':'tools/list'}).encode(),\n                            headers={'content-type':'application/json','accept':'application/json'}, method='POST')\ntry:\n    with urllib.request.urlopen(req, timeout=10) as r:\n        assert 200 <= r.status < 300, f'MCP endpoint returned {r.status}'\nexcept Exception as e:\n    raise SystemExit(f'MCP transport unreachable at {client.mcp_url}: {e}')","typeGuard":"from worldmonitor_sdk import APIError\n\ndef is_mcp_transport_error(exc: Exception) -> bool:\n    # APIError raised from _rpc (MCP path) rather than get (REST path) is not\n    # distinguishable by class alone — distinguish by call site in the handler.\n    return isinstance(exc, APIError) and exc.status in (403, 404, 405, 500, 502, 503, 504)","tryCatchPattern":"import time, random\nfrom worldmonitor_sdk import APIError, MCPError, WorldMonitorError\n\nfor attempt in range(4):\n    try:\n        result = client.call_tool('get_market_data', asset_class='crypto')\n        break\n    except MCPError:\n        raise  # JSON-RPC application error — not a transport issue\n    except APIError as e:\n        if e.status in (502, 503, 504) and attempt < 3:\n            time.sleep((2 ** attempt) + random.random())\n            continue\n        raise\nelse:\n    raise RuntimeError('MCP transport failed after retries')","preventionTips":["Leave mcp_url on the default production endpoint unless you operate a self-hosted relay.","Do not set WORLDMONITOR_MCP_URL to the REST base_url — the MCP endpoint is a distinct host/path.","Ensure any reverse proxy in front of a self-hosted /mcp passes POST bodies through unchanged.","Retry only transient gateway statuses (502/503/504); a persistent 404 means the route is missing."],"tags":["python","mcp","http","transport","gateway","sdk"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}