diff --git a/src/main/java/com/auth0/exception/RateLimitException.java b/src/main/java/com/auth0/exception/RateLimitException.java new file mode 100644 index 00000000..c5e79e81 --- /dev/null +++ b/src/main/java/com/auth0/exception/RateLimitException.java @@ -0,0 +1,50 @@ +package com.auth0.exception; + +/** + * Represents a server error when a rate limit has been exceeded. + *

+ * Getters for {@code limit, remaining} and {@code reset} corresponds to {@code X-RateLimit-Limit, X-RateLimit-Remaining} and {@code X-RateLimit-Reset} HTTP headers. + * If the value of any headers is missing, then a default value -1 will assigned. + *

+ * To learn more about rate limits, visit https://auth0.com/docs/policies/rate-limits + */ +public class RateLimitException extends APIException { + + private final long limit; + private final long remaining; + private final long reset; + + private static final int STATUS_CODE_TOO_MANY_REQUEST = 429; + + public RateLimitException(long limit, long remaining, long reset) { + super("Rate limit reached", STATUS_CODE_TOO_MANY_REQUEST, null); + this.limit = limit; + this.remaining = remaining; + this.reset = reset; + } + + /** + * Getter for the maximum number of requests available in the current time frame. + * @return The maximum number of requests or -1 if missing. + */ + public long getLimit() { + return limit; + } + + /** + * Getter for the number of remaining requests in the current time frame. + * @return Number of remaining requests or -1 if missing. + */ + public long getRemaining() { + return remaining; + } + + /** + * Getter for the UNIX timestamp of the expected time when the rate limit will reset. + * @return The UNIX timestamp or -1 if missing. + */ + public long getReset() { + return reset; + } + +} diff --git a/src/main/java/com/auth0/net/CustomRequest.java b/src/main/java/com/auth0/net/CustomRequest.java index 431e80f9..b67e84f0 100644 --- a/src/main/java/com/auth0/net/CustomRequest.java +++ b/src/main/java/com/auth0/net/CustomRequest.java @@ -2,6 +2,7 @@ import com.auth0.exception.APIException; import com.auth0.exception.Auth0Exception; +import com.auth0.exception.RateLimitException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -15,6 +16,7 @@ @SuppressWarnings("WeakerAccess") public class CustomRequest extends BaseRequest implements CustomizableRequest { + private static final String CONTENT_TYPE_APPLICATION_JSON = "application/json"; private final String url; @@ -25,6 +27,8 @@ public class CustomRequest extends BaseRequest implements CustomizableRequ private final Map parameters; private Object body; + private static final int STATUS_CODE_TOO_MANY_REQUEST = 429; + CustomRequest(OkHttpClient client, String url, String method, ObjectMapper mapper, TypeReference tType) { super(client); this.url = url; @@ -96,6 +100,10 @@ protected RequestBody createBody() throws Auth0Exception { } protected Auth0Exception createResponseException(Response response) { + if (response.code() == STATUS_CODE_TOO_MANY_REQUEST) { + return createRateLimitException(response); + } + String payload = null; try (ResponseBody body = response.body()) { payload = body.string(); @@ -106,4 +114,12 @@ protected Auth0Exception createResponseException(Response response) { return new APIException(payload, response.code(), e); } } + + private RateLimitException createRateLimitException(Response response) { + // -1 as default value if the header could not be found. + long limit = Long.parseLong(response.header("X-RateLimit-Limit", "-1")); + long remaining = Long.parseLong(response.header("X-RateLimit-Remaining", "-1")); + long reset = Long.parseLong(response.header("X-RateLimit-Reset", "-1")); + return new RateLimitException(limit, remaining, reset); + } } diff --git a/src/test/java/com/auth0/client/MockServer.java b/src/test/java/com/auth0/client/MockServer.java index 88bf1df9..4837b197 100644 --- a/src/test/java/com/auth0/client/MockServer.java +++ b/src/test/java/com/auth0/client/MockServer.java @@ -76,7 +76,6 @@ public class MockServer { public static final String MGMT_EMPTY_LIST = "src/test/resources/mgmt/empty_list.json"; public static final String MGMT_JOB_POST_VERIFICATION_EMAIL = "src/test/resources/mgmt/post_verification_email.json"; - private final MockWebServer server; public MockServer() throws Exception { @@ -108,6 +107,20 @@ public void jsonResponse(String path, int statusCode) throws IOException { server.enqueue(response); } + public void rateLimitReachedResponse(long limit, long remaining, long reset) throws IOException { + MockResponse response = new MockResponse().setResponseCode(429); + if (limit != -1) { + response.addHeader("X-RateLimit-Limit", String.valueOf(limit)); + } + if (remaining != -1) { + response.addHeader("X-RateLimit-Remaining", String.valueOf(remaining)); + } + if (reset != -1) { + response.addHeader("X-RateLimit-Reset", String.valueOf(reset)); + } + server.enqueue(response); + } + public void textResponse(String path, int statusCode) throws IOException { MockResponse response = new MockResponse() .setResponseCode(statusCode) diff --git a/src/test/java/com/auth0/net/CustomRequestTest.java b/src/test/java/com/auth0/net/CustomRequestTest.java index f52ef446..906aab85 100644 --- a/src/test/java/com/auth0/net/CustomRequestTest.java +++ b/src/test/java/com/auth0/net/CustomRequestTest.java @@ -21,6 +21,7 @@ import java.util.Map; import static com.auth0.client.MockServer.*; +import com.auth0.exception.RateLimitException; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import static org.mockito.ArgumentMatchers.any; @@ -301,5 +302,55 @@ public void shouldParsePlainTextErrorResponse() throws Exception { assertThat(authException.getValue("non_existing_key"), is(nullValue())); assertThat(authException.getStatusCode(), is(400)); } + + @Test + public void shouldParseRateLimitsHeaders() throws Exception { + CustomRequest request = new CustomRequest<>(client, server.getBaseUrl(), "GET", listType); + server.rateLimitReachedResponse(100, 10, 5); + Exception exception = null; + try { + request.execute(); + server.takeRequest(); + } catch (Exception e) { + exception = e; + } + assertThat(exception, is(notNullValue())); + assertThat(exception, is(instanceOf(RateLimitException.class))); + assertThat(exception.getCause(), is(nullValue())); + assertThat(exception.getMessage(), is("Request failed with status code 429: Rate limit reached")); + RateLimitException rateLimitException = (RateLimitException) exception; + assertThat(rateLimitException.getDescription(), is("Rate limit reached")); + assertThat(rateLimitException.getError(), is(nullValue())); + assertThat(rateLimitException.getValue("non_existing_key"), is(nullValue())); + assertThat(rateLimitException.getStatusCode(), is(429)); + assertThat(rateLimitException.getLimit(), is(100L)); + assertThat(rateLimitException.getRemaining(), is(10L)); + assertThat(rateLimitException.getReset(), is(5L)); + } + + @Test + public void shouldDefaultRateLimitsHeadersWhenMissing() throws Exception { + CustomRequest request = new CustomRequest<>(client, server.getBaseUrl(), "GET", listType); + server.rateLimitReachedResponse(-1, -1, -1); + Exception exception = null; + try { + request.execute(); + server.takeRequest(); + } catch (Exception e) { + exception = e; + } + assertThat(exception, is(notNullValue())); + assertThat(exception, is(instanceOf(RateLimitException.class))); + assertThat(exception.getCause(), is(nullValue())); + assertThat(exception.getMessage(), is("Request failed with status code 429: Rate limit reached")); + RateLimitException rateLimitException = (RateLimitException) exception; + assertThat(rateLimitException.getDescription(), is("Rate limit reached")); + assertThat(rateLimitException.getError(), is(nullValue())); + assertThat(rateLimitException.getValue("non_existing_key"), is(nullValue())); + assertThat(rateLimitException.getStatusCode(), is(429)); + assertThat(rateLimitException.getLimit(), is(-1L)); + assertThat(rateLimitException.getRemaining(), is(-1L)); + assertThat(rateLimitException.getReset(), is(-1L)); + } -} \ No newline at end of file +}