Skip to content

Commit d98c4b8

Browse files
authored
Merge pull request #705 from Thetwam/issue/704-ratelimit
RateLimitExceeded
2 parents 9f03017 + 5fd1b7f commit d98c4b8

File tree

5 files changed

+64
-58
lines changed

5 files changed

+64
-58
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased]
44

5+
### General
6+
7+
- Updated `RateLimitExceeded` exception to trigger on HTTP 429 instead of old 403.
8+
59
## [3.4.0] - 2025-11-10
610

711
### New Endpoint Coverage

canvasapi/requester.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -262,21 +262,19 @@ def request(
262262
else:
263263
raise Unauthorized(response.json())
264264
elif response.status_code == 403:
265-
if b"Rate Limit Exceeded" in response.content:
266-
remaining = str(
267-
response.headers.get("X-Rate-Limit-Remaining", "Unknown")
268-
)
269-
raise RateLimitExceeded(
270-
"Rate Limit Exceeded. X-Rate-Limit-Remaining: {}".format(remaining)
271-
)
272-
else:
273-
raise Forbidden(response.text)
265+
raise Forbidden(response.text)
274266
elif response.status_code == 404:
275267
raise ResourceDoesNotExist("Not Found")
276268
elif response.status_code == 409:
277269
raise Conflict(response.text)
278270
elif response.status_code == 422:
279271
raise UnprocessableEntity(response.text)
272+
elif response.status_code == 429:
273+
raise RateLimitExceeded(
274+
"Rate Limit Exceeded. X-Rate-Limit-Remaining: {}".format(
275+
response.headers.get("X-Rate-Limit-Remaining", "Unknown")
276+
)
277+
)
280278
elif response.status_code > 400:
281279
# generic catch-all for error codes
282280
raise CanvasException(

docs/exceptions.rst

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ Quick Guide
1717
+-----------------------------------------------------+-----------------+---------------------------------------------------------------------------------+
1818
| :class:`~canvasapi.exceptions.Forbidden` | 403 | Canvas has denied access to the resource for this user. |
1919
+-----------------------------------------------------+-----------------+---------------------------------------------------------------------------------+
20-
| :class:`~canvasapi.exceptions.RateLimitExceeded` | 403 | Canvas is throttling this request. Try again later. |
21-
+-----------------------------------------------------+-----------------+---------------------------------------------------------------------------------+
2220
| :class:`~canvasapi.exceptions.ResourceDoesNotExist` | 404 | Canvas could not locate the requested resource. |
2321
+-----------------------------------------------------+-----------------+---------------------------------------------------------------------------------+
2422
| :class:`~canvasapi.exceptions.Conflict` | 409 | Canvas had a conflict with an existing resource. |
2523
+-----------------------------------------------------+-----------------+---------------------------------------------------------------------------------+
2624
| :class:`~canvasapi.exceptions.UnprocessableEntity` | 422 | Canvas was unable to process the request. |
2725
+-----------------------------------------------------+-----------------+---------------------------------------------------------------------------------+
26+
| :class:`~canvasapi.exceptions.RateLimitExceeded` | 429 | Canvas is throttling this request. Try again later. |
27+
+-----------------------------------------------------+-----------------+---------------------------------------------------------------------------------+
2828
| :class:`~canvasapi.exceptions.RequiredFieldMissing` | N/A | A required keyword argument was not included. |
2929
+-----------------------------------------------------+-----------------+---------------------------------------------------------------------------------+
3030
| :class:`~canvasapi.exceptions.CanvasException` | N/A | An unknown error was thrown. |
@@ -83,11 +83,6 @@ Class Reference
8383

8484
The :class:`~canvasapi.exceptions.Forbidden` exception is thrown when Canvas returns an HTTP 403 error.
8585

86-
.. autoclass:: canvasapi.exceptions.RateLimitExceeded
87-
:members:
88-
89-
The :class:`~canvasapi.exceptions.RateLimitExceeded` exception is thrown when Canvas returns an HTTP 403 error that includes the body "403 Forbidden (Rate Limit Exceeded)". It will include the value of the ``X-Rate-Limit-Remaining`` header (if available) for reference.
90-
9186
.. autoclass:: canvasapi.exceptions.Conflict
9287
:members:
9388

@@ -97,3 +92,8 @@ Class Reference
9792
:members:
9893

9994
The :class:`~canvasapi.exceptions.UnprocessableEntity` exception is thrown when Canvas returns an HTTP 422 error.
95+
96+
.. autoclass:: canvasapi.exceptions.RateLimitExceeded
97+
:members:
98+
99+
The :class:`~canvasapi.exceptions.RateLimitExceeded` exception is thrown when Canvas returns an HTTP 429 error. It will include the value of the ``X-Rate-Limit-Remaining`` header (if available) for reference.

tests/fixtures/requests.json

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,6 @@
2626
"data": {},
2727
"status_code": 403
2828
},
29-
"403_rate_limit": {
30-
"method": "ANY",
31-
"endpoint": "403_rate_limit",
32-
"data": "403 Forbidden (Rate Limit Exceeded)",
33-
"headers": {
34-
"X-Rate-Limit-Remaining": "3.14159265359",
35-
"X-Request-Cost": "1.61803398875"
36-
},
37-
"status_code": 403
38-
},
39-
"403_rate_limit_no_remaining_header": {
40-
"method": "ANY",
41-
"endpoint": "403_rate_limit_no_remaining_header",
42-
"data": "403 Forbidden (Rate Limit Exceeded)",
43-
"headers": {
44-
"X-Request-Cost": "1.61803398875"
45-
},
46-
"status_code": 403
47-
},
4829
"404": {
4930
"method": "ANY",
5031
"endpoint": "404",
@@ -63,6 +44,29 @@
6344
"data": {},
6445
"status_code": 422
6546
},
47+
"429_rate_limit": {
48+
"method": "ANY",
49+
"endpoint": "429_rate_limit",
50+
"data": {
51+
"error": "Rate limit exceeded. Please wait and try again."
52+
},
53+
"headers": {
54+
"X-Rate-Limit-Remaining": "3.14159265359",
55+
"X-Request-Cost": "1.61803398875"
56+
},
57+
"status_code": 429
58+
},
59+
"429_rate_limit_no_remaining_header": {
60+
"method": "ANY",
61+
"endpoint": "429_rate_limit_no_remaining_header",
62+
"data": {
63+
"error": "Rate limit exceeded. Please wait and try again."
64+
},
65+
"headers": {
66+
"X-Request-Cost": "1.61803398875"
67+
},
68+
"status_code": 429
69+
},
6670
"500": {
6771
"method": "ANY",
6872
"endpoint": "500",
@@ -117,4 +121,4 @@
117121
"data": {},
118122
"status_code": 200
119123
}
120-
}
124+
}

