Skip to content

Commit 1a1c206

Browse files
committed
Check GitHub ratelimit correctly
Just removing the ratelimit threshold will cause Anitya to fail on KeyError as instead of 403 when the ratelimit is reached GitHub returns 200 and json with error structure, which is completely different then standard response. To fix this we need to check the response header for specific parameters regarding rate limit. This change is checking X-RateLimit-Remaining and X-RateLimit-Reset parameters in response header and raising RateLimitException when the limit is reached. Signed-off-by: Michal Konecny <mkonecny@redhat.com>
1 parent 330d3d4 commit 1a1c206

15 files changed

+45
-63
lines changed

anitya/lib/backends/github.py

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
# -*- coding: utf-8 -*-
22

33
"""
4-
(c) 2014-2020 - Copyright Red Hat Inc
4+
(c) 2014-2025 - Copyright Red Hat Inc
55
66
Authors:
77
Pierre-Yves Chibon <pingou@pingoured.fr>
88
Michal Konecny <mkonecny@redhat.com>
99
1010
"""
11-
1211
import logging
1312

13+
import arrow
14+
1415
from anitya.config import config
1516
from anitya.lib import utilities
1617
from anitya.lib.backends import REQUEST_HEADERS, BaseBackend, http_session
@@ -20,13 +21,6 @@
2021

2122
_log = logging.getLogger(__name__)
2223

23-
"""
24-
Reset time that is currently set for GitHub backend.
25-
Used when GitHub starts returning HTTP status code 403,
26-
which doesn't contains this information anymore.
27-
"""
28-
reset_time = "1970-01-01T00:00:00Z"
29-
3024

3125
class GithubBackend(BaseBackend):
3226
"""The custom class for projects hosted on github.com.
@@ -95,10 +89,17 @@ def _retrieve_versions(cls, owner, repo, project):
9589
) from err
9690

9791
if resp.ok:
92+
if int(resp.headers["X-RateLimit-Remaining"]) == 0:
93+
_log.info("Github API ratelimit reached.")
94+
reset_time = arrow.Arrow.utcfromtimestamp(
95+
resp.headers["X-RateLimit-Reset"]
96+
)
97+
raise RateLimitException(reset_time.isoformat())
9898
json = resp.json()
9999
elif resp.status_code == 403:
100100
_log.info("Github API ratelimit reached.")
101-
raise RateLimitException(reset_time)
101+
reset_time = arrow.Arrow.utcfromtimestamp(resp.headers["X-RateLimit-Reset"])
102+
raise RateLimitException(reset_time.isoformat())
102103
else:
103104
raise AnityaPluginException(
104105
f"{project.name}: Server responded with status "
@@ -200,23 +201,6 @@ def parse_json(json, project):
200201
when rate limit threshold is reached.
201202
202203
"""
203-
global reset_time # pylint: disable=W0603
204-
# We need to check limit first,
205-
# because exceeding the limit will also return error
206-
try:
207-
remaining = json["data"]["rateLimit"]["remaining"]
208-
reset_time = json["data"]["rateLimit"]["resetAt"]
209-
_log.debug(
210-
"Github API ratelimit remains %s, will reset at %s UTC",
211-
remaining,
212-
reset_time,
213-
)
214-
215-
if remaining <= 0:
216-
raise RateLimitException(reset_time)
217-
except KeyError:
218-
_log.info("Github API ratelimit key is missing. Checking for errors.")
219-
220204
if "errors" in json:
221205
error_str = ""
222206
for error in json["errors"]:

anitya/tests/lib/backends/test_github.py

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: utf-8 -*-
22
#
3-
# Copyright © 2018 Red Hat, Inc.
3+
# Copyright © 2018-2025 Red Hat, Inc.
44
#
55
# This copyrighted material is made available to anyone wishing to use,
66
# modify, copy, or redistribute it subject to the terms and conditions
@@ -25,6 +25,7 @@
2525

2626
import unittest
2727

28+
import arrow
2829
import mock
2930

3031
import anitya.lib.backends.github as backend
@@ -414,6 +415,20 @@ def test_get_versions_no_version_retrieved(self):
414415
AnityaPluginException, backend.GithubBackend.get_versions, project
415416
)
416417

418+
@mock.patch.dict("anitya.config.config", {"GITHUB_ACCESS_TOKEN": "foobar"})
419+
@mock.patch("anitya.lib.backends.http_session.post")
420+
def test_get_versions_ratelimit_reached(self, mock_post):
421+
"""Test the get_versions function of the github when ratelimit is reached."""
422+
mock_resp = mock.MagicMock()
423+
mock_resp.ok = True
424+
mock_resp.headers = {"X-RateLimit-Remaining": "0", "X-RateLimit-Reset": "0"}
425+
mock_post.return_value = mock_resp
426+
project = self.projects["valid_without_version_url"]
427+
with self.assertRaises(RateLimitException) as test:
428+
backend.GithubBackend.get_versions(project)
429+
430+
self.assertEqual(test.exception.reset_time, arrow.get("1970-01-01T00:00:00Z"))
431+
417432
@mock.patch.dict("anitya.config.config", {"GITHUB_ACCESS_TOKEN": "foobar"})
418433
@mock.patch("anitya.lib.backends.http_session.post")
419434
def test_get_versions_403(self, mock_post):
@@ -423,13 +438,13 @@ def test_get_versions_403(self, mock_post):
423438
mock_resp = mock.MagicMock()
424439
mock_resp.status_code = 403
425440
mock_resp.ok = False
441+
mock_resp.headers = {"X-RateLimit-Remaining": "0", "X-RateLimit-Reset": "0"}
426442
mock_post.return_value = mock_resp
427443
project = self.projects["valid_without_version_url"]
428-
backend.reset_time = "1970-01-01T00:00:00Z"
429-
self.assertRaises(
430-
RateLimitException, backend.GithubBackend.get_versions, project
431-
)
432-
self.assertEqual(backend.reset_time, "1970-01-01T00:00:00Z")
444+
with self.assertRaises(RateLimitException) as test:
445+
backend.GithubBackend.get_versions(project)
446+
447+
self.assertEqual(test.exception.reset_time, arrow.get("1970-01-01T00:00:00Z"))
433448

