Skip to content

Commit 043a6d0

Browse files
committed
feat: improved the exception handling to ensure that they are aligned across all SDKs and that API errors can be differentiated
1 parent d73c9ec commit 043a6d0

12 files changed

Lines changed: 61 additions & 106 deletions

spec/auth/using_access_token_spec.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import pytest
44

55
import zitadel_client as zitadel
6-
from zitadel_client.exceptions import OpenApiError
6+
from zitadel_client.exceptions import ZitadelError
77

88

99
@pytest.fixture(scope="module")
@@ -58,5 +58,5 @@ def test_raises_api_exception_with_invalid_token(
5858
base_url,
5959
"invalid",
6060
)
61-
with pytest.raises(OpenApiError):
61+
with pytest.raises(ZitadelError):
6262
client.settings.settings_service_get_general_settings()

spec/auth/using_client_credentials_spec.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import pytest
44

55
import zitadel_client as zitadel
6-
from zitadel_client.exceptions import OpenApiError
6+
from zitadel_client.exceptions import ZitadelError
77

88

99
@pytest.fixture(scope="module")
@@ -70,5 +70,5 @@ def test_raises_api_exception_with_invalid_client_credentials(
7070
"invalid",
7171
"invalid",
7272
)
73-
with pytest.raises(OpenApiError):
73+
with pytest.raises(ZitadelError):
7474
client.settings.settings_service_get_general_settings()

spec/auth/using_private_key_spec.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import pytest
55

66
import zitadel_client as zitadel
7-
from zitadel_client import OpenApiError
7+
from zitadel_client import ZitadelError
88

99

1010
@pytest.fixture(scope="module")
@@ -58,5 +58,5 @@ def test_raises_api_exception_with_invalid_private_key(
5858
"https://zitadel.cloud",
5959
key_file,
6060
)
61-
with pytest.raises(OpenApiError):
61+
with pytest.raises(ZitadelError):
6262
client.settings.settings_service_get_general_settings()

spec/check_session_service_spec.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pytest
66

