From 349910b883b01226c5666f251269852c1e360b3a Mon Sep 17 00:00:00 2001 From: Kayvan Zahiri <123409429+Kayvan-Zahiri@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:30:24 -0700 Subject: [PATCH] Raise API exceptions for empty HTTP error responses --- sift/client.py | 49 +++++++++++++++--------------- tests/test_empty_error_response.py | 49 ++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 24 deletions(-) create mode 100644 tests/test_empty_error_response.py diff --git a/sift/client.py b/sift/client.py index b421ce4..3ad0aae 100644 --- a/sift/client.py +++ b/sift/client.py @@ -61,34 +61,24 @@ def __init__(self, http_response: requests.Response) -> None: self.body: dict[str, t.Any] | None = None self.request: dict[str, t.Any] | None = None - if ( - self.http_status_code not in self.HTTP_CODES_WITHOUT_BODY - ) and http_response.text: - try: - self.body = http_response.json() + try: + if ( + self.http_status_code not in self.HTTP_CODES_WITHOUT_BODY + ) and http_response.text: + try: + self.body = http_response.json() - if "status" in self.body: - self.api_status = self.body["status"] + if "status" in self.body: + self.api_status = self.body["status"] - if "error_message" in self.body: - self.api_error_message = self.body["error_message"] + if "error_message" in self.body: + self.api_error_message = self.body["error_message"] - if isinstance(self.body.get("request"), str): - self.request = json.loads(self.body["request"]) - except ValueError: - raise ApiException( - f"Failed to parse json response from {self.url}", - url=self.url, - http_status_code=self.http_status_code, - body=self.body, - api_status=self.api_status, - api_error_message=self.api_error_message, - request=self.request, - ) - finally: - if not 200 <= self.http_status_code < 300: + if isinstance(self.body.get("request"), str): + self.request = json.loads(self.body["request"]) + except ValueError: raise ApiException( - f"{self.url} returned non-2XX http status code {self.http_status_code}", + f"Failed to parse json response from {self.url}", url=self.url, http_status_code=self.http_status_code, body=self.body, @@ -96,6 +86,17 @@ def __init__(self, http_response: requests.Response) -> None: api_error_message=self.api_error_message, request=self.request, ) + finally: + if not 200 <= self.http_status_code < 300: + raise ApiException( + f"{self.url} returned non-2XX http status code {self.http_status_code}", + url=self.url, + http_status_code=self.http_status_code, + body=self.body, + api_status=self.api_status, + api_error_message=self.api_error_message, + request=self.request, + ) def __str__(self) -> str: body = ( diff --git a/tests/test_empty_error_response.py b/tests/test_empty_error_response.py new file mode 100644 index 0000000..bde1d97 --- /dev/null +++ b/tests/test_empty_error_response.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from unittest import TestCase, mock + +import requests + +from sift.client import Client, Response +from sift.exceptions import ApiException + + +class TestEmptyErrorResponse(TestCase): + def test_empty_and_whitespace_errors_raise(self) -> None: + for status in (400, 401, 429, 500, 503): + for body in (b"", b" "): + with self.subTest(status=status, body=body): + response = requests.Response() + response.status_code = status + response.url = "https://api.sift.com/v205/events" + response._content = body + with self.assertRaises(ApiException) as caught: + Response(response) + self.assertEqual(caught.exception.http_status_code, status) + self.assertEqual(caught.exception.url, response.url) + + def test_empty_success_remains_valid(self) -> None: + for status in (200, 201, 204): + with self.subTest(status=status): + response = requests.Response() + response.status_code = status + response.url = "https://api.sift.com/v205/events" + response._content = b"" + result = Response(response) + self.assertEqual(result.http_status_code, status) + self.assertIsNone(result.body) + + def test_public_track_propagates_empty_server_error(self) -> None: + response = requests.Response() + response.status_code = 503 + response.url = "https://api.sift.com/v205/events" + response._content = b"" + with requests.Session() as session: + with mock.patch.object( + session, "post", return_value=response + ) as post: + client = Client(api_key="test-key", session=session) + with self.assertRaises(ApiException) as caught: + client.track("$create_account", {"$user_id": "test-user"}) + post.assert_called_once() + self.assertEqual(caught.exception.http_status_code, 503)