Не может сделать более одного запроса на java.net.http.HttpClient или получит: javax.net.ssl.SSLHandshakeException

Я тестировал новый HttpClient с Java 11 и натолкнулся на следующее поведение:

Я делаю два асинхронных запроса к общедоступному REST API для тестирования и пробовал его с одним клиентом и двумя отдельными запросами. Этот процесс не выбрасывал никаких исключений.

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();

Затем я попытался рефакторинг HttpClient в метод, и я получил следующее исключение, когда он попытался сделать второй запрос:

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

Это приводит к следующему исключению после успешного выполнения первого запроса и сбоя во втором:

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

Я попытался передать отдельные клиенты и запросы через параметры в методе, но он дал тот же результат. Что здесь происходит?

1 ответ

Решение

Очевидно, объекты SSLContext не являются потокобезопасными. (Обычно правильно предположить, что любой изменчивый объект, чей контракт явно не гарантирует безопасность потока, не является потокобезопасным.)

HttpClients используют SSLContext по умолчанию, если контекст явно не задан. Похоже, что ваши два запроса пытаются одновременно использовать этот контекст по умолчанию.

Решение состоит в том, чтобы указать новый SSLContext для каждого 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();
}
Другие вопросы по тегам