77
import zitadel_client as zitadel
8-
from zitadel_client.exceptions import ApiException
8+
from zitadel_client.exceptions import ApiError
99
from zitadel_client.models import (
1010
SessionServiceChecks,
1111
SessionServiceCheckUser,
@@ -61,7 +61,7 @@ def session(client: zitadel.Zitadel) -> Generator[SessionServiceCreateSessionRes
6161
response.session_id if response.session_id is not None else "",
6262
delete_body,
6363
)
64-
except ApiException:
64+
except ApiError:
6565
pass
6666

6767

@@ -124,7 +124,7 @@ def test_raises_api_exception_for_nonexistent_session(
124124
session: SessionServiceCreateSessionResponse,
125125
) -> None:
126126
"""Raises an ApiException when retrieving a non-existent session."""
127-
with pytest.raises(ApiException):
127+
with pytest.raises(ApiError):
128128
client.sessions.session_service_get_session(
129129
str(uuid.uuid4()),
130130
session_token=session.session_token,

spec/check_user_service_spec.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pytest
66

77
import zitadel_client as zitadel
8-
from zitadel_client.exceptions import ApiException
8+
from zitadel_client.exceptions import ApiError
99
from zitadel_client.models import (
1010
UserServiceAddHumanUserRequest,
1111
UserServiceAddHumanUserResponse,
@@ -54,7 +54,7 @@ def user(client: zitadel.Zitadel) -> Generator[UserServiceAddHumanUserResponse,
5454
yield response
5555
try:
5656
client.users.user_service_delete_user(response.user_id) # type: ignore[arg-type]
57-
except ApiException:
57+
except ApiError:
5858
pass
5959

6060

@@ -113,5 +113,5 @@ def test_raises_api_exception_for_nonexistent_user(
113113
client: zitadel.Zitadel,
114114
) -> None:
115115
"""Raises an ApiException when retrieving a non-existent user."""
116-
with pytest.raises(ApiException):
116+
with pytest.raises(ApiError):
117117
client.users.user_service_get_user_by_id(str(uuid.uuid4()))

test/test_api_exception.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import unittest
2+
3+
from zitadel_client.exceptions import ApiError
4+
5+
6+
class ApiExceptionTest(unittest.TestCase):
7+
def test_api_exception(self) -> None:
8+
headers = {"H": "v"}
9+
e = ApiError(418, headers, "body")
10+
11+
self.assertEqual(str(e), "Error 418")
12+
self.assertEqual(e.code, 418)
13+
self.assertEqual(e.response_headers, headers)
14+
self.assertEqual(e.response_body, "body")

zitadel_client/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
from .api_response import ApiResponse # noqa F401
55
from .configuration import Configuration # noqa F401
66
from .exceptions import (
7-
ApiException, # noqa F401
8-
OpenApiError, # noqa F401
7+
ApiError, # noqa F401
8+
ZitadelError, # noqa F401
99
)
1010
from .models import * # noqa: F403, F401
1111
from .zitadel import Zitadel # noqa F401

zitadel_client/api_client.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from zitadel_client.api_response import T as ApiResponseT
2323
from zitadel_client.auth.no_auth_authenticator import NoAuthAuthenticator
2424
from zitadel_client.configuration import Configuration
25-
from zitadel_client.exceptions import ApiException
25+
from zitadel_client.exceptions import ApiError
2626

2727
RequestSerialized = Tuple[str, str, Dict[str, str], Optional[str], List[str]]
2828

@@ -203,7 +203,7 @@ def call_api(
203203
_request_timeout=_request_timeout,
204204
)
205205

206-
except ApiException as e:
206+
except ApiError as e:
207207
raise e
208208

209209
return response_data
@@ -246,11 +246,7 @@ def response_deserialize(
246246
return_data = self.deserialize(response_text, response_type, content_type)
247247
finally:
248248
if not 200 <= response_data.status <= 299:
249-
raise ApiException.from_response(
250-
http_resp=response_data,
251-
body=response_text,
252-
data=return_data,
253-
)
249+
raise ApiError(response_data.status, response_data.getheaders(), return_data)
254250
else:
255251
return ApiResponse(
256252
status_code=response_data.status,
@@ -339,7 +335,7 @@ def deserialize(self, response_text: str, response_type: str, content_type: Opti
339335
elif re.match(r"^text/[a-z.+-]+\s*(;|$)", content_type, re.IGNORECASE):
340336
data = response_text
341337
else:
342-
raise ApiException(status=0, reason="Unsupported content type: {0}".format(content_type))
338+
raise RuntimeError("Unsupported content type: {0}".format(content_type))
343339

344340
return self.__deserialize(data, response_type)
345341

@@ -600,7 +596,7 @@ def __deserialize_date(string):
600596
except ImportError:
601597
return string
602598
except ValueError as err:
603-
raise rest.ApiException(status=0, reason="Failed to parse `{0}` as date object".format(string)) from err
599+
raise RuntimeError("Failed to parse `{0}` as date object".format(string)) from err
604600

605601
# noinspection PyNestedDecorators
606602
@no_type_check
@@ -618,7 +614,7 @@ def __deserialize_datetime(string):
618614
except ImportError:
619615
return string
620616
except ValueError as err:
621-
raise rest.ApiException(status=0, reason=("Failed to parse `{0}` as datetime object".format(string))) from err
617+
raise RuntimeError("Failed to parse `{0}` as datetime object".format(string)) from err
622618

623619
# noinspection PyNestedDecorators
624620
@no_type_check
@@ -633,7 +629,7 @@ def __deserialize_enum(data, klass):
633629
try:
634630
return klass(data)
635631
except ValueError as err:
636-
raise rest.ApiException(status=0, reason=("Failed to parse `{0}` as `{1}`".format(data, klass))) from err
632+
raise RuntimeError("Failed to parse `{0}` as `{1}`".format(data, klass)) from err
637633

638634
# noinspection PyNestedDecorators
639635
@no_type_check

zitadel_client/auth/oauth_authenticator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from authlib.integrations.requests_client import OAuth2Session
66

7-
from zitadel_client import OpenApiError
7+
from zitadel_client import ZitadelError
88
from zitadel_client.auth.authenticator import Authenticator, Token
99
from zitadel_client.auth.open_id import OpenId
1010

@@ -72,7 +72,7 @@ def refresh_token(self) -> Token:
7272
self.token = Token(access_token, expires_at)
7373
return self.token
7474
except Exception as e:
75-
raise OpenApiError("Failed to refresh token: " + str(e)) from e
75+
raise ZitadelError("Failed to refresh token: " + str(e)) from e
7676

7777

7878
T = TypeVar("T", bound="OAuthAuthenticatorBuilder[Any]")

zitadel_client/exceptions.py

Lines changed: 18 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,27 @@
1-
from typing import Any, Optional
1+
from typing import Any, Dict, Optional
22

3-
from typing_extensions import Self
43

5-
import zitadel_client.rest_response
4+
class ZitadelError(Exception):
5+
"""Base exception for all Zitadel API errors."""
66

77

8-
class OpenApiError(Exception):
9-
"""The base exception class for all OpenApiErrors"""
8+
class ApiError(ZitadelError):
9+
"""
10+
Represents an HTTP error from the Zitadel API.
1011
12+
Attributes:
13+
code int HTTP status code
14+
response_headers Dict[str, str] HTTP response headers
15+
response_body Any HTTP response body
16+
"""
1117

12-
class ApiException(OpenApiError): # noqa N818 should be named with an Error suffix later
1318
def __init__(
1419
self,
15-
status: Optional[int] = None,
16-
reason: Optional[str] = None,
17-
http_resp: Optional[zitadel_client.rest_response.RESTResponse] = None,
18-
*,
19-
body: Optional[str] = None,
20-
data: Optional[Any] = None,
20+
code: int,
21+
response_headers: Dict[str, str],
22+
response_body: Optional[Any],
2123
) -> None:
22-
self.status = status
23-
self.reason = reason
24-
self.body = body
25-
self.data = data
26-
self.headers = None
27-
28-
if http_resp:
29-
if self.status is None:
30-
self.status = http_resp.status
31-
if self.reason is None:
32-
self.reason = http_resp.reason
33-
if self.body is None and http_resp.data is not None:
34-
# noinspection PyBroadException
35-
try:
36-
self.body = http_resp.data.decode("utf-8")
37-
except Exception: # noqa: S110
38-
pass
39-
self.headers = http_resp.getheaders()
40-
41-
@classmethod
42-
def from_response(
43-
cls,
44-
*,
45-
http_resp: zitadel_client.rest_response.RESTResponse,
46-
body: Optional[str],
47-
data: Optional[Any],
48-
) -> Self:
49-
if http_resp.status == 400:
50-
raise ApiException(http_resp=http_resp, body=body, data=data)
51-
52-
if http_resp.status == 401:
53-
raise ApiException(http_resp=http_resp, body=body, data=data)
54-
55-
if http_resp.status == 403:
56-
raise ApiException(http_resp=http_resp, body=body, data=data)
57-
58-
if http_resp.status == 404:
59-
raise ApiException(http_resp=http_resp, body=body, data=data)
60-
61-
# Added new conditions for 409 and 422
62-
if http_resp.status == 409:
63-
raise ApiException(http_resp=http_resp, body=body, data=data)
64-
65-
if http_resp.status == 422:
66-
raise ApiException(http_resp=http_resp, body=body, data=data)
67-
68-
if 500 <= http_resp.status <= 599:
69-
raise ApiException(http_resp=http_resp, body=body, data=data)
70-
raise ApiException(http_resp=http_resp, body=body, data=data)
71-
72-
def __str__(self) -> str:
73-
"""Custom error messages for exception"""
74-
error_message = "({0})\nReason: {1}\n".format(self.status, self.reason)
75-
if self.headers:
76-
error_message += "HTTP response headers: {0}\n".format(self.headers)
77-
78-
if self.data or self.body:
79-
error_message += "HTTP response body: {0}\n".format(self.data or self.body)
80-
81-
return error_message
24+
super().__init__(f"Error {code}")
25+
self.code = code
26+
self.response_headers = response_headers
27+
self.response_body = response_body

0 commit comments

Comments
 (0)