Skip to content

Instantly share code, notes, and snippets.

@vincentjames501
Created December 16, 2020 22:52
Show Gist options
  • Save vincentjames501/5050662740e30aac48dedb41306add87 to your computer and use it in GitHub Desktop.
Save vincentjames501/5050662740e30aac48dedb41306add87 to your computer and use it in GitHub Desktop.
JDK11+ HttpClient Retry
import javax.net.ssl.SSLException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.ConnectException;
import java.net.URI;
import java.net.UnknownHostException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
public class RetryDemo {
@FunctionalInterface
interface RetryHandler<T> {
CompletableFuture<Boolean> shouldRetry(HttpResponse<T> response, Throwable ex, HttpRequest request, int retryCount);
}
private static final class Result<T> {
private final HttpResponse<T> response;
private final Throwable ex;
public Result(HttpResponse<T> response, Throwable ex) {
this.response = response;
this.ex = ex;
}
public HttpResponse<T> getResponse() {
return response;
}
public Throwable getEx() {
return ex;
}
@Override
public String toString() {
return "Result{" +
"response=" + response +
", ex=" + ex +
'}';
}
}
private static final Set<Class<? extends IOException>> nonRetriableClasses = new HashSet<>(Arrays.asList(
InterruptedIOException.class,
UnknownHostException.class,
ConnectException.class,
SSLException.class
));
private static final Set<String> idempotentMethods = new HashSet<>(Arrays.asList(
"GET", "HEAD", "OPTIONS"
));
private static Throwable unwrapException(Throwable ex) {
if (ex instanceof CompletionException || ex instanceof ExecutionException) {
return ex.getCause();
} else {
return ex;
}
}
private static final RetryHandler<String> DEFAULT_RETRY_HANDLER = (response, ex, request, retryCount) -> {
// Get our root exception
Throwable unwrappedException = unwrapException(ex);
// Don't retry if more than 3 failures
if (retryCount >= 3) {
return CompletableFuture.completedFuture(false);
}
// Skip non-retriable exception types
if (unwrappedException != null) {
if (nonRetriableClasses.contains(unwrappedException.getClass())) {
return CompletableFuture.completedFuture(false);
} else {
for (final Class<? extends IOException> rejectException : nonRetriableClasses) {
if (rejectException.isInstance(unwrappedException)) {
return CompletableFuture.completedFuture(false);
}
}
}
}
// Retry if request is idempotent
if (unwrappedException instanceof IOException) {
if (idempotentMethods.contains(request.method())) {
return CompletableFuture.completedFuture(true);
}
}
// Don't retry
return CompletableFuture.completedFuture(false);
};
public static void main(String[] args) {
HttpClient client = HttpClient.newHttpClient();
try {
System.out.println(
makeAsyncRequest(
client,
HttpRequest.newBuilder(URI.create("https://httpbin.org/delay/5")).timeout(Duration.ofMillis(3000)).build(),
0,
DEFAULT_RETRY_HANDLER
).get()
);
} catch (Exception e) {
e.printStackTrace();
}
try {
System.out.println(
makeSyncRequest(
client,
HttpRequest.newBuilder(URI.create("https://httpbin.org/delay/5")).timeout(Duration.ofMillis(3000)).build(),
0,
DEFAULT_RETRY_HANDLER
)
);
} catch (Exception e) {
e.printStackTrace();
}
}
private static CompletableFuture<HttpResponse<String>> makeAsyncRequest(final HttpClient client, final HttpRequest request, final int retryCount, final RetryHandler<String> retryHandler) {
System.out.println("Making async request. Retry count: " + retryCount);
return client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.handle(Result::new)
.thenCompose(result -> retryHandler.shouldRetry(result.getResponse(), result.getEx(), request, retryCount)
.thenCompose(shouldRetry -> {
if (shouldRetry) {
return makeAsyncRequest(client, request, retryCount + 1, retryHandler);
} else {
if (result.getEx() != null) {
return CompletableFuture.failedFuture(result.getEx());
} else {
return CompletableFuture.completedFuture(result.getResponse());
}
}
}));
}
private static HttpResponse<String> makeSyncRequest(final HttpClient client, final HttpRequest request, final int retryCount, final RetryHandler<String> retryHandler) throws IOException, InterruptedException, ExecutionException {
System.out.println("Making sync request. Retry count: " + retryCount);
try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (retryHandler.shouldRetry(response, null, request, retryCount).get()) {
return makeSyncRequest(client, request, retryCount + 1, retryHandler);
} else {
return response;
}
} catch (Exception ex) {
if (retryHandler.shouldRetry(null, ex, request, retryCount).get()) {
return makeSyncRequest(client, request, retryCount + 1, retryHandler);
} else {
throw ex;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment