Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8336864

HttpClient does not respond to TLS closure alert, tries to reuse connection

XMLWordPrintable

    • generic
    • generic

      A DESCRIPTION OF THE PROBLEM :
      java.net.http.HttpClient does not respond to TLS closure alert (notify_close) when using HTTPS 1.1. I didn't test with HTTPS 2.

      When the HTTPS 1.1 server tries to closes the connection with notify_close, HttpClient does not respond with notify_close, resulting in a half-closed TLS connection. Later when another HTTPS request is finding a connection, HttpClient will try to reuse the half-closed TLS connection, unable to receive any response.

      Non-idempotent requests are not retried automatically. Thus the exception propagates to the caller when the request method is POST etc.

      Debug logs from JDK indicates that it indeed received notify_close, SSLEngine set its isInboundDone() to true.

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      1. Open a HTTPS server with uvicorn with default configuration. Just follow the quickstart from README, and add following arguments to add SSL key/cert file:
      --ssl-keyfile ./key --ssl-certfile ./cert
      I used a self-signed certificate made with mkcert.

      2. With JDK HttpClient, send a HTTPS 1.1 POST request, sleep for 5 seconds or more, send another HTTPS 1.1 POST request.

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      1. Uvicorn sends TLS notify_close after 5 seconds, half-closing the connection. HttpClient should accept the closure by responding back with notify_close, after which the connection should be completely closed.
      2. HttpClient should not try to reuse half-closed TLS connections.
      ACTUAL -
      1. HttpClient does not respond back with notify_close, neither does it completely close the connection.
      2. HttpClient tries to reuse half-closed TLS connections, resulting in reading EOF.

      EOF error stacktrace:

      Exception in thread "main" java.io.IOException: HTTP/1.1 header parser received no bytes
      at java.net.http/jdk.internal.net.http.HttpClientImpl.send(HttpClientImpl.java:969)
      at java.net.http/jdk.internal.net.http.HttpClientFacade.send(HttpClientFacade.java:133)
      at HttpClientTest.main(HttpClientTest.java:41)
      Caused by: java.io.IOException: HTTP/1.1 header parser received no bytes

      Caused by: java.io.IOException: HTTP/1.1 header parser received no bytes
      at java.net.http/jdk.internal.net.http.common.Utils.wrapWithExtraDetail(Utils.java:440)
      at java.net.http/jdk.internal.net.http.Http1Response$HeadersReader.onReadError(Http1Response.java:590)
      at java.net.http/jdk.internal.net.http.Http1AsyncReceiver.checkForErrors(Http1AsyncReceiver.java:302)
      at java.net.http/jdk.internal.net.http.Http1AsyncReceiver.flush(Http1AsyncReceiver.java:268)
      at java.net.http/jdk.internal.net.http.common.SequentialScheduler$LockingRestartableTask.run(SequentialScheduler.java:182)
      at java.net.http/jdk.internal.net.http.common.SequentialScheduler$CompleteRestartableTask.run(SequentialScheduler.java:149)
      at java.net.http/jdk.internal.net.http.common.SequentialScheduler$SchedulableTask.run(SequentialScheduler.java:207)
      at java.net.http/jdk.internal.net.http.HttpClientImpl$DelegatingExecutor.execute(HttpClientImpl.java:178)
      at java.net.http/jdk.internal.net.http.common.SequentialScheduler.runOrSchedule(SequentialScheduler.java:282)
      at java.net.http/jdk.internal.net.http.common.SequentialScheduler.runOrSchedule(SequentialScheduler.java:251)
      at java.net.http/jdk.internal.net.http.Http1AsyncReceiver.onReadError(Http1AsyncReceiver.java:516)
      at java.net.http/jdk.internal.net.http.Http1AsyncReceiver$Http1TubeSubscriber.onComplete(Http1AsyncReceiver.java:601)
      at java.net.http/jdk.internal.net.http.common.SSLTube$DelegateWrapper.onComplete(SSLTube.java:276)
      at java.net.http/jdk.internal.net.http.common.SSLTube$SSLSubscriberWrapper.complete(SSLTube.java:440)
      at java.net.http/jdk.internal.net.http.common.SSLTube$SSLSubscriberWrapper.onComplete(SSLTube.java:541)
      at java.net.http/jdk.internal.net.http.common.SubscriberWrapper.checkCompletion(SubscriberWrapper.java:474)
      at java.net.http/jdk.internal.net.http.common.SubscriberWrapper$DownstreamPusher.run1(SubscriberWrapper.java:334)
      at java.net.http/jdk.internal.net.http.common.SubscriberWrapper$DownstreamPusher.run(SubscriberWrapper.java:259)
      at java.net.http/jdk.internal.net.http.common.SequentialScheduler$LockingRestartableTask.run(SequentialScheduler.java:182)
      at java.net.http/jdk.internal.net.http.common.SequentialScheduler$CompleteRestartableTask.run(SequentialScheduler.java:149)
      at java.net.http/jdk.internal.net.http.common.SequentialScheduler$SchedulableTask.run(SequentialScheduler.java:207)
      at java.net.http/jdk.internal.net.http.common.SequentialScheduler.runOrSchedule(SequentialScheduler.java:280)
      at java.net.http/jdk.internal.net.http.common.SequentialScheduler.runOrSchedule(SequentialScheduler.java:233)
      at java.net.http/jdk.internal.net.http.common.SubscriberWrapper.outgoing(SubscriberWrapper.java:232)
      at java.net.http/jdk.internal.net.http.common.SSLFlowDelegate$Reader.processData(SSLFlowDelegate.java:540)
      at java.net.http/jdk.internal.net.http.common.SSLFlowDelegate$Reader$ReaderDownstreamPusher.run(SSLFlowDelegate.java:283)
      at java.net.http/jdk.internal.net.http.common.SequentialScheduler$LockingRestartableTask.run(SequentialScheduler.java:182)
      at java.net.http/jdk.internal.net.http.common.SequentialScheduler$CompleteRestartableTask.run(SequentialScheduler.java:149)
      at java.net.http/jdk.internal.net.http.common.SequentialScheduler$SchedulableTask.run(SequentialScheduler.java:207)
      at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
      at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
      at java.base/java.lang.Thread.run(Thread.java:1570)
      Caused by: java.io.EOFException: EOF reached while reading

      Caused by: java.io.EOFException: EOF reached while reading
      ... 21 more

      ---------- BEGIN SOURCE ----------
      import java.io.IOException;
      import java.net.URI;
      import java.net.http.HttpClient;
      import java.net.http.HttpRequest;
      import java.net.http.HttpResponse;
      import java.security.KeyManagementException;
      import java.security.NoSuchAlgorithmException;
      import java.security.SecureRandom;
      import java.security.cert.CertificateException;
      import java.security.cert.X509Certificate;
      import javax.net.ssl.HttpsURLConnection;
      import javax.net.ssl.SSLContext;
      import javax.net.ssl.TrustManager;
      import javax.net.ssl.X509TrustManager;

      public final class HttpClientTest {
          public static void main(String[] args) throws NoSuchAlgorithmException, KeyManagementException, InterruptedException, IOException {
              /* allow self-signed certs for testing
              var trustAllCerts = new TrustManager[] { new X509TrustManager() {
                  @Override
                  public X509Certificate[] getAcceptedIssuers() {
                      return null;
                  }

                  @Override
                  public void checkClientTrusted(X509Certificate[] chain, String authType) {}

                  @Override
                  public void checkServerTrusted(X509Certificate[] chain, String authType) {}
              } };

              System.setProperty("jdk.internal.httpclient.disableHostnameVerification", "true");
              var sc = SSLContext.getInstance("TLS");
              sc.init(null, trustAllCerts, new SecureRandom());
              HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
               */

              try (var client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1)/*.sslContext(sc)*/.build()) {
                  System.out.println("first request");
                  System.out.println(client.send(HttpRequest.newBuilder(URI.create("https://127.0.0.1:8000")).POST(HttpRequest.BodyPublishers.ofString("asdf")).build(), HttpResponse.BodyHandlers.ofString()).body());
                  Thread.sleep(5_000);
                  System.out.println("second request");
                  System.out.println(client.send(HttpRequest.newBuilder(URI.create("https://127.0.0.1:8000")).POST(HttpRequest.BodyPublishers.ofString("asdf")).build(), HttpResponse.BodyHandlers.ofString()).body());
              }
          }
      }

      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      Enable duplex-close with VM argument:
      -Djdk.tls.acknowledgeCloseNotify=true

      FREQUENCY : always


            jpai Jaikiran Pai
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

              Created:
              Updated: