Skip to content

Commit fa5a831

Browse files
committed
feat: added headers to ResendError
1 parent c8835ae commit fa5a831

3 files changed

Lines changed: 77 additions & 4 deletions

File tree

resend/exceptions.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
codes as outlined in https://resend.com/docs/api-reference/errors.
55
"""
66

7-
from typing import Any, Dict, NoReturn, Union
7+
from typing import Any, Dict, NoReturn, Optional, Union
88

99

1010
class ResendError(Exception):
@@ -29,12 +29,14 @@ def __init__(
2929
error_type: str,
3030
message: str,
3131
suggested_action: str,
32+
headers: Optional[Dict[str, str]] = None,
3233
):
3334
Exception.__init__(self, message)
3435
self.code = code
3536
self.message = message
3637
self.suggested_action = suggested_action
3738
self.error_type = error_type
39+
self.headers = headers or {}
3840

3941

4042
class MissingApiKeyError(ResendError):
@@ -45,6 +47,7 @@ def __init__(
4547
message: str,
4648
error_type: str,
4749
code: Union[str, int],
50+
headers: Optional[Dict[str, str]] = None,
4851
):
4952
suggested_action = """Include the following header
5053
Authorization: Bearer YOUR_API_KEY in the request."""
@@ -57,6 +60,7 @@ def __init__(
5760
suggested_action=suggested_action,
5861
code=code,
5962
error_type=error_type,
63+
headers=headers,
6064
)
6165

6266

@@ -68,6 +72,7 @@ def __init__(
6872
message: str,
6973
error_type: str,
7074
code: Union[str, int],
75+
headers: Optional[Dict[str, str]] = None,
7176
):
7277
suggested_action = """Generate a new API key in the dashboard."""
7378

@@ -77,6 +82,7 @@ def __init__(
7782
suggested_action=suggested_action,
7883
code=code,
7984
error_type=error_type,
85+
headers=headers,
8086
)
8187

8288

@@ -88,6 +94,7 @@ def __init__(
8894
message: str,
8995
error_type: str,
9096
code: Union[str, int],
97+
headers: Optional[Dict[str, str]] = None,
9198
):
9299
default_message = """
93100
The request body is missing one or more required fields."""
@@ -104,6 +111,7 @@ def __init__(
104111
message=message,
105112
suggested_action=suggested_action,
106113
error_type=error_type,
114+
headers=headers,
107115
)
108116

109117

@@ -115,6 +123,7 @@ def __init__(
115123
message: str,
116124
error_type: str,
117125
code: Union[str, int],
126+
headers: Optional[Dict[str, str]] = None,
118127
):
119128
default_message = """
120129
The request body is missing one or more required fields."""
@@ -131,6 +140,7 @@ def __init__(
131140
message=message,
132141
suggested_action=suggested_action,
133142
error_type=error_type,
143+
headers=headers,
134144
)
135145

136146

@@ -142,6 +152,7 @@ def __init__(
142152
message: str,
143153
error_type: str,
144154
code: Union[str, int],
155+
headers: Optional[Dict[str, str]] = None,
145156
):
146157
default_message = """
147158
Something went wrong."""
@@ -157,6 +168,7 @@ def __init__(
157168
message=message,
158169
suggested_action=suggested_action,
159170
error_type=error_type,
171+
headers=headers,
160172
)
161173

162174

@@ -168,6 +180,7 @@ def __init__(
168180
message: str,
169181
error_type: str,
170182
code: Union[str, int],
183+
headers: Optional[Dict[str, str]] = None,
171184
):
172185
suggested_action = """Reduce your request rate or wait before retrying. """
173186
suggested_action += """Check the response headers for rate limit information."""
@@ -178,6 +191,7 @@ def __init__(
178191
message=message,
179192
suggested_action=suggested_action,
180193
error_type=error_type,
194+
headers=headers,
181195
)
182196

183197

@@ -200,14 +214,18 @@ def __init__(
200214

201215

202216
def raise_for_code_and_type(
203-
code: Union[str, int], error_type: str, message: str
217+
code: Union[str, int],
218+
error_type: str,
219+
message: str,
220+
headers: Optional[Dict[str, str]] = None,
204221
) -> NoReturn:
205222
"""Raise the appropriate error based on the code and type.
206223
207224
Args:
208225
code (str): The error code
209226
error_type (str): The error type
210227
message (str): The error message
228+
headers (Optional[Dict[str, str]]): The HTTP response headers
211229
212230
Raises:
213231
ResendError: If it is a Resend err
@@ -231,7 +249,8 @@ def raise_for_code_and_type(
231249
# Handle the case where the error might be unknown
232250
if error is None:
233251
raise ResendError(
234-
code=code, message=message, error_type=error_type, suggested_action=""
252+
code=code, message=message, error_type=error_type, suggested_action="",
253+
headers=headers,
235254
)
236255

237256
# Raise error from errors list
@@ -242,10 +261,12 @@ def raise_for_code_and_type(
242261
code=code,
243262
message=message,
244263
error_type=error_type,
264+
headers=headers,
245265
)
246266
# defaults to ResendError if finally can't find error type
247267
raise ResendError(
248-
code=code, message=message, error_type=error_type, suggested_action=""
268+
code=code, message=message, error_type=error_type, suggested_action="",
269+
headers=headers,
249270
)
250271

251272

resend/request.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def perform(self) -> Union[T, None]:
3838
code=data.get("statusCode") or 500,
3939
message=data.get("message", "Unknown error"),
4040
error_type=data.get("name", "InternalServerError"),
41+
headers=self._response_headers,
4142
)
4243

4344
if isinstance(data, dict):
@@ -106,6 +107,7 @@ def make_request(self, url: str) -> Union[Dict[str, Any], List[Any]]:
106107
code=500,
107108
message=f"Expected JSON response but got: {content_type}",
108109
error_type="InternalServerError",
110+
headers=self._response_headers,
109111
)
110112

111113
try:
@@ -120,4 +122,5 @@ def make_request(self, url: str) -> Union[Dict[str, Any], List[Any]]:
120122
code=500,
121123
message="Failed to decode JSON response",
122124
error_type="InternalServerError",
125+
headers=self._response_headers,
123126
)

tests/exceptions_test.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,52 @@ def test_monthly_quota_exceeded_error(self) -> None:
5555
assert e.type is RateLimitError
5656
assert e.value.code == 429
5757
assert e.value.error_type == "monthly_quota_exceeded"
58+
59+
def test_headers_default_to_empty_dict(self) -> None:
60+
with pytest.raises(ResendError) as e:
61+
raise_for_code_and_type(999, "error_type", "msg")
62+
assert e.value.headers == {}
63+
64+
def test_headers_passed_to_known_error(self) -> None:
65+
headers = {
66+
"retry-after": "5",
67+
"x-ratelimit-limit": "100",
68+
"x-ratelimit-remaining": "0",
69+
"x-ratelimit-reset": "1699564800",
70+
}
71+
with pytest.raises(RateLimitError) as e:
72+
raise_for_code_and_type(
73+
429, "rate_limit_exceeded", "Rate limit exceeded",
74+
headers=headers,
75+
)
76+
assert e.value.headers == headers
77+
assert e.value.headers["retry-after"] == "5"
78+
assert e.value.headers["x-ratelimit-remaining"] == "0"
79+
80+
def test_headers_passed_to_unknown_error(self) -> None:
81+
headers = {"x-request-id": "req_123"}
82+
with pytest.raises(ResendError) as e:
83+
raise_for_code_and_type(999, "unknown", "msg", headers=headers)
84+
assert e.value.headers == headers
85+
86+
def test_headers_passed_to_unknown_error_type(self) -> None:
87+
headers = {"x-request-id": "req_456"}
88+
with pytest.raises(ResendError) as e:
89+
raise_for_code_and_type(500, "unknown_type", "msg", headers=headers)
90+
assert e.value.headers == headers
91+
92+
def test_headers_on_validation_error(self) -> None:
93+
headers = {"x-request-id": "req_789"}
94+
with pytest.raises(ValidationError) as e:
95+
raise_for_code_and_type(
96+
400, "validation_error", "err", headers=headers,
97+
)
98+
assert e.value.headers == headers
99+
100+
def test_headers_on_application_error(self) -> None:
101+
headers = {"x-request-id": "req_abc"}
102+
with pytest.raises(ApplicationError) as e:
103+
raise_for_code_and_type(
104+
500, "application_error", "err", headers=headers,
105+
)
106+
assert e.value.headers == headers

0 commit comments

Comments
 (0)