Skip to content

Commit 249dbff

Browse files
feat: Add retry_after property to ApiError
1 parent 8d6900c commit 249dbff

File tree

3 files changed

+40
-3
lines changed

3 files changed

+40
-3
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66

77
Check our main [developer changelog](https://developer.paddle.com/?utm_source=dx&utm_medium=paddle-python-sdk) for information about changes to the Paddle Billing platform, the Paddle API, and other developer tools.
88

9+
## [Unreleased]
10+
11+
### Added
12+
13+
- `ApiError` will now have `retry_after` property set for [too_many_requests](https://developer.paddle.com/errors/shared/too_many_requests?utm_source=dx&utm_medium=paddle-python-sdk) errors
14+
915
## 1.10.0 - 2025-08-15
1016

1117
### Added
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
from paddle_billing.Exceptions.FieldError import FieldError
22

3-
from requests import HTTPError
3+
from requests import HTTPError, Response
44

55

66
class ApiError(HTTPError):
7-
def __init__(self, response, error_type, error_code, detail, docs_url, *field_errors):
7+
def __init__(self, response: Response, error_type: str, error_code: str, detail: str, docs_url: str, *field_errors):
88
super().__init__(detail, response=response)
99
self.error_type = error_type
1010
self.error_code = error_code
1111
self.detail = detail
1212
self.docs_url = docs_url
1313
self.field_errors = field_errors
14+
retry_after = response.headers.get("Retry-After")
15+
self.retry_after = int(retry_after) if retry_after else None
1416

1517
def __repr__(self):
1618
return (
@@ -19,6 +21,6 @@ def __repr__(self):
1921
)
2022

2123
@classmethod
22-
def from_error_data(cls, response, error):
24+
def from_error_data(cls, response: Response, error):
2325
field_errors = [FieldError(fe["field"], fe["message"]) for fe in error.get("errors", [])]
2426
return cls(response, error["type"], error["code"], error["detail"], error["documentation_url"], *field_errors)

tests/Functional/Client/test_Client.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ class TestClient:
1919
"expected_reason",
2020
"expected_response_body",
2121
"expected_exception",
22+
"headers",
23+
"expected_retry_after",
2224
],
2325
[
2426
(
@@ -35,6 +37,8 @@ class TestClient:
3537
"meta": {"request_id": "f00bb3ca-399d-4686-889c-50b028f4c912"},
3638
},
3739
ApiError,
40+
{},
41+
None,
3842
),
3943
(
4044
404,
@@ -49,6 +53,8 @@ class TestClient:
4953
"meta": {"request_id": "f00bb3ca-399d-4686-889c-50b028f4c912"},
5054
},
5155
ApiError,
56+
{},
57+
None,
5258
),
5359
(
5460
400,
@@ -63,12 +69,31 @@ class TestClient:
6369
"meta": {"request_id": "f00bb3ca-399d-4686-889c-50b028f4c912"},
6470
},
6571
AddressApiError,
72+
{},
73+
None,
74+
),
75+
(
76+
429,
77+
"Too Many Requests",
78+
{
79+
"error": {
80+
"type": "request_error",
81+
"code": "too_many_requests",
82+
"detail": "IP address exceeded the allowed rate limit. Retry after the number of seconds in the Retry-After header.",
83+
"documentation_url": "https://developer.paddle.com/errors/shared/too_many_requests",
84+
},
85+
"meta": {"request_id": "f00bb3ca-399d-4686-889c-50b028f4c912"},
86+
},
87+
ApiError,
88+
{"Retry-After": "42"},
89+
42,
6690
),
6791
],
6892
ids=[
6993
"Returns bad_request response",
7094
"Returns not_found response",
7195
"Returns address_location_not_allowed response",
96+
"Returns too_many_requests response",
7297
],
7398
)
7499
def test_post_raw_returns_error_response(
@@ -80,6 +105,8 @@ def test_post_raw_returns_error_response(
80105
expected_reason,
81106
expected_response_body,
82107
expected_exception,
108+
headers,
109+
expected_retry_after,
83110
):
84111
expected_request_url = f"{test_client.base_url}/some/url"
85112
expected_request_body = {"some_property": "some value"}
@@ -88,6 +115,7 @@ def test_post_raw_returns_error_response(
88115
status_code=expected_response_status,
89116
text=dumps(expected_response_body),
90117
reason=expected_reason,
118+
headers=headers,
91119
)
92120

93121
with raises(expected_exception) as exception_info:
@@ -111,6 +139,7 @@ def test_post_raw_returns_error_response(
111139
assert api_error.error_code == expected_response_body["error"]["code"]
112140
assert api_error.detail == expected_response_body["error"]["detail"]
113141
assert api_error.docs_url == expected_response_body["error"]["documentation_url"]
142+
assert api_error.retry_after == expected_retry_after
114143

115144
if "errors" in expected_response_body["error"]:
116145
assert len(api_error.field_errors) == len(expected_response_body["error"]["errors"])

0 commit comments

Comments
 (0)