diff --git a/src/sdk/client.py b/src/sdk/client.py index 95b2f5c8c..7361ec586 100644 --- a/src/sdk/client.py +++ b/src/sdk/client.py @@ -24,8 +24,13 @@ def _request(self, method: str, path: str, data: Dict = None) -> Dict: try: with urlopen(req) as resp: - return json.loads(resp.read().decode()) + raw = resp.read() + if resp.status == 204 or not raw: + return {} + return json.loads(raw.decode()) except HTTPError as e: + if e.code == 204: + return {} return {"error": e.code, "message": e.reason} def register_agent(self, name: str, agent_type: str, config: Dict = None) -> Dict: diff --git a/tests/test_sdk_client.py b/tests/test_sdk_client.py new file mode 100644 index 000000000..1d689ff38 --- /dev/null +++ b/tests/test_sdk_client.py @@ -0,0 +1,69 @@ +"""Tests for SDK 204 No Content response handling.""" + +import json +from unittest.mock import patch, MagicMock +from io import BytesIO + +from src.sdk.client import OrchestratorClient + + +def make_mock_resp(status=200, body=b""): + """Helper to create a mock urllib response.""" + resp = MagicMock() + resp.status = status + resp.read.return_value = body + resp.__enter__ = MagicMock(return_value=resp) + resp.__exit__ = MagicMock(return_value=False) + return resp + + +class TestRequest204Handling: + """Regression tests for #4086 - Handle 204 responses without JSON decoding.""" + + def setup_method(self): + self.client = OrchestratorClient( + base_url="https://test.example.com", api_key="test-key" + ) + + @patch("src.sdk.client.urlopen") + def test_delete_returns_empty_dict_on_204(self, mock_urlopen): + """A 204 No Content response should return an empty dict.""" + mock_urlopen.return_value = make_mock_resp(status=204, body=b"") + result = self.client.delete_agent("agent-123") + assert result == {} + + @patch("src.sdk.client.urlopen") + def test_delete_returns_empty_dict_on_empty_body(self, mock_urlopen): + """An empty response body should return an empty dict even on 200.""" + mock_urlopen.return_value = make_mock_resp(status=200, body=b"") + result = self.client.delete_agent("agent-123") + assert result == {} + + @patch("src.sdk.client.urlopen") + def test_200_with_json_body_still_parses(self, mock_urlopen): + """A 200 with a valid JSON body should still parse normally.""" + data = json.dumps({"id": "agent-123", "status": "running"}).encode() + mock_urlopen.return_value = make_mock_resp(status=200, body=data) + result = self.client.get_agent("agent-123") + assert result == {"id": "agent-123", "status": "running"} + + @patch("src.sdk.client.urlopen") + def test_stop_agent_handles_204(self, mock_urlopen): + """Stop endpoint returning 204 should not crash.""" + mock_urlopen.return_value = make_mock_resp(status=204, body=b"") + result = self.client.stop_agent("agent-456") + assert result == {} + + @patch("src.sdk.client.urlopen") + def test_404_error_still_returns_error_dict(self, mock_urlopen): + """Non-204 errors should still return the error dict.""" + from urllib.error import HTTPError + mock_urlopen.side_effect = HTTPError( + url="https://test.example.com/api/v2/agents/missing", + code=404, + msg="Not Found", + hdrs=MagicMock(), + fp=BytesIO(b""), + ) + result = self.client.get_agent("missing") + assert result == {"error": 404, "message": "Not Found"}