Skip to content

Commit 9d03435

Browse 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

2 files changed

Lines changed: 96 additions & 7 deletions

File tree

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

Lines 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

Lines 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)