Search code examples
javahttprequestretry-logicjava-http-client

Retry HTTP request (Java 11 - HttpClient)


Problem

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.


Attempt

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.


Other frameworks

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).


Reference

For reference, here are some related JavaDoc links:


Solution

  • 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);
        }
    }