fix(client): check HTTP status before parsing ping response in _validate_api_key (#5639)
This commit is contained in:
+4
-4
@@ -151,10 +151,10 @@ class MemoryClient:
|
|||||||
try:
|
try:
|
||||||
params = self._prepare_params()
|
params = self._prepare_params()
|
||||||
response = self.client.get("/v1/ping/", params=params)
|
response = self.client.get("/v1/ping/", params=params)
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
if data.get("org_id") and data.get("project_id"):
|
if data.get("org_id") and data.get("project_id"):
|
||||||
self.org_id = data.get("org_id")
|
self.org_id = data.get("org_id")
|
||||||
self.project_id = data.get("project_id")
|
self.project_id = data.get("project_id")
|
||||||
@@ -1044,10 +1044,10 @@ class AsyncMemoryClient:
|
|||||||
},
|
},
|
||||||
params=params,
|
params=params,
|
||||||
)
|
)
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
if data.get("org_id") and data.get("project_id"):
|
if data.get("org_id") and data.get("project_id"):
|
||||||
self.org_id = data.get("org_id")
|
self.org_id = data.get("org_id")
|
||||||
self.project_id = data.get("project_id")
|
self.project_id = data.get("project_id")
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
"""Tests for MemoryClient entity parameter rejection."""
|
"""Tests for MemoryClient entity parameter rejection."""
|
||||||
|
|
||||||
|
import json
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import httpx
|
||||||
import pytest
|
import pytest
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -207,3 +210,56 @@ class TestDeleteLinked:
|
|||||||
|
|
||||||
call_args = mock_memory_client.client.delete.call_args
|
call_args = mock_memory_client.client.delete.call_args
|
||||||
assert "delete_linked" not in call_args.kwargs.get("params", {})
|
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="<html>503 Service Unavailable</html>", request=request)
|
||||||
|
response = MagicMock()
|
||||||
|
response.json.side_effect = json.JSONDecodeError("Expecting value", "<html>", 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="<html>503 Service Unavailable</html>", request=request)
|
||||||
|
response = MagicMock()
|
||||||
|
response.json.side_effect = requests.exceptions.JSONDecodeError("Expecting value", "<html>", 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user