Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can't make more than one request on java.net.http.HttpClient or will receive: javax.net.ssl.SSLHandshakeException

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?

like image 966
JamesJose Avatar asked Dec 13 '18 16:12

JamesJose


1 Answers

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();
}
like image 140
VGR Avatar answered Nov 05 '22 14:11

VGR