From 924868a9f14dede48ef32affb80430b0dd2dfd44 Mon Sep 17 00:00:00 2001 From: Lucas Parzianello Date: Tue, 25 Aug 2026 13:46:17 -0400 Subject: [PATCH] sdk: making http status errors more resilient (499 errors were raising ValueError) --- sdk/src/spectrumx/gateway.py | 4 +-- sdk/src/spectrumx/ops/network.py | 49 +++++++++++++++++++++++++++++++- sdk/tests/ops/test_network.py | 18 ++++++++++++ 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/sdk/src/spectrumx/gateway.py b/sdk/src/spectrumx/gateway.py index 745124a45..fb033b13e 100644 --- a/sdk/src/spectrumx/gateway.py +++ b/sdk/src/spectrumx/gateway.py @@ -6,7 +6,6 @@ from collections.abc import Collection from collections.abc import Iterator from enum import StrEnum -from http import HTTPStatus from pathlib import Path from pathlib import PurePosixPath from typing import Annotated @@ -21,6 +20,7 @@ from urllib3.util import Retry from spectrumx.models.captures import CaptureType +from spectrumx.ops.network import _safe_http_status from .config import DEFAULT_HTTP_TIMEOUT from .errors import AuthError @@ -258,7 +258,7 @@ def authenticate(self, *, verbose: bool = False) -> None: if code is None: msg = "No response code received from authentication request." raise AuthError(msg) - status = HTTPStatus(code) + status = _safe_http_status(code) log.bind(cat=LogCategory.NETWORK).debug(f"Authentication response: {status}") if status.is_success: return diff --git a/sdk/src/spectrumx/ops/network.py b/sdk/src/spectrumx/ops/network.py index a99ea2712..2ef80e28f 100644 --- a/sdk/src/spectrumx/ops/network.py +++ b/sdk/src/spectrumx/ops/network.py @@ -16,12 +16,59 @@ HTTPStatus.is_server_error = property(lambda s: 500 <= s <= 599) # noqa: PLR2004 +from dataclasses import dataclass + import requests from loguru import logger as log from spectrumx import errors +@dataclass(frozen=True, slots=True) +class _SyntheticHTTPStatus: + """Fallback for non-standard HTTP codes that HTTPStatus rejects.""" + + code: int + + @property + def is_informational(self) -> bool: + return 100 <= self.code <= 199 # noqa: PLR2004 + + @property + def is_success(self) -> bool: + return 200 <= self.code <= 299 # noqa: PLR2004 + + @property + def is_redirection(self) -> bool: + return 300 <= self.code <= 399 # noqa: PLR2004 + + @property + def is_client_error(self) -> bool: + return 400 <= self.code <= 499 # noqa: PLR2004 + + @property + def is_server_error(self) -> bool: + return 500 <= self.code <= 599 # noqa: PLR2004 + + def __eq__(self, other: object) -> bool: + if isinstance(other, _SyntheticHTTPStatus): + return self.code == other.code + if isinstance(other, HTTPStatus): + return self.code == other.value + return NotImplemented + + def __hash__(self) -> int: + return hash(self.code) + + +def _safe_http_status(code: int) -> HTTPStatus | _SyntheticHTTPStatus: + """Return an HTTPStatus-like object, handling non-standard codes gracefully.""" + try: + return HTTPStatus(code) + except ValueError: + return _SyntheticHTTPStatus(code=code) + + def success_or_raise( response: requests.Response, ContextException: type[errors.SDSError] = errors.SDSError, # noqa: N803 @@ -30,7 +77,7 @@ def success_or_raise( code = response.status_code if code is None: raise errors.SDSError(message="No status code in response.") - status = HTTPStatus(code) + status = _safe_http_status(code) if status.is_success: return diff --git a/sdk/tests/ops/test_network.py b/sdk/tests/ops/test_network.py index d72582d9b..fc42ab5f0 100644 --- a/sdk/tests/ops/test_network.py +++ b/sdk/tests/ops/test_network.py @@ -126,6 +126,24 @@ def test_success_or_raise_catchall_fallback() -> None: success_or_raise(response) +def test_success_or_raise_non_standard_code_499() -> None: + """Non-standard 499 should be treated as client error, not crash.""" + response = requests.Response() + response.status_code = 499 + response._content = b'{"detail": "Client closed request"}' + with pytest.raises(errors.SDSError, match="Client closed request"): + success_or_raise(response) + + +def test_success_or_raise_non_standard_code_599() -> None: + """Non-standard 599 should be treated as server error, not crash.""" + response = requests.Response() + response.status_code = 599 + response._content = b'{"detail": "Network connect timeout"}' + with pytest.raises(errors.ServiceError, match="Network connect timeout"): + success_or_raise(response, ContextException=errors.SDSError) + + def test_extract_error_details_from_html_no_matching_element() -> None: """When HTML lacks #summary/#pastebinTraceback, falls back to reason.""" response = requests.Response()