From 6640ede3dfbecb02062dea9fb20d137e92b7aeeb Mon Sep 17 00:00:00 2001 From: root Date: Fri, 1 May 2026 12:52:43 +0000 Subject: [PATCH] =?UTF-8?q?fix(V2):=20Deribit=20=5Fauthenticate=20gestisce?= =?UTF-8?q?=20error=20envelope=20(no=20pi=C3=B9=20KeyError=20'result')?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quando Deribit risponde con {"error": {...}} su public/auth (creds errate, scope mancante, env mismatch), il client esplodeva con KeyError: 'result' → 500 UNHANDLED_EXCEPTION sui tool privati (get_account_summary, get_positions). Ora _authenticate solleva DeribitAuthError tipizzata, _request la converte in error envelope coerente con il resto del flusso. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/cerbero_mcp/exchanges/deribit/client.py | 16 +++++++++++++++- tests/unit/exchanges/deribit/test_client.py | 17 +++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/cerbero_mcp/exchanges/deribit/client.py b/src/cerbero_mcp/exchanges/deribit/client.py index e779b3d..b4629c8 100644 --- a/src/cerbero_mcp/exchanges/deribit/client.py +++ b/src/cerbero_mcp/exchanges/deribit/client.py @@ -23,6 +23,10 @@ RESOLUTION_MAP = { } +class DeribitAuthError(Exception): + """Deribit auth failed (bad credentials, missing scope, env mismatch).""" + + @dataclass class DeribitClient: client_id: str @@ -50,6 +54,13 @@ class DeribitClient: async with async_client(timeout=15.0) as http: resp = await http.get(url, params=params) data = resp.json() + if "result" not in data: + error = data.get("error", {}) + msg = error.get("message", str(data)) if isinstance(error, dict) else str(error) + code = error.get("code") if isinstance(error, dict) else None + raise DeribitAuthError( + f"Deribit auth failed (code={code}, env={'testnet' if self.testnet else 'mainnet'}): {msg}" + ) result = data["result"] self._token = result["access_token"] self._token_expires_at = time.monotonic() + result.get("expires_in", 900) - 30 @@ -63,7 +74,10 @@ class DeribitClient: async def _request(self, method: str, params: dict[str, Any] | None = None) -> dict: is_private = method.startswith("private/") if is_private: - await self._get_token() + try: + await self._get_token() + except DeribitAuthError as e: + return {"result": None, "error": str(e)} url = f"{self.base_url}/{method}" request_params = dict(params) if params else {} diff --git a/tests/unit/exchanges/deribit/test_client.py b/tests/unit/exchanges/deribit/test_client.py index e46b20a..f7bc1df 100644 --- a/tests/unit/exchanges/deribit/test_client.py +++ b/tests/unit/exchanges/deribit/test_client.py @@ -154,6 +154,23 @@ async def test_get_account_summary(httpx_mock: HTTPXMock, client: DeribitClient) assert result["balance"] == 900.0 +@pytest.mark.asyncio +async def test_private_call_with_bad_auth_returns_error_envelope( + httpx_mock: HTTPXMock, client: DeribitClient +): + """Auth fallita (creds errate / scope mancante) → error envelope, non KeyError.""" + httpx_mock.add_response( + url=re.compile(r"https://test\.deribit\.com/api/v2/public/auth"), + json={"error": {"code": 13004, "message": "invalid_credentials"}}, + is_reusable=True, + ) + summary = await client.get_account_summary("USDC") + assert summary["equity"] == 0 + assert "invalid_credentials" in summary["error"] + positions = await client.get_positions("USDC") + assert positions == [] + + @pytest.mark.asyncio async def test_place_order(httpx_mock: HTTPXMock, client: DeribitClient): httpx_mock.add_response(