Skip to content

Commit 215a37c

Browse files
authored
propagate error message on rate limit exception (#579)
1 parent 229e3e7 commit 215a37c

10 files changed

Lines changed: 110 additions & 24 deletions

File tree

src/main/java/com/auth0/exception/RateLimitException.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.auth0.exception;
22

3+
import java.util.Map;
4+
35
/**
46
* Represents a server error when a rate limit has been exceeded.
57
* <p>
@@ -16,6 +18,13 @@ public class RateLimitException extends APIException {
1618

1719
private static final int STATUS_CODE_TOO_MANY_REQUEST = 429;
1820

21+
public RateLimitException(long limit, long remaining, long reset, Map<String, Object> values) {
22+
super(values, STATUS_CODE_TOO_MANY_REQUEST);
23+
this.limit = limit;
24+
this.remaining = remaining;
25+
this.reset = reset;
26+
}
27+
1928
public RateLimitException(long limit, long remaining, long reset) {
2029
super("Rate limit reached", STATUS_CODE_TOO_MANY_REQUEST, null);
2130
this.limit = limit;

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,15 @@ private RateLimitException createRateLimitException(Auth0HttpResponse response)
218218
long limit = Long.parseLong(response.getHeader("x-ratelimit-limit", "-1"));
219219
long remaining = Long.parseLong(response.getHeader("x-ratelimit-remaining", "-1"));
220220
long reset = Long.parseLong(response.getHeader("x-ratelimit-reset", "-1"));
221-
return new RateLimitException(limit, remaining, reset);
221+
222+
String payload = response.getBody();
223+
MapType mapType = mapper.getTypeFactory().constructMapType(HashMap.class, String.class, Object.class);
224+
try {
225+
Map<String, Object> values = mapper.readValue(payload, mapType);
226+
return new RateLimitException(limit, remaining, reset, values);
227+
} catch (IOException e) {
228+
return new RateLimitException(limit, remaining, reset);
229+
}
222230
}
223231

224232
/**

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,24 +67,24 @@ public Response intercept(@NotNull Chain chain) throws IOException {
6767
.withMaxRetries(maxRetries)
6868
.withBackoff(INITIAL_INTERVAL, MAX_INTERVAL, ChronoUnit.MILLIS)
6969
.withJitter(JITTER)
70-
.handleResultIf(response -> {
71-
if (response.code() == 429) {
72-
response.close();
73-
return true;
74-
}
75-
return false;
76-
});
70+
.handleResultIf(response -> response.code() == 429);
7771

7872
// For testing purposes only, allow test to hook into retry listener to enable verification of retry backoff
7973
if (retryListener != null) {
8074
retryPolicy.onRetry(retryListener);
8175
}
8276

8377
try {
78+
return Failsafe.with(retryPolicy).get((context) -> {
79+
// ensure response of last recorded response prior to retry is closed
80+
if (context.getLastResult() != null) {
81+
context.getLastResult().close();;
82+
}
83+
return chain.proceed(chain.request());
84+
});
85+
} catch (FailsafeException fe) {
8486
// throw Auth0Exception instead of FailSafe exception on error
8587
// see https://github.com/auth0/auth0-java/issues/483
86-
return Failsafe.with(retryPolicy).get(() -> chain.proceed(chain.request()));
87-
} catch (FailsafeException fe) {
8888
throw new Auth0Exception("Failed to execute request", fe.getCause());
8989
}
9090
}

src/main/java/com/auth0/net/client/DefaultHttpClient.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ public void onResponse(@NotNull Call call, @NotNull Response response) {
100100
future.complete(buildResponse(response));
101101
} catch (IOException e) {
102102
future.completeExceptionally(e);
103+
} finally {
104+
response.close();
103105
}
104106
}
105107
});
@@ -138,9 +140,7 @@ private Auth0HttpResponse buildResponse(Response okResponse) throws IOException
138140
ResponseBody responseBody = okResponse.body();
139141
String content = null;
140142

141-
// The RateLimitInterceptor needs to close the response; we don't need the body in that case and trying to
142-
// get the body will result in an exception because the responsebody has been closed
143-
if (Objects.nonNull(responseBody) && okResponse.code() != 429) {
143+
if (Objects.nonNull(responseBody)) {
144144
content = responseBody.string();
145145
}
146146
return Auth0HttpResponse.newBuilder()

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ public class MockServer {
144144
public static final String KEY_LIST = "src/test/resources/mgmt/key_list.json";
145145
public static final String KEY_REVOKE = "src/test/resources/mgmt/key_revoke.json";
146146
public static final String KEY_ROTATE = "src/test/resources/mgmt/key_rotate.json";
147+
public static final String RATE_LIMIT_ERROR = "src/test/resources/mgmt/rate_limit_error.json";
147148

148149
private final MockWebServer server;
149150

@@ -184,7 +185,11 @@ public void noContentResponse() {
184185
server.enqueue(response);
185186
}
186187

187-
public void rateLimitReachedResponse(long limit, long remaining, long reset) {
188+
public void rateLimitReachedResponse(long limit, long remaining, long reset) throws IOException {
189+
rateLimitReachedResponse(limit, remaining, reset, null);
190+
}
191+
192+
public void rateLimitReachedResponse(long limit, long remaining, long reset, String path) throws IOException {
188193
MockResponse response = new MockResponse().setResponseCode(429);
189194
if (limit != -1) {
190195
response.addHeader("x-ratelimit-limit", String.valueOf(limit));
@@ -195,6 +200,11 @@ public void rateLimitReachedResponse(long limit, long remaining, long reset) {
195200
if (reset != -1) {
196201
response.addHeader("x-ratelimit-reset", String.valueOf(reset));
197202
}
203+
if (path != null) {
204+
response
205+
.addHeader("Content-Type", "application/json")
206+
.setBody(readTextFile(path));
207+
}
198208
server.enqueue(response);
199209
}
200210

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -401,9 +401,9 @@ public void shouldParsePlainTextErrorResponse() throws Exception {
401401
}
402402

403403
@Test
404-
public void shouldParseRateLimitsHeaders() {
404+
public void shouldParseRateLimitException() throws Exception {
405405
BaseRequest<List> request = new BaseRequest<>(client, tokenProvider, server.getBaseUrl(), HttpMethod.GET, listType);
406-
server.rateLimitReachedResponse(100, 10, 5);
406+
server.rateLimitReachedResponse(100, 10, 5, RATE_LIMIT_ERROR);
407407
Exception exception = null;
408408
try {
409409
request.execute().getBody();
@@ -414,10 +414,10 @@ public void shouldParseRateLimitsHeaders() {
414414
assertThat(exception, Matchers.is(notNullValue()));
415415
assertThat(exception, Matchers.is(Matchers.instanceOf(RateLimitException.class)));
416416
assertThat(exception.getCause(), Matchers.is(nullValue()));
417-
assertThat(exception.getMessage(), Matchers.is("Request failed with status code 429: Rate limit reached"));
417+
assertThat(exception.getMessage(), Matchers.is("Request failed with status code 429: Global limit has been reached"));
418418
RateLimitException rateLimitException = (RateLimitException) exception;
419-
assertThat(rateLimitException.getDescription(), Matchers.is("Rate limit reached"));
420-
assertThat(rateLimitException.getError(), Matchers.is(nullValue()));
419+
assertThat(rateLimitException.getDescription(), Matchers.is("Global limit has been reached"));
420+
assertThat(rateLimitException.getError(), Matchers.is("too_many_requests"));
421421
assertThat(rateLimitException.getValue("non_existing_key"), Matchers.is(nullValue()));
422422
assertThat(rateLimitException.getStatusCode(), Matchers.is(429));
423423
assertThat(rateLimitException.getLimit(), Matchers.is(100L));
@@ -426,7 +426,7 @@ public void shouldParseRateLimitsHeaders() {
426426
}
427427

428428
@Test
429-
public void shouldDefaultRateLimitsHeadersWhenMissing() {
429+
public void shouldDefaultRateLimitsHeadersWhenMissing() throws Exception {
430430
BaseRequest<List> request = new BaseRequest<>(client, tokenProvider, server.getBaseUrl(), HttpMethod.GET, listType);
431431
server.rateLimitReachedResponse(-1, -1, -1);
432432
Exception exception = null;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ public void shouldParsePlainTextErrorResponse() throws Exception {
335335
}
336336

337337
@Test
338-
public void shouldParseRateLimitsHeaders() {
338+
public void shouldParseRateLimitsHeaders() throws Exception {
339339
MultipartRequest<List> request = new MultipartRequest<>(client, tokenProvider, server.getBaseUrl(), HttpMethod.POST, listType);
340340
request.addPart("non_empty", "body");
341341
server.rateLimitReachedResponse(100, 10, 5);
@@ -361,7 +361,7 @@ public void shouldParseRateLimitsHeaders() {
361361
}
362362

363363
@Test
364-
public void shouldDefaultRateLimitsHeadersWhenMissing() {
364+
public void shouldDefaultRateLimitsHeadersWhenMissing() throws Exception {
365365
MultipartRequest<List> request = new MultipartRequest<>(client, tokenProvider, server.getBaseUrl(), HttpMethod.POST, listType);
366366
request.addPart("non_empty", "body");
367367
server.rateLimitReachedResponse(-1, -1, -1);

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,7 @@ public void shouldReturnResponseWhenMaxRetriesHit() throws Exception {
108108

109109
int index = 0;
110110
for (int i = 0; i < maxRetries + 1; i++) {
111-
System.out.println("about to take request " + i);
112111
index = server.takeRequest().getSequenceNumber();
113-
System.out.println("took request " + i);
114112
}
115113

116114
assertThat(response.code(), is(429));

src/test/java/com/auth0/net/client/DefaultHttpClientTest.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,31 @@ public void alwaysCloseResponseOnSuccessfulRequest() throws IOException {
462462
verify(response, times(1)).close();
463463
}
464464

465+
@Test
466+
public void alwaysCloseResponseOnSuccessfulRequestAsync() throws Exception {
467+
OkHttpClient okClient = Mockito.mock(OkHttpClient.class);
468+
okhttp3.Response response = Mockito.mock(okhttp3.Response.class);
469+
Call call = Mockito.mock(Call.class);
470+
471+
doReturn(call).when(okClient).newCall(any());
472+
473+
doAnswer(invocation -> {
474+
((Callback) invocation.getArgument(0)).onResponse(call, response);
475+
return null;
476+
}).when(call).enqueue(any(Callback.class));
477+
478+
Headers headers = Mockito.mock(Headers.class);
479+
when(response.headers()).thenReturn(headers);
480+
481+
DefaultHttpClient client = new DefaultHttpClient(okClient);
482+
Auth0HttpRequest request = Auth0HttpRequest.newBuilder(server.url("/users/").toString(), HttpMethod.POST)
483+
.withBody(HttpRequestBody.create("application/json", "{}".getBytes()))
484+
.build();
485+
486+
client.sendRequestAsync(request).get();
487+
verify(response, times(1)).close();
488+
}
489+
465490
@Test
466491
public void closesResponseOnAPIError() throws Exception {
467492
okhttp3.Response response = Mockito.mock(okhttp3.Response.class);
@@ -490,6 +515,36 @@ public void closesResponseOnAPIError() throws Exception {
490515
verify(response, times(1)).close();
491516
}
492517

518+
@Test
519+
public void closesResponseOnAPIErrorAsync() throws Exception {
520+
OkHttpClient okClient = Mockito.mock(OkHttpClient.class);
521+
okhttp3.Response response = Mockito.mock(okhttp3.Response.class);
522+
Call call = Mockito.mock(Call.class);
523+
524+
doReturn(call).when(okClient).newCall(any());
525+
526+
doAnswer(invocation -> {
527+
((Callback) invocation.getArgument(0)).onResponse(call, response);
528+
return null;
529+
}).when(call).enqueue(any(Callback.class));
530+
531+
Headers headers = Mockito.mock(Headers.class);
532+
when(response.headers()).thenReturn(headers);
533+
534+
DefaultHttpClient client = new DefaultHttpClient(okClient);
535+
Auth0HttpRequest request = Auth0HttpRequest.newBuilder(server.url("/users/").toString(), HttpMethod.POST)
536+
.withBody(HttpRequestBody.create("application/json", "{}".getBytes()))
537+
.build();
538+
539+
MockResponse mockResponse = new MockResponse()
540+
.setResponseCode(500)
541+
.setBody("server error");
542+
543+
server.enqueue(mockResponse);
544+
client.sendRequestAsync(request).get();
545+
verify(response, times(1)).close();
546+
}
547+
493548
@Test
494549
public void makesFormBodyPostRequest() throws Exception {
495550
Map<String, Object> params = new HashMap<>();
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"statusCode":429,
3+
"error":"Too Many Requests",
4+
"message":"Global limit has been reached",
5+
"errorCode":"too_many_requests"
6+
}

0 commit comments

Comments
 (0)