Using HttpClient
from Java 11 (JDK, not Apache), how can I retry requests?
Lets say I want to retry a request up to 10 times if it did not return a status code of 200
or threw an exception.
Currently I am composing the returned future with re-schedules in a loop and I am wondering whether there might be a better or more elegant way.
CompletableFuture<HttpResponse<Foo>> task = client.sendAsync(request, bodyHandler);
for (int i = 0; i < 10; i++) {
task = task.thenComposeAsync(response -> response.statusCode() == 200 ?
CompletableFuture.completedFuture(response) :
client.sendAsync(request, bodyHandler));
}
// Do something with 'task' ...
And if we add retries for exceptional cases as well, I end up with
CompletableFuture<HttpResponse<Foo>> task = client.sendAsync(request, bodyHandler);
for (int i = 0; i < 10; i++) {
task = task.thenComposeAsync(response ->
response.statusCode() == 200 ?
CompletableFuture.completedFuture(response) :
client.sendAsync(request, bodyHandler))
.exceptionallyComposeAsync(e ->
client.sendAsync(request, bodyHandler));
}
// Do something with 'task' ...
Unfortunately there does not seem to be any composeAsync
that triggers for both, regular completion and exceptional. There is handleAsync
but it does not compose
, the lambda is required to return U
and not CompletionStage<U>
there.
For the sake of QA, I am also interested in answers that show how to achieve this with other frameworks, but I wont accept them.
For example, I have seen a library called Failsafe which might offer an elegant solution to this (see jodah.net/failsafe).
For reference, here are some related JavaDoc links:
I'd suggest to do something along these lines instead (assuming no security manager):
public static int MAX_RESEND = 10;
public static void main(String[] args) {
HttpClient client = HttpClient.newHttpClient();
HttpResponse.BodyHandler<String> handler = HttpResponse.BodyHandlers.ofString();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://example.com/")).build();
var response = client.sendAsync(request, handler)
.thenComposeAsync(r -> tryResend(client, request, handler, 1, r));
// do something with response...
}
public static <T> CompletableFuture<HttpResponse<T>>
tryResend(HttpClient client, HttpRequest request, BodyHandler<T> handler,
int count, HttpResponse<T> resp) {
if (resp.statusCode() == 200 || count >= MAX_RESEND) {
return CompletableFuture.completedFuture(resp);
} else {
return client.sendAsync(request, handler)
.thenComposeAsync(r -> tryResend(client, request, handler, count+1, r));
}
}
And if you wanted to handle both the regular and exceptional case you could do something like:
public static int MAX_RESEND = 5;
public static void main(String[] args) {
HttpClient client = HttpClient.newHttpClient();
BodyHandler<String> handler = BodyHandlers.ofString();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://example.com/")).build();
CompletableFuture<HttpResponse<String>> response =
client.sendAsync(request, handler)
.handleAsync((r, t) -> tryResend(client, request, handler, 1, r, t))
.thenCompose(Function.identity());
// do something with response ...
}
public static boolean shouldRetry(HttpResponse<?> r, Throwable t, int count) {
if (r != null && r.statusCode() == 200 || count >= MAX_RESEND) return false;
if (t instanceof ... ) return false;
return true;
}
public static <T> CompletableFuture<HttpResponse<T>>
tryResend(HttpClient client, HttpRequest request,
BodyHandler<T> handler, int count,
HttpResponse<T> resp, Throwable t) {
if (shouldRetry(resp, t, count)) {
return client.sendAsync(request, handler)
.handleAsync((r, x) -> tryResend(client, request, handler, count + 1, r, x))
.thenCompose(Function.identity());
} else if (t != null) {
return CompletableFuture.failedFuture(t);
} else {
return CompletableFuture.completedFuture(resp);
}
}