434449
@mock.patch.dict("anitya.config.config", {"GITHUB_ACCESS_TOKEN": "foobar"})
435450
def test_plexus_utils(self):
@@ -610,23 +625,6 @@ def test_parse_json_with_errors(self):
610625

611626
self.assertIn('"FOO": "BAR"', str(excinfo.exception))
612627

613-
def test_parse_json_threshold_exceeded(self):
614-
"""Test behavior when rate limit threshold is exceeded."""
615-
# Limit reached
616-
json = {
617-
"data": {
618-
"repository": {"refs": {"totalCount": 0}},
619-
"rateLimit": {
620-
"limit": 5000,
621-
"remaining": 0,
622-
"resetAt": "2008-09-03T20:56:35.450686",
623-
},
624-
}
625-
}
626-
with self.assertRaises(RateLimitException):
627-
backend.parse_json(json, self.project)
628-
self.assertEqual(backend.reset_time, "2008-09-03T20:56:35.450686")
629-
630628
def test_parse_json_tag_missing(self):
631629
"""Test parsing a JSON skips releases where tag is missing."""
632630
project = models.Project(

anitya/tests/request-data/anitya.tests.lib.backends.test_github.GithubBackendtests.test_gargoyle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ interactions:
4949
x-oauth-scopes: ['repo:status']
5050
x-ratelimit-limit: ['5000']
5151
x-ratelimit-remaining: ['5000']
52-
x-ratelimit-reset: ['1539938803']
52+
x-ratelimit-reset: ['2147483647']
5353
x-xss-protection: [1; mode=block]
5454
status: {code: 200, message: OK}
5555
version: 1

anitya/tests/request-data/anitya.tests.lib.backends.test_github.GithubBackendtests.test_get_version_invalid_unknown_project

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ interactions:
7272
X-RateLimit-Remaining:
7373
- '4999'
7474
X-RateLimit-Reset:
75-
- '1576605150'
75+
- '2147483647'
7676
X-XSS-Protection:
7777
- 1; mode=block
7878
content-length:

anitya/tests/request-data/anitya.tests.lib.backends.test_github.GithubBackendtests.test_get_version_valid_with_version_url

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ interactions:
7171
X-RateLimit-Remaining:
7272
- '4996'
7373
X-RateLimit-Reset:
74-
- '1576605295'
74+
- '2147483647'
7575
X-XSS-Protection:
7676
- 1; mode=block
7777
content-length:

anitya/tests/request-data/anitya.tests.lib.backends.test_github.GithubBackendtests.test_get_version_valid_without_version_url

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ interactions:
7171
X-RateLimit-Remaining:
7272
- '4994'
7373
X-RateLimit-Reset:
74-
- '1576605418'
74+
- '2147483647'
7575
X-XSS-Protection:
7676
- 1; mode=block
7777
content-length:

anitya/tests/request-data/anitya.tests.lib.backends.test_github.GithubBackendtests.test_get_versions_filter

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ interactions:
8989
X-RateLimit-Remaining:
9090
- '4999'
9191
X-RateLimit-Reset:
92-
- '1615217132'
92+
- '2147483647'
9393
X-RateLimit-Used:
9494
- '1'
9595
X-XSS-Protection:

anitya/tests/request-data/anitya.tests.lib.backends.test_github.GithubBackendtests.test_get_versions_invalid_unknown_project

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ interactions:
7474
X-RateLimit-Remaining:
7575
- '4990'
7676
X-RateLimit-Reset:
77-
- '1576682771'
77+
- '2147483647'
7878
X-XSS-Protection:
7979
- 1; mode=block
8080
content-length:

anitya/tests/request-data/anitya.tests.lib.backends.test_github.GithubBackendtests.test_get_versions_no_token

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ interactions:
3737
x-github-request-id: ['BF56:23A8:3340315:62F7CF2:5B7FEF90']
3838
x-ratelimit-limit: ['0']
3939
x-ratelimit-remaining: ['0']
40-
x-ratelimit-reset: ['1535114656']
40+
x-ratelimit-reset: ['2147483647']
4141
x-runtime-rack: ['0.011707']
4242
x-xss-protection: [1; mode=block]
4343
status: {code: 401, message: Unauthorized}

anitya/tests/request-data/anitya.tests.lib.backends.test_github.GithubBackendtests.test_get_versions_no_version_retrieved

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ interactions:
4444
x-oauth-scopes: ['repo:status']
4545
x-ratelimit-limit: ['5000']
4646
x-ratelimit-remaining: ['5000']
47-
x-ratelimit-reset: ['1537438156']
47+
x-ratelimit-reset: ['2147483647']
4848
x-runtime-rack: ['0.107819']
4949
x-xss-protection: [1; mode=block]
5050
status: {code: 200, message: OK}

0 commit comments

Comments
 (0)