Created
December 16, 2020 22:52
-
-
Save vincentjames501/5050662740e30aac48dedb41306add87 to your computer and use it in GitHub Desktop.
JDK11+ HttpClient Retry
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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