Skip to content

Commit 18079fc

Browse files
committed
PTHMINT-131: Add error handling for transport exceptions and server errors in Client
1 parent 3da99c0 commit 18079fc

2 files changed

Lines changed: 54 additions & 1 deletion

File tree

src/multisafepay/client/client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ def _create_request(
344344
"Content-Type": "application/json",
345345
}
346346

347+
response = None
347348
try:
348349
response = self.transport.request(
349350
method=method,
@@ -354,7 +355,8 @@ def _create_request(
354355
response.raise_for_status()
355356
except Exception as e:
356357
if (
357-
hasattr(response, "status_code")
358+
response is not None
359+
and hasattr(response, "status_code")
358360
and 500 <= response.status_code < 600
359361
):
360362
raise ApiException(f"Request failed: {e}") from e

tests/multisafepay/unit/client/test_unit_client.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from multisafepay.client.credential_resolver import (
1515
ScopedCredentialResolver,
1616
)
17+
from multisafepay.exception.api import ApiException
1718
from multisafepay.transport import RequestsTransport
1819

1920
requests = pytest.importorskip("requests")
@@ -54,6 +55,32 @@ def request(self: "_CaptureTransport", **kwargs: dict) -> _FakeResponse:
5455
return _FakeResponse()
5556

5657

58+
class _FailingTransport:
59+
"""Transport stub that fails before returning a response."""
60+
61+
@staticmethod
62+
def request(**kwargs: dict) -> _FakeResponse:
63+
raise RuntimeError("network unavailable")
64+
65+
66+
class _ServerErrorResponse(_FakeResponse):
67+
"""Response stub that raises for a server error."""
68+
69+
status_code = 500
70+
71+
@staticmethod
72+
def raise_for_status() -> None:
73+
raise RuntimeError("server error")
74+
75+
76+
class _ServerErrorTransport:
77+
"""Transport stub that returns a server error response."""
78+
79+
@staticmethod
80+
def request(**kwargs: dict) -> _ServerErrorResponse:
81+
return _ServerErrorResponse()
82+
83+
5784
def _build_resolver_client(
5885
resolver: ScopedCredentialResolver,
5986
transport: _CaptureTransport,
@@ -328,6 +355,30 @@ def test_create_delete_request_sends_authorization_header() -> None:
328355
assert transport.headers["Authorization"] == "Bearer test_key"
329356

330357

358+
def test_request_exception_without_response_is_reraised() -> None:
359+
"""Reraise transport exceptions when no response exists."""
360+
client = Client(
361+
api_key="test_key",
362+
is_production=False,
363+
transport=_FailingTransport(),
364+
)
365+
366+
with pytest.raises(RuntimeError, match="network unavailable"):
367+
client.create_get_request("json/orders")
368+
369+
370+
def test_server_error_response_raises_api_exception() -> None:
371+
"""Wrap server error responses in ApiException."""
372+
client = Client(
373+
api_key="test_key",
374+
is_production=False,
375+
transport=_ServerErrorTransport(),
376+
)
377+
378+
with pytest.raises(ApiException, match="Request failed: server error"):
379+
client.create_get_request("json/orders")
380+
381+
331382
def test_resolve_api_key_uses_credential_resolver() -> None:
332383
"""Prefer credential resolver when both api_key and resolver exist."""
333384
transport = _CaptureTransport()

0 commit comments

Comments
 (0)