Skip to content

Commit 66d189e

Browse files
authored
Merge pull request #153 from rvillablanca/rate-limits
support to retrieve rate limits values from response headers #99
2 parents 1bbdbcf + 6cc592a commit 66d189e

4 files changed

Lines changed: 132 additions & 2 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.auth0.exception;
2+
3+
/**
4+
* Represents a server error when a rate limit has been exceeded.
5+
* <p>
6+
* Getters for {@code limit, remaining} and {@code reset} corresponds to {@code X-RateLimit-Limit, X-RateLimit-Remaining} and {@code X-RateLimit-Reset} HTTP headers.
7+
* If the value of any headers is missing, then a default value -1 will assigned.
8+
* <p>
9+
* To learn more about rate limits, visit <a href="https://auth0.com/docs/policies/rate-limits">https://auth0.com/docs/policies/rate-limits</a>
10+
*/
11+
public class RateLimitException extends APIException {
12+
13+
private final long limit;
14+
private final long remaining;
15+
private final long reset;
16+
17+
private static final int STATUS_CODE_TOO_MANY_REQUEST = 429;
18+
19+
public RateLimitException(long limit, long remaining, long reset) {
20+
super("Rate limit reached", STATUS_CODE_TOO_MANY_REQUEST, null);
21+
this.limit = limit;
22+
this.remaining = remaining;
23+
this.reset = reset;
24+
}
25+
26+
/**
27+
* Getter for the maximum number of requests available in the current time frame.
28+
* @return The maximum number of requests or -1 if missing.
29+
*/
30+
public long getLimit() {
31+
return limit;
32+
}
33+
34+
/**
35+
* Getter for the number of remaining requests in the current time frame.
36+
* @return Number of remaining requests or -1 if missing.
37+
*/
38+
public long getRemaining() {
39+
return remaining;
40+
}
41+
42+
/**
43+
* Getter for the UNIX timestamp of the expected time when the rate limit will reset.
44+
* @return The UNIX timestamp or -1 if missing.
45+
*/
46+
public long getReset() {
47+
return reset;
48+
}
49+
50+
}

src/main/java/com/auth0/net/CustomRequest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.auth0.exception.APIException;
44
import com.auth0.exception.Auth0Exception;
5+
import com.auth0.exception.RateLimitException;
56
import com.fasterxml.jackson.core.JsonProcessingException;
67
import com.fasterxml.jackson.core.type.TypeReference;
78
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -15,6 +16,7 @@
1516

