hub4j / github-api

Java API for GitHub
https://github-api.kohsuke.org/
MIT License
1.12k stars 715 forks source link

Accept 429 response codes as an indication of the secondary rate limit being exceeded #1895

Open holly-cummins opened 3 days ago

holly-cummins commented 3 days ago

Resolves #1842. Resolves #1805.

Description

If a user uses this library to drive the GitHub API hard, there will quite often be failures caused by secondary rate limits being breached (#1842 and #1805). There’s been some discussion of headers on #1842, and also of pluggable 'handle the rate limit' modules, but I think there’s a more fundamental problem with the handling of rate limits. (The good news is this means there's a simpler solution, too.)

The AbuseLimitHandler checks for a rate limit violation by looking for a 403 header and some headers:



@Override
boolean isError(@Nonnull GitHubConnectorResponse connectorResponse) {
    return isForbidden(connectorResponse) && hasRetryOrLimitHeader(connectorResponse);
}

HTTP_FORBIDDEN is a 403. The GitHub docs indicate that limits from GitHub are signalled with either 403 or TOO_MANY_REQUESTS responses (429). In my experience, I've only seen 429s.

What about headers?

The GitHub docs docs suggest that any headers are optional, and that in the absence of headers the code should just wait a minute (which, luckily, is what the current implementation already does):

If the retry-after response header is present, you should not retry your request until after that many seconds has elapsed. If the x-ratelimit-remaining header is 0, you should not retry your request until after the time, in UTC epoch seconds, specified by the x-ratelimit-reset header. Otherwise, wait for at least one minute before retrying. If your request continues to fail due to a secondary rate limit, wait for an exponentially increasing amount of time between retries, and throw an error after a specific number of retries.

Based on these docs, I think it's sufficient to count a 429 as a rate limit response, even if headers are missing. Semantically, a 429 is an indication of an abuse detection/rate limit scenario, and should be handled as such.

What about the rate limit handler?

GitHubRateLimitHandler also requires a 403 to 'count' a response as rate limited:

@Override
boolean isError(@NotNull GitHubConnectorResponse connectorResponse) throws IOException {
connectorResponse.statusCode() == HttpURLConnection.HTTP_FORBIDDEN
            && "0".equals(connectorResponse.header("X-RateLimit-Remaining"));

I didn't update this one, since the secondary rate limit checks are now correctly being handled on the abuse path with my changes. It might be worth updating it, or also even considering whether the distinction between the abuse and rate limit paths is so fuzzy that having both is just maintenance overhead. But that’s a different discussion.

Before submitting a PR:

When creating a PR:

codecov[bot] commented 23 hours ago

Codecov Report

Attention: Patch coverage is 25.00000% with 3 lines in your changes missing coverage. Please review.

Project coverage is 81.03%. Comparing base (cab9bbb) to head (7a6cc42).

Files Patch % Lines
...va/org/kohsuke/github/GitHubAbuseLimitHandler.java 33.33% 0 Missing and 2 partials :warning:
...ain/java/org/kohsuke/github/AbuseLimitHandler.java 0.00% 1 Missing :warning:
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #1895 +/- ## ============================================ - Coverage 81.05% 81.03% -0.03% - Complexity 2442 2443 +1 ============================================ Files 237 237 Lines 7342 7344 +2 Branches 398 399 +1 ============================================ Hits 5951 5951 Misses 1145 1145 - Partials 246 248 +2 ```

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.