Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
69 changes: 69 additions & 0 deletions tests/test_sdk_client.py
Original file line number Diff line number Diff line change
@@ -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"}