Skip to content

Commit 5c5b694

Browse files
authored
fix: Prevent reusing tokens when rate limited (DenverCoder1#406)
1 parent 5555b3d commit 5c5b694

File tree

1 file changed

+63
-16
lines changed

1 file changed

+63
-16
lines changed

src/stats.php

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,14 @@ function getContributionGraphs(string $user): array
4343
// Get the years the user has contributed
4444
$contributionYears = getContributionYears($user);
4545
// build a list of individual requests
46+
$tokens = [];
4647
$requests = [];
4748
foreach ($contributionYears as $year) {
4849
// create query for year
4950
$query = buildContributionGraphQuery($user, $year);
5051
// create curl request
51-
$requests[$year] = getGraphQLCurlHandle($query);
52+
$tokens[$year] = getGitHubToken();
53+
$requests[$year] = getGraphQLCurlHandle($query, $tokens[$year]);
5254
}
5355
// build multi-curl handle
5456
$multi = curl_multi_init();
@@ -67,19 +69,23 @@ function getContributionGraphs(string $user): array
6769
$decoded = is_string($contents) ? json_decode($contents) : null;
6870
// if response is empty or invalid, retry request one time
6971
if (empty($decoded) || empty($decoded->data)) {
70-
// if rate limit is exceeded, don't retry
72+
// if rate limit is exceeded, don't retry with same token
7173
$message = $decoded->errors[0]->message ?? ($decoded->message ?? "An API error occurred.");
7274
if (str_contains($message, "rate limit exceeded")) {
73-
error_log("Error: $message");
74-
continue;
75+
removeGitHubToken($tokens[$year]);
7576
}
77+
error_log("First attempt to decode response for $user's $year contributions failed. $message");
7678
$query = buildContributionGraphQuery($user, $year);
77-
$request = getGraphQLCurlHandle($query);
79+
$token = getGitHubToken();
80+
$request = getGraphQLCurlHandle($query, $token);
7881
$contents = curl_exec($request);
7982
$decoded = is_string($contents) ? json_decode($contents) : null;
8083
// if the response is still empty or invalid, log an error and skip the year
8184
if (empty($decoded) || empty($decoded->data)) {
8285
$message = $decoded->errors[0]->message ?? ($decoded->message ?? "An API error occurred.");
86+
if (str_contains($message, "rate limit exceeded")) {
87+
removeGitHubToken($token);
88+
}
8389
error_log("Failed to decode response for $user's $year contributions after 2 attempts. $message");
8490
continue;
8591
}
@@ -118,16 +124,46 @@ function getGitHubTokens()
118124
return $tokens;
119125
}
120126

127+
/**
128+
* Get a token from the token pool
129+
*
130+
* @throws AssertionError if no tokens are available
131+
*/
132+
function getGitHubToken()
133+
{
134+
$all_tokens = getGitHubTokens();
135+
return $all_tokens[array_rand($all_tokens)];
136+
}
137+
138+
/**
139+
* Remove a token from the token pool
140+
*
141+
* @param string $token Token to remove
142+
*/
143+
function removeGitHubToken(string $token)
144+
{
145+
$index = array_search($token, $GLOBALS["ALL_TOKENS"]);
146+
if ($index !== false) {
147+
unset($GLOBALS["ALL_TOKENS"][$index]);
148+
}
149+
// if there is no available token, throw an error
150+
if (empty($GLOBALS["ALL_TOKENS"])) {
151+
throw new AssertionError(
152+
"We are being rate-limited! Check <a href='https://git.io/streak-ratelimit' font-weight='bold'>git.io/streak-ratelimit</a> for details.",
153+
429
154+
);
155+
}
156+
}
157+
121158
/** Create a CurlHandle for a POST request to GitHub's GraphQL API
122159
*
123160
* @param string $query GraphQL query
161+
* @param string $token GitHub token to use for the request
124162
*
125163
* @return CurlHandle The curl handle for the request
126164
*/
127-
function getGraphQLCurlHandle(string $query)
165+
function getGraphQLCurlHandle(string $query, string $token)
128166
{
129-
$all_tokens = getGitHubTokens();
130-
$token = $all_tokens[array_rand($all_tokens)];
131167
$headers = [
132168
"Authorization: bearer $token",
133169
"Content-Type: application/json",
@@ -151,36 +187,41 @@ function getGraphQLCurlHandle(string $query)
151187
* Create a POST request to GitHub's GraphQL API
152188
*
153189
* @param string $query GraphQL query
190+
* @param string $token GitHub token to use for the request
154191
*
155192
* @return stdClass An object from the json response of the request
156193
*
157194
* @throws AssertionError If SSL verification fails
158195
*/
159-
function fetchGraphQL(string $query): stdClass
196+
function fetchGraphQL(string $query, string $token): stdClass
160197
{
161-
$ch = getGraphQLCurlHandle($query);
198+
$ch = getGraphQLCurlHandle($query, $token);
162199
$response = curl_exec($ch);
163200
curl_close($ch);
164-
$obj = is_string($response) ? json_decode($response) : null;
201+
$decoded = is_string($response) ? json_decode($response) : null;
165202
// handle curl errors
166-
if ($response === false || $obj === null || curl_getinfo($ch, CURLINFO_HTTP_CODE) >= 400) {
203+
if ($response === false || $decoded === null || curl_getinfo($ch, CURLINFO_HTTP_CODE) >= 400) {
204+
$message = $decoded->errors[0]->message ?? ($decoded->message ?? "");
205+
if (str_contains($message, "rate limit exceeded")) {
206+
removeGitHubToken($token);
207+
}
167208
// set response code to curl error code
168209
http_response_code(curl_getinfo($ch, CURLINFO_HTTP_CODE));
169210
// Missing SSL certificate
170211
if (str_contains(curl_error($ch), "unable to get local issuer certificate")) {
171212
throw new AssertionError("You don't have a valid SSL Certificate installed or XAMPP.", 400);
172213
}
173214
// Handle errors such as "Bad credentials"
174-
if ($obj && $obj->message) {
175-
throw new AssertionError("Error: $obj->message \n<!-- $response -->", 401);
215+
if ($message) {
216+
throw new AssertionError("Error: $message \n<!-- $response -->", 401);
176217
}
177218
// Handle curl errors
178219
if (curl_errno($ch)) {
179220
throw new AssertionError("cURL error: " . curl_error($ch) . "\n<!-- $response -->", 500);
180221
}
181222
throw new AssertionError("An error occurred when getting a response from GitHub.\n<!-- $response -->", 502);
182223
}
183-
return $obj;
224+
return $decoded;
184225
}
185226

186227
/**
@@ -201,7 +242,13 @@ function getContributionYears(string $user): array
201242
}
202243
}
203244
}";
204-
$response = fetchGraphQL($query);
245+
try {
246+
$response = fetchGraphQL($query, getGitHubToken());
247+
} catch (AssertionError $e) {
248+
// retry once if an error occurred
249+
error_log("An error occurred getting contribution years for $user: " . $e->getMessage());
250+
$response = fetchGraphQL($query, getGitHubToken());
251+
}
205252
// User not found
206253
if (!empty($response->errors)) {
207254
$type = $response->errors[0]->type ?? "";

0 commit comments

Comments
 (0)