Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Prevent reusing tokens when rate limited #406

Merged
merged 3 commits into from
Jan 3, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 63 additions & 16 deletions src/stats.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,14 @@ function getContributionGraphs(string $user): array
// Get the years the user has contributed
$contributionYears = getContributionYears($user);
// build a list of individual requests
$tokens = [];
$requests = [];
foreach ($contributionYears as $year) {
// create query for year
$query = buildContributionGraphQuery($user, $year);
// create curl request
$requests[$year] = getGraphQLCurlHandle($query);
$tokens[$year] = getGitHubToken();
$requests[$year] = getGraphQLCurlHandle($query, $tokens[$year]);
}
// build multi-curl handle
$multi = curl_multi_init();
Expand All @@ -67,19 +69,23 @@ function getContributionGraphs(string $user): array
$decoded = is_string($contents) ? json_decode($contents) : null;
// if response is empty or invalid, retry request one time
if (empty($decoded) || empty($decoded->data)) {
// if rate limit is exceeded, don't retry
// if rate limit is exceeded, don't retry with same token
$message = $decoded->errors[0]->message ?? ($decoded->message ?? "An API error occurred.");
if (str_contains($message, "rate limit exceeded")) {
error_log("Error: $message");
continue;
removeGitHubToken($tokens[$year]);
}
error_log("First attempt to decode response for $user's $year contributions failed. $message");
$query = buildContributionGraphQuery($user, $year);
$request = getGraphQLCurlHandle($query);
$token = getGitHubToken();
$request = getGraphQLCurlHandle($query, $token);
$contents = curl_exec($request);
$decoded = is_string($contents) ? json_decode($contents) : null;
// if the response is still empty or invalid, log an error and skip the year
if (empty($decoded) || empty($decoded->data)) {
$message = $decoded->errors[0]->message ?? ($decoded->message ?? "An API error occurred.");
if (str_contains($message, "rate limit exceeded")) {
removeGitHubToken($token);
}
error_log("Failed to decode response for $user's $year contributions after 2 attempts. $message");
continue;
}
Expand Down Expand Up @@ -118,16 +124,46 @@ function getGitHubTokens()
return $tokens;
}

/**
* Get a token from the token pool
*
* @throws AssertionError if no tokens are available
*/
function getGitHubToken()
{
$all_tokens = getGitHubTokens();
return $all_tokens[array_rand($all_tokens)];
}

/**
* Remove a token from the token pool
*
* @param string $token Token to remove
*/
function removeGitHubToken(string $token)
{
$index = array_search($token, $GLOBALS["ALL_TOKENS"]);
if ($index !== false) {
unset($GLOBALS["ALL_TOKENS"][$index]);
}
// if there is no available token, throw an error
if (empty($GLOBALS["ALL_TOKENS"])) {
throw new AssertionError(
"We are being rate-limited! Check <a href='https://git.io/streak-ratelimit' font-weight='bold'>git.io/streak-ratelimit</a> for details.",
429
);
}
}

/** Create a CurlHandle for a POST request to GitHub's GraphQL API
*
* @param string $query GraphQL query
* @param string $token GitHub token to use for the request
*
* @return CurlHandle The curl handle for the request
*/
function getGraphQLCurlHandle(string $query)
function getGraphQLCurlHandle(string $query, string $token)
{
$all_tokens = getGitHubTokens();
$token = $all_tokens[array_rand($all_tokens)];
$headers = [
"Authorization: bearer $token",
"Content-Type: application/json",
Expand All @@ -151,36 +187,41 @@ function getGraphQLCurlHandle(string $query)
* Create a POST request to GitHub's GraphQL API
*
* @param string $query GraphQL query
* @param string $token GitHub token to use for the request
*
* @return stdClass An object from the json response of the request
*
* @throws AssertionError If SSL verification fails
*/
function fetchGraphQL(string $query): stdClass
function fetchGraphQL(string $query, string $token): stdClass
{
$ch = getGraphQLCurlHandle($query);
$ch = getGraphQLCurlHandle($query, $token);
$response = curl_exec($ch);
curl_close($ch);
$obj = is_string($response) ? json_decode($response) : null;
$decoded = is_string($response) ? json_decode($response) : null;
// handle curl errors
if ($response === false || $obj === null || curl_getinfo($ch, CURLINFO_HTTP_CODE) >= 400) {
if ($response === false || $decoded === null || curl_getinfo($ch, CURLINFO_HTTP_CODE) >= 400) {
$message = $decoded->errors[0]->message ?? ($decoded->message ?? "");
if (str_contains($message, "rate limit exceeded")) {
removeGitHubToken($token);
}
// set response code to curl error code
http_response_code(curl_getinfo($ch, CURLINFO_HTTP_CODE));
// Missing SSL certificate
if (str_contains(curl_error($ch), "unable to get local issuer certificate")) {
throw new AssertionError("You don't have a valid SSL Certificate installed or XAMPP.", 400);
}
// Handle errors such as "Bad credentials"
if ($obj && $obj->message) {
throw new AssertionError("Error: $obj->message \n<!-- $response -->", 401);
if ($message) {
throw new AssertionError("Error: $message \n<!-- $response -->", 401);
}
// Handle curl errors
if (curl_errno($ch)) {
throw new AssertionError("cURL error: " . curl_error($ch) . "\n<!-- $response -->", 500);
}
throw new AssertionError("An error occurred when getting a response from GitHub.\n<!-- $response -->", 502);
}
return $obj;
return $decoded;
}

/**
Expand All @@ -201,7 +242,13 @@ function getContributionYears(string $user): array
}
}
}";
$response = fetchGraphQL($query);
try {
$response = fetchGraphQL($query, getGitHubToken());
} catch (AssertionError $e) {
// retry once if an error occurred
error_log("An error occurred getting contribution years for $user: " . $e->getMessage());
$response = fetchGraphQL($query, getGitHubToken());
}
// User not found
if (!empty($response->errors)) {
$type = $response->errors[0]->type ?? "";
Expand Down