Skip to content

Commit b107c06

Browse files
authored
fix(backchannel): expose headers on slow_down errors (HTTP 429s) (#744)
### Changes In #720, we added the `headers` field on instances of `Auth0Error`, with the intent to actually expose the `Retry-After` headers included in backchannel responses (when the `slow_down` error case is hit). This error case is actually producing an HTTP 429 status code, which leads to instantiating a `RateLimitError` instead of an `Auth0Error` (the former was not adjusted to expose `headers` in the previous PR). In this PR, I am adding the `headers` field to `RateLimitError` as well as setting this field when raising this exception. ### References Please include relevant links supporting this change such as a: - support ticket - community post - StackOverflow post - support forum thread ### Testing Please describe how this can be tested by reviewers. Be specific about anything not tested and reasons why. If this library has unit and/or integration testing, tests should be added for new functionality and existing tests should complete without errors. - [x] This change adds unit test coverage - [ ] This change adds integration test coverage - [ ] This change has been tested on the latest version of the platform/language or why not ### Checklist - [x] I have read the [Auth0 general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md) - [x] I have read the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md) - [x] All existing and new tests complete without errors
2 parents 612c26e + 4fdcd26 commit b107c06

File tree

5 files changed

+20
-7
lines changed

5 files changed

+20
-7
lines changed

auth0/exceptions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ def __str__(self) -> str:
2323

2424

2525
class RateLimitError(Auth0Error):
26-
def __init__(self, error_code: str, message: str, reset_at: int) -> None:
27-
super().__init__(status_code=429, error_code=error_code, message=message)
26+
def __init__(self, error_code: str, message: str, reset_at: int, headers: Any | None = None) -> None:
27+
super().__init__(status_code=429, error_code=error_code, message=message, headers=headers)
2828
self.reset_at = reset_at
2929

3030

auth0/rest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ def content(self) -> Any:
289289
error_code=self._error_code(),
290290
message=self._error_message(),
291291
reset_at=reset_at,
292+
headers=self._headers,
292293
)
293294
if self._error_code() == "mfa_required":
294295
raise Auth0Error(

auth0/test/authentication/test_base.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,10 @@ def test_post_rate_limit_error(self, mock_request):
158158
self.assertEqual(context.exception.message, "desc")
159159
self.assertIsInstance(context.exception, RateLimitError)
160160
self.assertEqual(context.exception.reset_at, 9)
161+
self.assertIsNotNone(context.exception.headers)
162+
self.assertEqual(context.exception.headers["x-ratelimit-limit"], "3")
163+
self.assertEqual(context.exception.headers["x-ratelimit-remaining"], "6")
164+
self.assertEqual(context.exception.headers["x-ratelimit-reset"], "9")
161165

162166
@mock.patch("requests.request")
163167
def test_post_rate_limit_error_without_headers(self, mock_request):
@@ -177,6 +181,8 @@ def test_post_rate_limit_error_without_headers(self, mock_request):
177181
self.assertEqual(context.exception.message, "desc")
178182
self.assertIsInstance(context.exception, RateLimitError)
179183
self.assertEqual(context.exception.reset_at, -1)
184+
self.assertIsNotNone(context.exception.headers)
185+
self.assertEqual(context.exception.headers, {})
180186

181187
@mock.patch("requests.request")
182188
def test_post_error_with_code_property(self, mock_request):

auth0/test/authentication/test_get_token.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from cryptography.hazmat.primitives import asymmetric, serialization
88

9-
from ... import Auth0Error
9+
from ...exceptions import RateLimitError
1010
from ...authentication.get_token import GetToken
1111

1212

@@ -339,22 +339,22 @@ def test_backchannel_login(self, mock_post):
339339
)
340340

341341
@mock.patch("requests.request")
342-
def test_backchannel_login_headers_on_failure(self, mock_requests_request):
342+
def test_backchannel_login_headers_on_slow_down(self, mock_requests_request):
343343
response = requests.Response()
344-
response.status_code = 400
344+
response.status_code = 429
345345
response.headers = {"Retry-After": "100"}
346346
response._content = b'{"error":"slow_down"}'
347347
mock_requests_request.return_value = response
348348

349349
g = GetToken("my.domain.com", "cid", client_secret="csec")
350350

351-
with self.assertRaises(Auth0Error) as context:
351+
with self.assertRaises(RateLimitError) as context:
352352
g.backchannel_login(
353353
auth_req_id="reqid",
354354
grant_type="urn:openid:params:grant-type:ciba",
355355
)
356356
self.assertEqual(context.exception.headers["Retry-After"], "100")
357-
self.assertEqual(context.exception.status_code, 400)
357+
self.assertEqual(context.exception.status_code, 429)
358358

359359
@mock.patch("auth0.rest.RestClient.post")
360360
def test_connection_login(self, mock_post):

auth0/test/management/test_rest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,10 @@ def test_get_rate_limit_error(self, mock_request):
278278
self.assertEqual(context.exception.message, "message")
279279
self.assertIsInstance(context.exception, RateLimitError)
280280
self.assertEqual(context.exception.reset_at, 9)
281+
self.assertIsNotNone(context.exception.headers)
282+
self.assertEqual(context.exception.headers["x-ratelimit-limit"], "3")
283+
self.assertEqual(context.exception.headers["x-ratelimit-remaining"], "6")
284+
self.assertEqual(context.exception.headers["x-ratelimit-reset"], "9")
281285

282286
self.assertEqual(rc._metrics["retries"], 0)
283287

@@ -300,6 +304,8 @@ def test_get_rate_limit_error_without_headers(self, mock_request):
300304
self.assertEqual(context.exception.message, "message")
301305
self.assertIsInstance(context.exception, RateLimitError)
302306
self.assertEqual(context.exception.reset_at, -1)
307+
self.assertIsNotNone(context.exception.headers)
308+
self.assertEqual(context.exception.headers, {})
303309

304310
self.assertEqual(rc._metrics["retries"], 1)
305311

0 commit comments

Comments
 (0)