1617
@SuppressWarnings("WeakerAccess")
1718
public class CustomRequest<T> extends BaseRequest<T> implements CustomizableRequest<T> {
19+
1820
private static final String CONTENT_TYPE_APPLICATION_JSON = "application/json";
1921

2022
private final String url;
@@ -25,6 +27,8 @@ public class CustomRequest<T> extends BaseRequest<T> implements CustomizableRequ
2527
private final Map<String, Object> parameters;
2628
private Object body;
2729

30+
private static final int STATUS_CODE_TOO_MANY_REQUEST = 429;
31+
2832
CustomRequest(OkHttpClient client, String url, String method, ObjectMapper mapper, TypeReference<T> tType) {
2933
super(client);
3034
this.url = url;
@@ -96,6 +100,10 @@ protected RequestBody createBody() throws Auth0Exception {
96100
}
97101

98102
protected Auth0Exception createResponseException(Response response) {
103+
if (response.code() == STATUS_CODE_TOO_MANY_REQUEST) {
104+
return createRateLimitException(response);
105+
}
106+
99107
String payload = null;
100108
try (ResponseBody body = response.body()) {
101109
payload = body.string();
@@ -106,4 +114,12 @@ protected Auth0Exception createResponseException(Response response) {
106114
return new APIException(payload, response.code(), e);
107115
}
108116
}
117+
118+
private RateLimitException createRateLimitException(Response response) {
119+
// -1 as default value if the header could not be found.
120+
long limit = Long.parseLong(response.header("X-RateLimit-Limit", "-1"));
121+
long remaining = Long.parseLong(response.header("X-RateLimit-Remaining", "-1"));
122+
long reset = Long.parseLong(response.header("X-RateLimit-Reset", "-1"));
123+
return new RateLimitException(limit, remaining, reset);
124+
}
109125
}

src/test/java/com/auth0/client/MockServer.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ public class MockServer {
7676
public static final String MGMT_EMPTY_LIST = "src/test/resources/mgmt/empty_list.json";
7777
public static final String MGMT_JOB_POST_VERIFICATION_EMAIL = "src/test/resources/mgmt/post_verification_email.json";
7878

79-
8079
private final MockWebServer server;
8180

8281
public MockServer() throws Exception {
@@ -108,6 +107,20 @@ public void jsonResponse(String path, int statusCode) throws IOException {
108107
server.enqueue(response);
109108
}
110109

110+
public void rateLimitReachedResponse(long limit, long remaining, long reset) throws IOException {
111+
MockResponse response = new MockResponse().setResponseCode(429);
112+
if (limit != -1) {
113+
response.addHeader("X-RateLimit-Limit", String.valueOf(limit));
114+
}
115+
if (remaining != -1) {
116+
response.addHeader("X-RateLimit-Remaining", String.valueOf(remaining));
117+
}
118+
if (reset != -1) {
119+
response.addHeader("X-RateLimit-Reset", String.valueOf(reset));
120+
}
121+
server.enqueue(response);
122+
}
123+
111124
public void textResponse(String path, int statusCode) throws IOException {
112125
MockResponse response = new MockResponse()
113126
.setResponseCode(statusCode)

src/test/java/com/auth0/net/CustomRequestTest.java

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.Map;
2222

2323
import static com.auth0.client.MockServer.*;
24+
import com.auth0.exception.RateLimitException;
2425
import static org.hamcrest.MatcherAssert.assertThat;
2526
import static org.hamcrest.Matchers.*;
2627
import static org.mockito.ArgumentMatchers.any;
@@ -301,5 +302,55 @@ public void shouldParsePlainTextErrorResponse() throws Exception {
301302
assertThat(authException.getValue("non_existing_key"), is(nullValue()));
302303
assertThat(authException.getStatusCode(), is(400));
303304
}
305+
306+
@Test
307+
public void shouldParseRateLimitsHeaders() throws Exception {
308+
CustomRequest<List> request = new CustomRequest<>(client, server.getBaseUrl(), "GET", listType);
309+
server.rateLimitReachedResponse(100, 10, 5);
310+
Exception exception = null;
311+
try {
312+
request.execute();
313+
server.takeRequest();
314+
} catch (Exception e) {
315+
exception = e;
316+
}
317+
assertThat(exception, is(notNullValue()));
318+
assertThat(exception, is(instanceOf(RateLimitException.class)));
319+
assertThat(exception.getCause(), is(nullValue()));
320+
assertThat(exception.getMessage(), is("Request failed with status code 429: Rate limit reached"));
321+
RateLimitException rateLimitException = (RateLimitException) exception;
322+
assertThat(rateLimitException.getDescription(), is("Rate limit reached"));
323+
assertThat(rateLimitException.getError(), is(nullValue()));
324+
assertThat(rateLimitException.getValue("non_existing_key"), is(nullValue()));
325+
assertThat(rateLimitException.getStatusCode(), is(429));
326+
assertThat(rateLimitException.getLimit(), is(100L));
327+
assertThat(rateLimitException.getRemaining(), is(10L));
328+
assertThat(rateLimitException.getReset(), is(5L));
329+
}
330+
331+
@Test
332+
public void shouldDefaultRateLimitsHeadersWhenMissing() throws Exception {
333+
CustomRequest<List> request = new CustomRequest<>(client, server.getBaseUrl(), "GET", listType);
334+
server.rateLimitReachedResponse(-1, -1, -1);
335+
Exception exception = null;
336+
try {
337+
request.execute();
338+
server.takeRequest();
339+
} catch (Exception e) {
340+
exception = e;
341+
}
342+
assertThat(exception, is(notNullValue()));
343+
assertThat(exception, is(instanceOf(RateLimitException.class)));
344+
assertThat(exception.getCause(), is(nullValue()));
345+
assertThat(exception.getMessage(), is("Request failed with status code 429: Rate limit reached"));
346+
RateLimitException rateLimitException = (RateLimitException) exception;
347+
assertThat(rateLimitException.getDescription(), is("Rate limit reached"));
348+
assertThat(rateLimitException.getError(), is(nullValue()));
349+
assertThat(rateLimitException.getValue("non_existing_key"), is(nullValue()));
350+
assertThat(rateLimitException.getStatusCode(), is(429));
351+
assertThat(rateLimitException.getLimit(), is(-1L));
352+
assertThat(rateLimitException.getRemaining(), is(-1L));
353+
assertThat(rateLimitException.getReset(), is(-1L));
354+
}
304355

305-
}
356+
}

0 commit comments

Comments
 (0)