From fccd8a2872b925c73f02495c71488e61ce0096c1 Mon Sep 17 00:00:00 2001 From: Jim Anderson <jimranderson@gmail.com> Date: Thu, 2 Nov 2023 14:50:13 -0500 Subject: [PATCH 1/4] propagate error message on rate limit exception --- .../java/com/auth0/client/auth/AuthAPI.java | 2 +- .../auth0/exception/RateLimitException.java | 9 +++++++++ src/main/java/com/auth0/net/BaseRequest.java | 10 +++++++++- .../com/auth0/net/RateLimitInterceptor.java | 18 +++++++++--------- .../auth0/net/client/DefaultHttpClient.java | 4 +--- src/test/java/com/auth0/client/MockServer.java | 12 +++++++++++- .../java/com/auth0/net/BaseRequestTest.java | 12 ++++++------ .../com/auth0/net/MultipartRequestTest.java | 4 ++-- .../auth0/net/RateLimitInterceptorTest.java | 2 -- src/test/resources/mgmt/rate_limit_error.json | 6 ++++++ 10 files changed, 54 insertions(+), 25 deletions(-) create mode 100644 src/test/resources/mgmt/rate_limit_error.json diff --git a/src/main/java/com/auth0/client/auth/AuthAPI.java b/src/main/java/com/auth0/client/auth/AuthAPI.java index 9a94076e..5b35b3c8 100644 --- a/src/main/java/com/auth0/client/auth/AuthAPI.java +++ b/src/main/java/com/auth0/client/auth/AuthAPI.java @@ -1359,7 +1359,7 @@ public Builder withHttpClient(Auth0HttpClient httpClient) { */ public AuthAPI build() { return new AuthAPI(domain, clientId, clientSecret, clientAssertionSigner, - Objects.nonNull(httpClient) ? httpClient : DefaultHttpClient.newBuilder().build()); + Objects.nonNull(httpClient) ? httpClient : DefaultHttpClient.newBuilder().withMaxRetries(0).build()); } } } diff --git a/src/main/java/com/auth0/exception/RateLimitException.java b/src/main/java/com/auth0/exception/RateLimitException.java index c5e79e81..738a941f 100644 --- a/src/main/java/com/auth0/exception/RateLimitException.java +++ b/src/main/java/com/auth0/exception/RateLimitException.java @@ -1,5 +1,7 @@ package com.auth0.exception; +import java.util.Map; + /** * Represents a server error when a rate limit has been exceeded. * <p> @@ -16,6 +18,13 @@ public class RateLimitException extends APIException { private static final int STATUS_CODE_TOO_MANY_REQUEST = 429; + public RateLimitException(long limit, long remaining, long reset, Map<String, Object> values) { + super(values, STATUS_CODE_TOO_MANY_REQUEST); + this.limit = limit; + this.remaining = remaining; + this.reset = reset; + } + public RateLimitException(long limit, long remaining, long reset) { super("Rate limit reached", STATUS_CODE_TOO_MANY_REQUEST, null); this.limit = limit; diff --git a/src/main/java/com/auth0/net/BaseRequest.java b/src/main/java/com/auth0/net/BaseRequest.java index e124ea12..44f5e1b4 100644 --- a/src/main/java/com/auth0/net/BaseRequest.java +++ b/src/main/java/com/auth0/net/BaseRequest.java @@ -218,7 +218,15 @@ private RateLimitException createRateLimitException(Auth0HttpResponse response) long limit = Long.parseLong(response.getHeader("x-ratelimit-limit", "-1")); long remaining = Long.parseLong(response.getHeader("x-ratelimit-remaining", "-1")); long reset = Long.parseLong(response.getHeader("x-ratelimit-reset", "-1")); - return new RateLimitException(limit, remaining, reset); + + String payload = response.getBody(); + MapType mapType = mapper.getTypeFactory().constructMapType(HashMap.class, String.class, Object.class); + try { + Map<String, Object> values = mapper.readValue(payload, mapType); + return new RateLimitException(limit, remaining, reset, values); + } catch (IOException e) { + return new RateLimitException(limit, remaining, reset); + } } /** diff --git a/src/main/java/com/auth0/net/RateLimitInterceptor.java b/src/main/java/com/auth0/net/RateLimitInterceptor.java index 24dd69ad..842f143a 100644 --- a/src/main/java/com/auth0/net/RateLimitInterceptor.java +++ b/src/main/java/com/auth0/net/RateLimitInterceptor.java @@ -67,13 +67,7 @@ public Response intercept(@NotNull Chain chain) throws IOException { .withMaxRetries(maxRetries) .withBackoff(INITIAL_INTERVAL, MAX_INTERVAL, ChronoUnit.MILLIS) .withJitter(JITTER) - .handleResultIf(response -> { - if (response.code() == 429) { - response.close(); - return true; - } - return false; - }); + .handleResultIf(response -> response.code() == 429); // For testing purposes only, allow test to hook into retry listener to enable verification of retry backoff if (retryListener != null) { @@ -81,10 +75,16 @@ public Response intercept(@NotNull Chain chain) throws IOException { } try { + return Failsafe.with(retryPolicy).get((context) -> { + // ensure response of last recorded response prior to retry is closed + if (context.getLastResult() != null) { + context.getLastResult().close();; + } + return chain.proceed(chain.request()); + }); + } catch (FailsafeException fe) { // throw Auth0Exception instead of FailSafe exception on error // see https://github.com/auth0/auth0-java/issues/483 - return Failsafe.with(retryPolicy).get(() -> chain.proceed(chain.request())); - } catch (FailsafeException fe) { throw new Auth0Exception("Failed to execute request", fe.getCause()); } } diff --git a/src/main/java/com/auth0/net/client/DefaultHttpClient.java b/src/main/java/com/auth0/net/client/DefaultHttpClient.java index 2785a67c..3a4e73eb 100644 --- a/src/main/java/com/auth0/net/client/DefaultHttpClient.java +++ b/src/main/java/com/auth0/net/client/DefaultHttpClient.java @@ -138,9 +138,7 @@ private Auth0HttpResponse buildResponse(Response okResponse) throws IOException ResponseBody responseBody = okResponse.body(); String content = null; - // The RateLimitInterceptor needs to close the response; we don't need the body in that case and trying to - // get the body will result in an exception because the responsebody has been closed - if (Objects.nonNull(responseBody) && okResponse.code() != 429) { + if (Objects.nonNull(responseBody)) { content = responseBody.string(); } return Auth0HttpResponse.newBuilder() diff --git a/src/test/java/com/auth0/client/MockServer.java b/src/test/java/com/auth0/client/MockServer.java index a9d5caa5..26a1bcac 100644 --- a/src/test/java/com/auth0/client/MockServer.java +++ b/src/test/java/com/auth0/client/MockServer.java @@ -144,6 +144,7 @@ public class MockServer { public static final String KEY_LIST = "src/test/resources/mgmt/key_list.json"; public static final String KEY_REVOKE = "src/test/resources/mgmt/key_revoke.json"; public static final String KEY_ROTATE = "src/test/resources/mgmt/key_rotate.json"; + public static final String RATE_LIMIT_ERROR = "src/test/resources/mgmt/rate_limit_error.json"; private final MockWebServer server; @@ -184,7 +185,11 @@ public void noContentResponse() { server.enqueue(response); } - public void rateLimitReachedResponse(long limit, long remaining, long reset) { + public void rateLimitReachedResponse(long limit, long remaining, long reset) throws IOException { + rateLimitReachedResponse(limit, remaining, reset, null); + } + + public void rateLimitReachedResponse(long limit, long remaining, long reset, String path) throws IOException { MockResponse response = new MockResponse().setResponseCode(429); if (limit != -1) { response.addHeader("x-ratelimit-limit", String.valueOf(limit)); @@ -195,6 +200,11 @@ public void rateLimitReachedResponse(long limit, long remaining, long reset) { if (reset != -1) { response.addHeader("x-ratelimit-reset", String.valueOf(reset)); } + if (path != null) { + response + .addHeader("Content-Type", "application/json") + .setBody(readTextFile(path)); + } server.enqueue(response); } diff --git a/src/test/java/com/auth0/net/BaseRequestTest.java b/src/test/java/com/auth0/net/BaseRequestTest.java index b66f21c0..5fe9f002 100644 --- a/src/test/java/com/auth0/net/BaseRequestTest.java +++ b/src/test/java/com/auth0/net/BaseRequestTest.java @@ -401,9 +401,9 @@ public void shouldParsePlainTextErrorResponse() throws Exception { } @Test - public void shouldParseRateLimitsHeaders() { + public void shouldParseRateLimitException() throws Exception { BaseRequest<List> request = new BaseRequest<>(client, tokenProvider, server.getBaseUrl(), HttpMethod.GET, listType); - server.rateLimitReachedResponse(100, 10, 5); + server.rateLimitReachedResponse(100, 10, 5, RATE_LIMIT_ERROR); Exception exception = null; try { request.execute().getBody(); @@ -414,10 +414,10 @@ public void shouldParseRateLimitsHeaders() { assertThat(exception, Matchers.is(notNullValue())); assertThat(exception, Matchers.is(Matchers.instanceOf(RateLimitException.class))); assertThat(exception.getCause(), Matchers.is(nullValue())); - assertThat(exception.getMessage(), Matchers.is("Request failed with status code 429: Rate limit reached")); + assertThat(exception.getMessage(), Matchers.is("Request failed with status code 429: Global limit has been reached")); RateLimitException rateLimitException = (RateLimitException) exception; - assertThat(rateLimitException.getDescription(), Matchers.is("Rate limit reached")); - assertThat(rateLimitException.getError(), Matchers.is(nullValue())); + assertThat(rateLimitException.getDescription(), Matchers.is("Global limit has been reached")); + assertThat(rateLimitException.getError(), Matchers.is("too_many_requests")); assertThat(rateLimitException.getValue("non_existing_key"), Matchers.is(nullValue())); assertThat(rateLimitException.getStatusCode(), Matchers.is(429)); assertThat(rateLimitException.getLimit(), Matchers.is(100L)); @@ -426,7 +426,7 @@ public void shouldParseRateLimitsHeaders() { } @Test - public void shouldDefaultRateLimitsHeadersWhenMissing() { + public void shouldDefaultRateLimitsHeadersWhenMissing() throws Exception { BaseRequest<List> request = new BaseRequest<>(client, tokenProvider, server.getBaseUrl(), HttpMethod.GET, listType); server.rateLimitReachedResponse(-1, -1, -1); Exception exception = null; diff --git a/src/test/java/com/auth0/net/MultipartRequestTest.java b/src/test/java/com/auth0/net/MultipartRequestTest.java index 670a704c..9eb1f61f 100644 --- a/src/test/java/com/auth0/net/MultipartRequestTest.java +++ b/src/test/java/com/auth0/net/MultipartRequestTest.java @@ -335,7 +335,7 @@ public void shouldParsePlainTextErrorResponse() throws Exception { } @Test - public void shouldParseRateLimitsHeaders() { + public void shouldParseRateLimitsHeaders() throws Exception { MultipartRequest<List> request = new MultipartRequest<>(client, tokenProvider, server.getBaseUrl(), HttpMethod.POST, listType); request.addPart("non_empty", "body"); server.rateLimitReachedResponse(100, 10, 5); @@ -361,7 +361,7 @@ public void shouldParseRateLimitsHeaders() { } @Test - public void shouldDefaultRateLimitsHeadersWhenMissing() { + public void shouldDefaultRateLimitsHeadersWhenMissing() throws Exception { MultipartRequest<List> request = new MultipartRequest<>(client, tokenProvider, server.getBaseUrl(), HttpMethod.POST, listType); request.addPart("non_empty", "body"); server.rateLimitReachedResponse(-1, -1, -1); diff --git a/src/test/java/com/auth0/net/RateLimitInterceptorTest.java b/src/test/java/com/auth0/net/RateLimitInterceptorTest.java index f737578c..f05ea72b 100644 --- a/src/test/java/com/auth0/net/RateLimitInterceptorTest.java +++ b/src/test/java/com/auth0/net/RateLimitInterceptorTest.java @@ -108,9 +108,7 @@ public void shouldReturnResponseWhenMaxRetriesHit() throws Exception { int index = 0; for (int i = 0; i < maxRetries + 1; i++) { - System.out.println("about to take request " + i); index = server.takeRequest().getSequenceNumber(); - System.out.println("took request " + i); } assertThat(response.code(), is(429)); diff --git a/src/test/resources/mgmt/rate_limit_error.json b/src/test/resources/mgmt/rate_limit_error.json new file mode 100644 index 00000000..27478a62 --- /dev/null +++ b/src/test/resources/mgmt/rate_limit_error.json @@ -0,0 +1,6 @@ +{ + "statusCode":429, + "error":"Too Many Requests", + "message":"Global limit has been reached", + "errorCode":"too_many_requests" +} From 6d01c8875a3b471a1fbfdc242b0bba36f6e997cd Mon Sep 17 00:00:00 2001 From: Jim Anderson <jimranderson@gmail.com> Date: Fri, 3 Nov 2023 11:24:26 -0500 Subject: [PATCH 2/4] ensure async response is closed --- .../auth0/net/client/DefaultHttpClient.java | 2 + .../net/client/DefaultHttpClientTest.java | 55 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/main/java/com/auth0/net/client/DefaultHttpClient.java b/src/main/java/com/auth0/net/client/DefaultHttpClient.java index 3a4e73eb..8daf3cea 100644 --- a/src/main/java/com/auth0/net/client/DefaultHttpClient.java +++ b/src/main/java/com/auth0/net/client/DefaultHttpClient.java @@ -100,6 +100,8 @@ public void onResponse(@NotNull Call call, @NotNull Response response) { future.complete(buildResponse(response)); } catch (IOException e) { future.completeExceptionally(e); + } finally { + response.close(); } } }); diff --git a/src/test/java/com/auth0/net/client/DefaultHttpClientTest.java b/src/test/java/com/auth0/net/client/DefaultHttpClientTest.java index 0066a581..a9f1e7e7 100644 --- a/src/test/java/com/auth0/net/client/DefaultHttpClientTest.java +++ b/src/test/java/com/auth0/net/client/DefaultHttpClientTest.java @@ -462,6 +462,31 @@ public void alwaysCloseResponseOnSuccessfulRequest() throws IOException { verify(response, times(1)).close(); } + @Test + public void alwaysCloseResponseOnSuccessfulRequestAsync() throws Exception { + OkHttpClient okClient = Mockito.mock(OkHttpClient.class); + okhttp3.Response response = Mockito.mock(okhttp3.Response.class); + Call call = Mockito.mock(Call.class); + + doReturn(call).when(okClient).newCall(any()); + + doAnswer(invocation -> { + ((Callback) invocation.getArgument(0)).onResponse(call, response); + return null; + }).when(call).enqueue(any(Callback.class)); + + Headers headers = Mockito.mock(Headers.class); + when(response.headers()).thenReturn(headers); + + DefaultHttpClient client = new DefaultHttpClient(okClient); + Auth0HttpRequest request = Auth0HttpRequest.newBuilder(server.url("/users/").toString(), HttpMethod.POST) + .withBody(HttpRequestBody.create("application/json", "{}".getBytes())) + .build(); + + client.sendRequestAsync(request).get(); + verify(response, times(1)).close(); + } + @Test public void closesResponseOnAPIError() throws Exception { okhttp3.Response response = Mockito.mock(okhttp3.Response.class); @@ -490,6 +515,36 @@ public void closesResponseOnAPIError() throws Exception { verify(response, times(1)).close(); } + @Test + public void closesResponseOnAPIErrorAsync() throws Exception { + OkHttpClient okClient = Mockito.mock(OkHttpClient.class); + okhttp3.Response response = Mockito.mock(okhttp3.Response.class); + Call call = Mockito.mock(Call.class); + + doReturn(call).when(okClient).newCall(any()); + + doAnswer(invocation -> { + ((Callback) invocation.getArgument(0)).onResponse(call, response); + return null; + }).when(call).enqueue(any(Callback.class)); + + Headers headers = Mockito.mock(Headers.class); + when(response.headers()).thenReturn(headers); + + DefaultHttpClient client = new DefaultHttpClient(okClient); + Auth0HttpRequest request = Auth0HttpRequest.newBuilder(server.url("/users/").toString(), HttpMethod.POST) + .withBody(HttpRequestBody.create("application/json", "{}".getBytes())) + .build(); + + MockResponse mockResponse = new MockResponse() + .setResponseCode(500) + .setBody("server error"); + + server.enqueue(mockResponse); + client.sendRequestAsync(request).get(); + verify(response, times(1)).close(); + } + @Test public void makesFormBodyPostRequest() throws Exception { Map<String, Object> params = new HashMap<>(); From 9111008f199b11fd3466db208991c9a57f8ea01b Mon Sep 17 00:00:00 2001 From: Jim Anderson <jimranderson@gmail.com> Date: Fri, 3 Nov 2023 11:25:41 -0500 Subject: [PATCH 3/4] revert authapi retry config change --- src/main/java/com/auth0/client/auth/AuthAPI.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/auth0/client/auth/AuthAPI.java b/src/main/java/com/auth0/client/auth/AuthAPI.java index 5b35b3c8..9a94076e 100644 --- a/src/main/java/com/auth0/client/auth/AuthAPI.java +++ b/src/main/java/com/auth0/client/auth/AuthAPI.java @@ -1359,7 +1359,7 @@ public Builder withHttpClient(Auth0HttpClient httpClient) { */ public AuthAPI build() { return new AuthAPI(domain, clientId, clientSecret, clientAssertionSigner, - Objects.nonNull(httpClient) ? httpClient : DefaultHttpClient.newBuilder().withMaxRetries(0).build()); + Objects.nonNull(httpClient) ? httpClient : DefaultHttpClient.newBuilder().build()); } } } From a0134432fe70037220598f677eec08a23a294a31 Mon Sep 17 00:00:00 2001 From: Jim Anderson <jimranderson@gmail.com> Date: Mon, 6 Nov 2023 19:38:35 -0600 Subject: [PATCH 4/4] Release 2.8.0 --- CHANGELOG.md | 6 ++++++ README.md | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf8fbb3e..aece5d83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [2.8.0](https://github.com/auth0/auth0-java/tree/2.8.0) (2023-11-07) +[Full Changelog](https://github.com/auth0/auth0-java/compare/2.7.0...2.8.0) + +**Fixed** +- Propagate error messages on rate limit exceptions [\#579](https://github.com/auth0/auth0-java/pull/579) ([jimmyjames](https://github.com/jimmyjames)) + ## [2.7.0](https://github.com/auth0/auth0-java/tree/2.7.0) (2023-10-31) [Full Changelog](https://github.com/auth0/auth0-java/compare/2.6.1...2.7.0) diff --git a/README.md b/README.md index 4da400ed..6789a3bd 100644 --- a/README.md +++ b/README.md @@ -34,14 +34,14 @@ Add the dependency via Maven: <dependency> <groupId>com.auth0</groupId> <artifactId>auth0</artifactId> - <version>2.7.0</version> + <version>2.8.0</version> </dependency> ``` or Gradle: ```gradle -implementation 'com.auth0:auth0:2.7.0' +implementation 'com.auth0:auth0:2.8.0' ``` ### Configure the SDK