-
Bug
-
Resolution: Unresolved
-
P4
-
22
-
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
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