I was testing out the new HttpClient
from Java 11 and came across the following behaviour:
I am making two Async requests to a public REST API for testing and tried it with one client and two separate requests. This process didn't throw any exceptions.
String singleCommentUrl = "https://jsonplaceholder.typicode.com/comments/1";
String commentsUrl = "https://jsonplaceholder.typicode.com/comments";
Consumer<String> handleOneComment = s -> {
Gson gson = new Gson();
Comment comment = gson.fromJson(s, Comment.class);
System.out.println(comment);
};
Consumer<String> handleListOfComments = s -> {
Gson gson = new Gson();
Comment[] comments = gson.fromJson(s, Comment[].class);
List<Comment> commentList = Arrays.asList(comments);
commentList.forEach(System.out::println);
};
HttpClient client = HttpClient.newBuilder().build();
client.sendAsync(HttpRequest.newBuilder(URI.create(singleCommentUrl)).build(), HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(handleOneComment)
.join();
client.sendAsync(HttpRequest.newBuilder(URI.create(commentsUrl)).build(), HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(handleListOfComments)
.join();
Then I tried refactoring the HttpClient
into a method and I got the following exception when it tried to make the second request:
public void run() {
String singleCommentUrl = "https://jsonplaceholder.typicode.com/comments/1";
String commentsUrl = "https://jsonplaceholder.typicode.com/comments";
Consumer<String> handleOneComment = s -> {
Gson gson = new Gson();
Comment comment = gson.fromJson(s, Comment.class);
System.out.println(comment);
};
Consumer<String> handleListOfComments = s -> {
Gson gson = new Gson();
Comment[] comments = gson.fromJson(s, Comment[].class);
List<Comment> commentList = Arrays.asList(comments);
commentList.forEach(System.out::println);
};
sendRequest(handleOneComment, singleCommentUrl);
sendRequest(handleListOfComments, commentsUrl);
}
private void sendRequest(Consumer<String> onSucces, String url) {
HttpClient client = HttpClient.newBuilder().build();
HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build();
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(onSucces)
.join();
}
This produces the following exception after succesfully executing the first request and failing at the second one:
Exception in thread "main" java.util.concurrent.CompletionException: javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure
at java.base/java.util.concurrent.CompletableFuture.encodeRelay(CompletableFuture.java:367)
at java.base/java.util.concurrent.CompletableFuture.completeRelay(CompletableFuture.java:376)
at java.base/java.util.concurrent.CompletableFuture$UniCompose.tryFire(CompletableFuture.java:1074)
at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:506)
at java.base/java.util.concurrent.CompletableFuture.completeExceptionally(CompletableFuture.java:2088)
at java.net.http/jdk.internal.net.http.common.SSLFlowDelegate.handleError(SSLFlowDelegate.java:904)
at java.net.http/jdk.internal.net.http.common.SSLFlowDelegate$Reader.processData(SSLFlowDelegate.java:450)
at java.net.http/jdk.internal.net.http.common.SSLFlowDelegate$Reader$ReaderDownstreamPusher.run(SSLFlowDelegate.java:263)
at java.net.http/jdk.internal.net.http.common.SequentialScheduler$SynchronizedRestartableTask.run(SequentialScheduler.java:175)
at java.net.http/jdk.internal.net.http.common.SequentialScheduler$CompleteRestartableTask.run(SequentialScheduler.java:147)
at java.net.http/jdk.internal.net.http.common.SequentialScheduler$SchedulableTask.run(SequentialScheduler.java:198)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure
at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:128)
at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:117)
at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:308)
at java.base/sun.security.ssl.Alert$AlertConsumer.consume(Alert.java:279)
at java.base/sun.security.ssl.TransportContext.dispatch(TransportContext.java:181)
at java.base/sun.security.ssl.SSLTransport.decode(SSLTransport.java:164)
at java.base/sun.security.ssl.SSLEngineImpl.decode(SSLEngineImpl.java:672)
at java.base/sun.security.ssl.SSLEngineImpl.readRecord(SSLEngineImpl.java:627)
at java.base/sun.security.ssl.SSLEngineImpl.unwrap(SSLEngineImpl.java:443)
at java.base/sun.security.ssl.SSLEngineImpl.unwrap(SSLEngineImpl.java:422)
at java.base/javax.net.ssl.SSLEngine.unwrap(SSLEngine.java:634)
at java.net.http/jdk.internal.net.http.common.SSLFlowDelegate$Reader.unwrapBuffer(SSLFlowDelegate.java:480)
at java.net.http/jdk.internal.net.http.common.SSLFlowDelegate$Reader.processData(SSLFlowDelegate.java:389)
... 7 more
I tried passing separate clients and requests via parameters in the method as well but it produced the same result. What is going on here?
Apparently, SSLContext objects are not thread-safe. (It’s usually correct to assume that any mutable object whose contract doesn’t explicitly guarantee thread safety is not thread-safe.)
HttpClients use the default SSLContext if not given a context explicitly. So it appears your two requests are trying to simultaneously share that default context.
The solution is to specify a brand new SSLContext for each HttpClient:
private void sendRequest(Consumer<String> onSucces, String url) {
SSLContext context;
try {
context = SSLContext.getInstance("TLSv1.3");
context.init(null, null, null);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
HttpClient client = HttpClient.newBuilder().sslContext(context).build();
HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build();
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(onSucces)
.join();
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With