tests/test_requester.py

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -153,28 +153,6 @@ def test_request_403(self, m):
153153
with self.assertRaises(Forbidden):
154154
self.requester.request("GET", "403")
155155

156-
def test_request_403_RateLimitExeeded(self, m):
157-
register_uris({"requests": ["403_rate_limit"]}, m)
158-
159-
with self.assertRaises(RateLimitExceeded) as exc:
160-
self.requester.request("GET", "403_rate_limit")
161-
162-
self.assertEqual(
163-
exc.exception.message,
164-
"Rate Limit Exceeded. X-Rate-Limit-Remaining: 3.14159265359",
165-
)
166-
167-
def test_request_403_RateLimitExeeded_no_remaining_header(self, m):
168-
register_uris({"requests": ["403_rate_limit_no_remaining_header"]}, m)
169-
170-
with self.assertRaises(RateLimitExceeded) as exc:
171-
self.requester.request("GET", "403_rate_limit_no_remaining_header")
172-
173-
self.assertEqual(
174-
exc.exception.message,
175-
"Rate Limit Exceeded. X-Rate-Limit-Remaining: Unknown",
176-
)
177-
178156
def test_request_404(self, m):
179157
register_uris({"requests": ["404"]}, m)
180158

@@ -193,6 +171,28 @@ def test_request_422(self, m):
193171
with self.assertRaises(UnprocessableEntity):
194172
self.requester.request("GET", "422")
195173

174+
def test_request_429_RateLimitExeeded(self, m):
175+
register_uris({"requests": ["429_rate_limit"]}, m)
176+
177+
with self.assertRaises(RateLimitExceeded) as exc:
178+
self.requester.request("GET", "429_rate_limit")
179+
180+
self.assertEqual(
181+
exc.exception.message,
182+
"Rate Limit Exceeded. X-Rate-Limit-Remaining: 3.14159265359",
183+
)
184+
185+
def test_request_429_RateLimitExeeded_no_remaining_header(self, m):
186+
register_uris({"requests": ["429_rate_limit_no_remaining_header"]}, m)
187+
188+
with self.assertRaises(RateLimitExceeded) as exc:
189+
self.requester.request("GET", "429_rate_limit_no_remaining_header")
190+
191+
self.assertEqual(
192+
exc.exception.message,
193+
"Rate Limit Exceeded. X-Rate-Limit-Remaining: Unknown",
194+
)
195+
196196
def test_request_500(self, m):
197197
register_uris({"requests": ["500"]}, m)
198198

0 commit comments

Comments
 (0)