Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 9d03435

Browse filesBrowse files
committed
Expose Rate Limit Headers
Exposes the rate limit header responses so that consumers of the API can proactively tune their usage
1 parent 470da06 commit 9d03435
Copy full SHA for 9d03435

File tree

Expand file treeCollapse file tree

2 files changed

+96
-7
lines changed
Filter options
Expand file treeCollapse file tree

2 files changed

+96
-7
lines changed

‎src/main/java/org/kohsuke/github/GitHub.java

Copy file name to clipboardExpand all lines: src/main/java/org/kohsuke/github/GitHub.java
+27Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050
import java.util.Set;
5151
import java.util.TimeZone;
5252

53+
import java.util.logging.Level;
54+
import javax.annotation.CheckForNull;
5355
import org.apache.commons.codec.Charsets;
5456
import org.apache.commons.codec.binary.Base64;
5557

@@ -90,6 +92,9 @@ public class GitHub {
9092

9193
private HttpConnector connector = HttpConnector.DEFAULT;
9294

95+
private final Object headerRateLimitLock = new Object();
96+
private GHRateLimit headerRateLimit = null;
97+
9398
/**
9499
* Creates a client API root object.
95100
*
@@ -300,6 +305,28 @@ public GHRateLimit getRateLimit() throws IOException {
300305
}
301306
}
302307

308+
/*package*/ void updateRateLimit(@Nonnull GHRateLimit observed) {
309+
synchronized (headerRateLimitLock) {
310+
if (headerRateLimit == null
311+
|| headerRateLimit.getResetDate().getTime() < observed.getResetDate().getTime()
312+
|| headerRateLimit.remaining > observed.remaining) {
313+
headerRateLimit = observed;
314+
LOGGER.log(Level.INFO, "Rate limit now: {0}", headerRateLimit);
315+
}
316+
}
317+
}
318+
319+
/**
320+
* Returns the most recently observed rate limit data or {@code null} if either there is no rate limit
321+
* (for example GitHub Enterprise) or if no requests have been made.
322+
*
323+
* @return the most recentlt observed rate limit data or {@code null}.
324+
*/
325+
@CheckForNull
326+
public GHRateLimit lastRateLimit() {
327+
return headerRateLimit;
328+
}
329+
303330
/**
304331
* Gets the {@link GHUser} that represents yourself.
305332
*/

‎src/main/java/org/kohsuke/github/Requester.java

Copy file name to clipboardExpand all lines: src/main/java/org/kohsuke/github/Requester.java
+69-7Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@
2525

2626
import com.fasterxml.jackson.databind.JsonMappingException;
2727
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
28-
import org.apache.commons.io.IOUtils;
29-
3028
import java.io.FileNotFoundException;
3129
import java.io.IOException;
3230
import java.io.InputStream;
@@ -42,23 +40,26 @@
4240
import java.net.URLEncoder;
4341
import java.util.ArrayList;
4442
import java.util.Collection;
43+
import java.util.Date;
4544
import java.util.HashMap;
4645
import java.util.Iterator;
4746
import java.util.LinkedHashMap;
4847
import java.util.List;
4948
import java.util.Locale;
5049
import java.util.Map;
5150
import java.util.NoSuchElementException;
51+
import java.util.logging.Level;
5252
import java.util.logging.Logger;
5353
import java.util.regex.Matcher;
5454
import java.util.regex.Pattern;
5555
import java.util.zip.GZIPInputStream;
56-
5756
import javax.annotation.WillClose;
57+
import org.apache.commons.io.IOUtils;
58+
import org.apache.commons.lang.StringUtils;
5859

5960
import static java.util.Arrays.asList;
6061
import static java.util.logging.Level.FINE;
61-
import static org.kohsuke.github.GitHub.*;
62+
import static org.kohsuke.github.GitHub.MAPPER;
6263

6364
/**
6465
* A builder pattern for making HTTP call and parsing its output.
@@ -281,6 +282,8 @@ private <T> T _to(String tailApiUrl, Class<T> type, T instance) throws IOExcepti
281282
return result;
282283
} catch (IOException e) {
283284
handleApiError(e);
285+
} finally {
286+
noteRateLimit(tailApiUrl);
284287
}
285288
}
286289
}
@@ -299,6 +302,8 @@ public int asHttpStatusCode(String tailApiUrl) throws IOException {
299302
return uc.getResponseCode();
300303
} catch (IOException e) {
301304
handleApiError(e);
305+
} finally {
306+
noteRateLimit(tailApiUrl);
302307
}
303308
}
304309
}
@@ -313,6 +318,59 @@ public InputStream asStream(String tailApiUrl) throws IOException {
313318
return wrapStream(uc.getInputStream());
314319
} catch (IOException e) {
315320
handleApiError(e);
321+
} finally {
322+
noteRateLimit(tailApiUrl);
323+
}
324+
}
325+
}
326+
327+
private void noteRateLimit(String tailApiUrl) {
328+
if ("/rate_limit".equals(tailApiUrl)) {
329+
// the rate_limit API is "free"
330+
return;
331+
}
332+
if (tailApiUrl.startsWith("/search")) {
333+
// the search API uses a different rate limit
334+
return;
335+
}
336+
String limit = uc.getHeaderField("X-RateLimit-Limit");
337+
if (StringUtils.isBlank(limit)) {
338+
// if we are missing a header, return fast
339+
return;
340+
}
341+
String remaining = uc.getHeaderField("X-RateLimit-Remaining");
342+
if (StringUtils.isBlank(remaining)) {
343+
// if we are missing a header, return fast
344+
return;
345+
}
346+
String reset = uc.getHeaderField("X-RateLimit-Reset");
347+
if (StringUtils.isBlank(reset)) {
348+
// if we are missing a header, return fast
349+
return;
350+
}
351+
GHRateLimit observed = new GHRateLimit();
352+
try {
353+
observed.limit = Integer.parseInt(limit);
354+
} catch (NumberFormatException e) {
355+
if (LOGGER.isLoggable(Level.FINEST)) {
356+
LOGGER.log(Level.FINEST, "Malformed X-RateLimit-Limit header value " + limit, e);
357+
}
358+
return;
359+
}
360+
try {
361+
observed.remaining = Integer.parseInt(remaining);
362+
} catch (NumberFormatException e) {
363+
if (LOGGER.isLoggable(Level.FINEST)) {
364+
LOGGER.log(Level.FINEST, "Malformed X-RateLimit-Remaining header value " + remaining, e);
365+
}
366+
return;
367+
}
368+
try {
369+
observed.reset = new Date(Long.parseLong(reset)); // this is madness, storing the date as seconds
370+
root.updateRateLimit(observed);
371+
} catch (NumberFormatException e) {
372+
if (LOGGER.isLoggable(Level.FINEST)) {
373+
LOGGER.log(Level.FINEST, "Malformed X-RateLimit-Reset header value " + reset, e);
316374
}
317375
}
318376
}
@@ -382,7 +440,7 @@ private boolean isMethodWithBody() {
382440
}
383441

384442
try {
385-
return new PagingIterator<T>(type, root.getApiURL(s.toString()));
443+
return new PagingIterator<T>(type, tailApiUrl, root.getApiURL(s.toString()));
386444
} catch (IOException e) {
387445
throw new Error(e);
388446
}
@@ -391,6 +449,7 @@ private boolean isMethodWithBody() {
391449
class PagingIterator<T> implements Iterator<T> {
392450

393451
private final Class<T> type;
452+
private final String tailApiUrl;
394453

395454
/**
396455
* The next batch to be returned from {@link #next()}.
@@ -402,9 +461,10 @@ class PagingIterator<T> implements Iterator<T> {
402461
*/
403462
private URL url;
404463

405-
PagingIterator(Class<T> type, URL url) {
406-
this.url = url;
464+
PagingIterator(Class<T> type, String tailApiUrl, URL url) {
407465
this.type = type;
466+
this.tailApiUrl = tailApiUrl;
467+
this.url = url;
408468
}
409469

410470
public boolean hasNext() {
@@ -438,6 +498,8 @@ private void fetch() {
438498
return;
439499
} catch (IOException e) {
440500
handleApiError(e);
501+
} finally {
502+
noteRateLimit(tailApiUrl);
441503
}
442504
}
443505
} catch (IOException e) {

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.