Repository navigation
Expand file tree
/
Copy pathGitHubAbuseLimitHandler.java
More file actions
204 lines (184 loc) · 8.28 KB
/
Copy pathGitHubAbuseLimitHandler.java
File metadata and controls
204 lines (184 loc) · 8.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
package org.kohsuke.github;
import org.apache.commons.io.IOUtils;
import org.kohsuke.github.connector.GitHubConnectorResponse;
import org.kohsuke.github.internal.WaitTimeUtils;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
// TODO: Auto-generated Javadoc
/**
* Pluggable strategy to determine what to do when the API rate limit is reached.
*
* @author Kohsuke Kawaguchi
* @author Liam Newman
* @see GitHubBuilder#withAbuseLimitHandler(GitHubAbuseLimitHandler)
* @see GitHubRateLimitHandler
*/
public abstract class GitHubAbuseLimitHandler extends GitHubConnectorResponseErrorHandler {
/**
* Fail immediately.
*/
public static final GitHubAbuseLimitHandler FAIL = new GitHubAbuseLimitHandler() {
@Override
public void onError(GitHubConnectorResponse connectorResponse) throws IOException {
throw new HttpException("Abuse limit reached",
connectorResponse.statusCode(),
connectorResponse.header("Status"),
connectorResponse.request().url().toString())
.withResponseHeaderFields(connectorResponse.allHeaders());
}
};
/**
* Wait until the API abuse "wait time" is passed.
*/
public static final GitHubAbuseLimitHandler WAIT = new GitHubAbuseLimitHandler() {
@Override
public void onError(GitHubConnectorResponse connectorResponse) throws IOException {
try {
Thread.sleep(parseWaitTime(connectorResponse));
} catch (InterruptedException ex) {
throw (InterruptedIOException) new InterruptedIOException().initCause(ex);
}
}
};
/**
* On a wait, even if the response suggests a very short wait, wait for a minimum duration.
*/
private static final int MINIMUM_ABUSE_RETRY_MILLIS = 1000;
/**
* Pattern matching the marker GitHub returns in the response body when the request is rejected by a secondary rate
* limit, regardless of whether {@code Retry-After} or {@code gh-limited-by} headers are present. Used as a fallback
* detection path so that responses without those headers are still recognized.
*
* @see <a href=
* "https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#about-secondary-rate-limits">About
* secondary rate limits</a>
*/
private static final Pattern SECONDARY_RATE_LIMIT_BODY_PATTERN = Pattern.compile("\\bsecondary rate\\b",
Pattern.CASE_INSENSITIVE);
// If "Retry-After" missing, wait for unambiguously over one minute per GitHub guidance
static long DEFAULT_WAIT_MILLIS = Duration.ofSeconds(61).toMillis();
/*
* Exposed for testability. Given an http response, find the retry-after header field and parse it as either a
* number or a date (the spec allows both). If no header is found, wait for a reasonably amount of time.
*
* The implementation lives in WaitTimeUtils, shared with GitHubRateLimitHandler.
*/
static long parseWaitTime(GitHubConnectorResponse connectorResponse) {
return WaitTimeUtils
.parseRetryAfterWaitTime(connectorResponse, DEFAULT_WAIT_MILLIS, MINIMUM_ABUSE_RETRY_MILLIS);
}
/**
* Create default GitHubAbuseLimitHandler instance
*/
public GitHubAbuseLimitHandler() {
}
/**
* Called when the library encounters HTTP error indicating that the API abuse limit is reached.
*
* <p>
* Any exception thrown from this method will cause the request to fail, and the caller of github-api will receive
* an exception. If this method returns normally, another request will be attempted. For that to make sense, the
* implementation needs to wait for some time.
*
* @param connectorResponse
* Response information for this request.
* @throws IOException
* on failure
* @see <a href="https://developer.github.com/v3/#abuse-rate-limits">API documentation from GitHub</a>
* @see <a href=
* "https://developer.github.com/v3/guides/best-practices-for-integrators/#dealing-with-abuse-rate-limits">Dealing
* with abuse rate limits</a>
*
*/
public abstract void onError(@Nonnull GitHubConnectorResponse connectorResponse) throws IOException;
/**
* Checks if the response contains a specific header.
*
* @param connectorResponse
* the response from the GitHub connector
* @param headerName
* the name of the header to check for
* @return true if the specified header is present
*/
private boolean hasHeader(GitHubConnectorResponse connectorResponse, String headerName) {
return connectorResponse.header(headerName) != null;
}
/**
* Checks if the response contains either "Retry-After" or "gh-limited-by" headers. GitHub does not guarantee the
* presence of the Retry-After header. However, the gh-limited-by header is included in the response when the error
* is due to rate limiting
*
* @param connectorResponse
* the response from the GitHub connector
* @return true if either "Retry-After" or "gh-limited-by" headers are present
* @see <a href=
* "https://docs.github.com/en/rest/using-the-rest-api/best-practices-for-using-the-rest-api?apiVersion=2022-11-28#handle-rate-limit-errors-appropriately">GitHub
* API Rate Limiting Documentation</a>
*/
private boolean hasRetryOrLimitHeader(GitHubConnectorResponse connectorResponse) {
return hasHeader(connectorResponse, "Retry-After") || hasHeader(connectorResponse, "gh-limited-by");
}
/**
* Checks if the response body contains the secondary rate limit marker. GitHub does not always return
* {@code Retry-After} or {@code gh-limited-by} headers when a secondary rate limit is hit; in those cases the body
* still contains a "secondary rate" marker that can be matched as a fallback detection path.
*
* @param connectorResponse
* the response from the GitHub connector
* @return true if the response body contains the secondary rate limit marker
* @throws IOException
* if reading the response body fails
* @see <a href="https://github.com/hub4j/github-api/issues/2009">hub4j/github-api#2009</a>
*/
private boolean hasSecondaryRateLimitBodyMarker(GitHubConnectorResponse connectorResponse) throws IOException {
try (InputStream bodyStream = connectorResponse.bodyStream()) {
String body = IOUtils.toString(bodyStream, StandardCharsets.UTF_8);
return SECONDARY_RATE_LIMIT_BODY_PATTERN.matcher(body).find();
}
}
/**
* Checks if the response status code is HTTP_FORBIDDEN (403).
*
* @param connectorResponse
* the response from the GitHub connector
* @return true if the status code is HTTP_FORBIDDEN
*/
private boolean isForbidden(GitHubConnectorResponse connectorResponse) {
return connectorResponse.statusCode() == HTTP_FORBIDDEN;
}
/**
* Checks if the response status code is TOO_MANY_REQUESTS (429).
*
* @param connectorResponse
* the response from the GitHub connector
* @return true if the status code is TOO_MANY_REQUESTS
*/
private boolean isTooManyRequests(GitHubConnectorResponse connectorResponse) {
return connectorResponse.statusCode() == TOO_MANY_REQUESTS;
}
/**
* Checks if is error.
*
* @param connectorResponse
* the connector response
* @return true, if is error
* @throws IOException
* Signals that an I/O exception has occurred.
*/
@Override
boolean isError(@Nonnull GitHubConnectorResponse connectorResponse) throws IOException {
if (isTooManyRequests(connectorResponse)) {
return true;
}
if (!isForbidden(connectorResponse)) {
return false;
}
return hasRetryOrLimitHeader(connectorResponse) || hasSecondaryRateLimitBodyMarker(connectorResponse);
}
}