From a2ed4fcce19ca4d923c4e2f6bbcac0772d31b306 Mon Sep 17 00:00:00 2001 From: Andrii Balitskyi <10balian10@gmail.com> Date: Wed, 12 Feb 2025 16:37:02 +0100 Subject: [PATCH 1/5] Handle non-json error responses --- seam/client.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/seam/client.py b/seam/client.py index 9965e905..098c87d4 100644 --- a/seam/client.py +++ b/seam/client.py @@ -61,6 +61,9 @@ def _handle_error_response(self, response: requests.Response): if status_code == 401: raise SeamHttpUnauthorizedError(request_id) + if not self._is_api_error_response(response): + response.raise_for_status() + error = response.json().get("error", {}) error_type = error.get("type", "unknown_error") error_message = error.get("message", "Unknown error") @@ -76,3 +79,30 @@ def _handle_error_response(self, response: requests.Response): raise SeamHttpInvalidInputError(error_details, status_code, request_id) raise SeamHttpApiError(error_details, status_code, request_id) + + def _is_api_error_response(self, response: requests.Response) -> bool: + try: + content_type = response.headers.get("content-type", "") + if not isinstance(content_type, str) or not content_type.startswith( + "application/json" + ): + return False + + data = response.json() + except Exception: + return False + + if not isinstance(data, dict): + return False + + error = data.get("error") + + if not isinstance(error, dict): + return False + + if not isinstance(error.get("type"), str) or not isinstance( + error.get("message"), str + ): + return False + + return True From 756479b9593e6bfb414bd2c8aa2fd8615231a549 Mon Sep 17 00:00:00 2001 From: Andrii Balitskyi <10balian10@gmail.com> Date: Wed, 12 Feb 2025 16:48:22 +0100 Subject: [PATCH 2/5] Catch specific errors --- seam/client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/seam/client.py b/seam/client.py index 098c87d4..5284a3be 100644 --- a/seam/client.py +++ b/seam/client.py @@ -83,13 +83,17 @@ def _handle_error_response(self, response: requests.Response): def _is_api_error_response(self, response: requests.Response) -> bool: try: content_type = response.headers.get("content-type", "") + if not isinstance(content_type, str) or not content_type.startswith( "application/json" ): return False + except ValueError: + return False + try: data = response.json() - except Exception: + except requests.exceptions.JSONDecodeError: return False if not isinstance(data, dict): From d2c5bf12d17c06549918a42db85df929fc537f00 Mon Sep 17 00:00:00 2001 From: Andrii Balitskyi <10balian10@gmail.com> Date: Wed, 12 Feb 2025 17:13:29 +0100 Subject: [PATCH 3/5] Fix lint too many return statements --- seam/client.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/seam/client.py b/seam/client.py index 5284a3be..6a593765 100644 --- a/seam/client.py +++ b/seam/client.py @@ -88,12 +88,9 @@ def _is_api_error_response(self, response: requests.Response) -> bool: "application/json" ): return False - except ValueError: - return False - try: data = response.json() - except requests.exceptions.JSONDecodeError: + except (ValueError, requests.exceptions.JSONDecodeError): return False if not isinstance(data, dict): From 0204dce5f67f1b7218bb9bd812d007d414f9dfcd Mon Sep 17 00:00:00 2001 From: Andrii Balitskyi <10balian10@gmail.com> Date: Wed, 12 Feb 2025 17:17:57 +0100 Subject: [PATCH 4/5] Test non-standard response --- test/http_error_test.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/http_error_test.py b/test/http_error_test.py index 7474f811..27454229 100644 --- a/test/http_error_test.py +++ b/test/http_error_test.py @@ -1,4 +1,5 @@ import pytest +import niquests from seam import Seam from seam.exceptions import ( SeamHttpApiError, @@ -44,3 +45,18 @@ def test_seam_http_throws_invalid_input_error(server): assert err.status_code == 400 assert err.code == "invalid_input" assert err.request_id.startswith("request") + + +def test_seam_http_throws_http_error_on_non_standard_response(server): + endpoint, seed = server + seam = Seam.from_api_key(seed["seam_apikey1_token"], endpoint=endpoint) + + seam.client.post( + "/_fake/simulate_workspace_outage", + json={"workspace_id": seed["seed_workspace_1"], "routes": ["/devices/list"]}, + ) + + with pytest.raises(niquests.HTTPError) as exc_info: + seam.devices.list() + + assert exc_info.value.response.status_code == 503 From 1ab722eadb2bc75cc5b66129258e6ad2bd6fd1e1 Mon Sep 17 00:00:00 2001 From: Andrii Balitskyi <10balian10@gmail.com> Date: Thu, 13 Feb 2025 15:29:33 +0100 Subject: [PATCH 5/5] Move is_api_error_response out of SeamHttpClient class --- seam/client.py | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/seam/client.py b/seam/client.py index 6a593765..b4c00c30 100644 --- a/seam/client.py +++ b/seam/client.py @@ -61,7 +61,7 @@ def _handle_error_response(self, response: requests.Response): if status_code == 401: raise SeamHttpUnauthorizedError(request_id) - if not self._is_api_error_response(response): + if not is_api_error_response(response): response.raise_for_status() error = response.json().get("error", {}) @@ -80,30 +80,31 @@ def _handle_error_response(self, response: requests.Response): raise SeamHttpApiError(error_details, status_code, request_id) - def _is_api_error_response(self, response: requests.Response) -> bool: - try: - content_type = response.headers.get("content-type", "") - if not isinstance(content_type, str) or not content_type.startswith( - "application/json" - ): - return False +def is_api_error_response(response: requests.Response) -> bool: + try: + content_type = response.headers.get("content-type", "") - data = response.json() - except (ValueError, requests.exceptions.JSONDecodeError): + if not isinstance(content_type, str) or not content_type.startswith( + "application/json" + ): return False - if not isinstance(data, dict): - return False + data = response.json() + except (ValueError, requests.exceptions.JSONDecodeError): + return False - error = data.get("error") + if not isinstance(data, dict): + return False - if not isinstance(error, dict): - return False + error = data.get("error") - if not isinstance(error.get("type"), str) or not isinstance( - error.get("message"), str - ): - return False + if not isinstance(error, dict): + return False + + if not isinstance(error.get("type"), str) or not isinstance( + error.get("message"), str + ): + return False - return True + return True