diff --git a/mem0/client/main.py b/mem0/client/main.py index 01798bd7a..6f07508d3 100644 --- a/mem0/client/main.py +++ b/mem0/client/main.py @@ -151,10 +151,10 @@ class MemoryClient: try: params = self._prepare_params() response = self.client.get("/v1/ping/", params=params) - data = response.json() - response.raise_for_status() + data = response.json() + if data.get("org_id") and data.get("project_id"): self.org_id = data.get("org_id") self.project_id = data.get("project_id") @@ -1044,10 +1044,10 @@ class AsyncMemoryClient: }, params=params, ) - data = response.json() - response.raise_for_status() + data = response.json() + if data.get("org_id") and data.get("project_id"): self.org_id = data.get("org_id") self.project_id = data.get("project_id") diff --git a/tests/test_client.py b/tests/test_client.py index c776ecb05..671ed973d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,8 +1,11 @@ """Tests for MemoryClient entity parameter rejection.""" +import json from unittest.mock import MagicMock, patch +import httpx import pytest +import requests @pytest.fixture @@ -207,3 +210,56 @@ class TestDeleteLinked: call_args = mock_memory_client.client.delete.call_args assert "delete_linked" not in call_args.kwargs.get("params", {}) + + +class TestValidateApiKeyHttpError: + """_validate_api_key should surface a clear ValueError on a non-JSON HTTP error. + + A 5xx from a CDN/proxy often has an HTML body, so response.json() fails. The + HTTP status must be checked before json() so the intended ValueError is raised + instead of a confusing JSONDecodeError during client construction. + """ + + def test_sync_client_non_json_5xx_raises_clear_error(self): + # HTML body from a CDN/proxy: json() fails, but raise_for_status() reports the 503. + request = httpx.Request("GET", "https://api.mem0.ai/v1/ping/") + error_response = httpx.Response(503, text="503 Service Unavailable", request=request) + response = MagicMock() + response.json.side_effect = json.JSONDecodeError("Expecting value", "", 0) + response.raise_for_status.side_effect = httpx.HTTPStatusError( + "Server error", request=request, response=error_response + ) + + with patch("mem0.client.main.httpx.Client") as mock_httpx: + mock_http_client = MagicMock() + mock_http_client.get.return_value = response + mock_httpx.return_value = mock_http_client + + with patch("mem0.client.main.capture_client_event"): + from mem0.client.main import MemoryClient + + with pytest.raises(ValueError) as exc_info: + MemoryClient(api_key="test-api-key") + + # The HTTP status must surface as the intended "Error: ..." ValueError, + # not the raw JSONDecodeError from parsing the HTML body. + assert not isinstance(exc_info.value, json.JSONDecodeError) + assert "Error:" in str(exc_info.value) + + def test_async_client_non_json_5xx_raises_clear_error(self): + request = httpx.Request("GET", "https://api.mem0.ai/v1/ping/") + error_response = httpx.Response(503, text="503 Service Unavailable", request=request) + response = MagicMock() + response.json.side_effect = requests.exceptions.JSONDecodeError("Expecting value", "", 0) + http_error = requests.exceptions.HTTPError("Server error", response=error_response) + response.raise_for_status.side_effect = http_error + + with patch("mem0.client.main.requests.get", return_value=response): + with patch("mem0.client.main.capture_client_event"): + from mem0.client.main import AsyncMemoryClient + + with pytest.raises(ValueError) as exc_info: + AsyncMemoryClient(api_key="test-api-key") + + assert not isinstance(exc_info.value, requests.exceptions.JSONDecodeError) + assert "Error:" in str(exc_info.value)