Please consider the code that's at the end of this mail. It is a simple client/server code where a HTTPS server is created and set to "needClientAuth". The client then uses HttpsURLConnection and (in this case intentionally) doesn't present any certificate, expecting the handshake to fail. In previous versions of Java, the handshake failure in this code would throw a java.net.SocketException as below:
Exception in thread "main" java.net.SocketException: Connection reset at java.net.SocketInputStream.read(SocketInputStream.java:210) at java.net.SocketInputStream.read(SocketInputStream.java:141) at sun.security.ssl.InputRecord.readFully(InputRecord.java:465) at sun.security.ssl.InputRecord.read(InputRecord.java:503) at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:983) at sun.security.ssl.SSLSocketImpl.waitForClose(SSLSocketImpl.java:1779) at sun.security.ssl.HandshakeOutStream.flush(HandshakeOutStream.java:124) at sun.security.ssl.Handshaker.sendChangeCipherSpec(Handshaker.java:1156) at sun.security.ssl.ClientHandshaker.sendChangeCipherAndFinish(ClientHandshaker.java:1266) at sun.security.ssl.ClientHandshaker.serverHelloDone(ClientHandshaker.java:1178) at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:348) at sun.security.ssl.Handshaker.processLoop(Handshaker.java:1052) at sun.security.ssl.Handshaker.process_record(Handshaker.java:987) at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1072) at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1385) at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1413) at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1397) at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:559) at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185) at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1564) at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1492) at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:263) at ClientCertTest.main(ClientCertTest.java:26) However, in the Java 11 (release candidate) as well as Java 12 (upstream), this code now throws a javax.net.ssl.SSLProtocolException with the java.net.SocketException wrapped in it: Exception in thread "main" javax.net.ssl.SSLProtocolException: Connection reset at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:126) at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:321) at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:264) at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:259) at java.base/sun.security.ssl.SSLSocketImpl.handleException(SSLSocketImpl.java:1314) at java.base/sun.security.ssl.SSLSocketImpl$AppInputStream.read(SSLSocketImpl.java:839) at java.base/java.io.BufferedInputStream.fill(BufferedInputStream.java:252) at java.base/java.io.BufferedInputStream.read1(BufferedInputStream.java:292) at java.base/java.io.BufferedInputStream.read(BufferedInputStream.java:351) at java.base/sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:746) at java.base/sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:689) at java.base/sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:717) at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1604) at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1509) at java.base/sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:245) at ClientCertTest.main(ClientCertTest.java:26) Caused by: java.net.SocketException: Connection reset at java.base/java.net.SocketInputStream.read(SocketInputStream.java:186) at java.base/java.net.SocketInputStream.read(SocketInputStream.java:140) at java.base/sun.security.ssl.SSLSocketInputRecord.read(SSLSocketInputRecord.java:448) at java.base/sun.security.ssl.SSLSocketInputRecord.bytesInCompletePacket(SSLSocketInputRecord.java:68) at java.base/sun.security.ssl.SSLSocketImpl.readApplicationRecord(SSLSocketImpl.java:1104) at java.base/sun.security.ssl.SSLSocketImpl$AppInputStream.read(SSLSocketImpl.java:823) ... 10 more Is this an intentional and expected change? As far as I could see, there isn't any specific API which says SocketException will be thrown in this case, so maybe the client applications which were catching this specific exception are expected to change their catch block to something more generic like a IOException? Here's the code to reproduce this: import java.io.*; import java.net.*; import javax.net.ssl.*; import java.util.*; import java.security.*; import com.sun.net.httpserver.*; import java.security.cert.*; public class ClientCertTest { private static final String keyFilename = "keystore"; private static final String keystorePass = "passphrase"; public static void main(final String[] args) throws Exception { final int port = 12345; final HttpsServer server = startServer("localhost", port); try { final URL targetURL = new URL("https://localhost:" + port + "/"); final SSLContext sslctx = SSLContext.getInstance("TLS"); sslctx.init(null, new TrustManager[] {new TrustAll()}, null); HttpsURLConnection.setDefaultHostnameVerifier((h, s) -> {return true;}); final HttpsURLConnection conn = (HttpsURLConnection) targetURL.openConnection(); // setup the HTTPS connection to use our SocketFactory which doesn't present // any cert when asked for (and thus is expected to fail handshake) conn.setSSLSocketFactory(sslctx.getSocketFactory()); try (final InputStream is = conn.getInputStream()) { is.read(); } catch (SocketException se) { System.out.println("*** Received the expected SocketException: " + se.getMessage()); throw se; } } finally { server.stop(0); System.out.println("Stopped server"); } } private static HttpsServer startServer(final String host, final int port) throws Exception { final HttpsServer server = HttpsServer.create(new InetSocketAddress(host, port), 0); final Thread t = new Thread(() -> { try { final SSLContext sslctx = SSLContext.getInstance("TLS"); final KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); final KeyStore ks = KeyStore.getInstance("JKS"); try (final FileInputStream fis = new FileInputStream(keyFilename)) { ks.load(fis, keystorePass.toCharArray()); } kmf.init(ks, keystorePass.toCharArray()); sslctx.init(kmf.getKeyManagers(), null, null); final SSLParameters sslParameters = sslctx.getDefaultSSLParameters(); // need client auth sslParameters.setNeedClientAuth(true); server.setHttpsConfigurator(new HttpsConfigurator(sslctx) { @Override public void configure(final HttpsParameters params) { params.setSSLParameters(sslParameters); } }); server.start(); System.out.println("Started server at " + server.getAddress()); } catch(Exception e) { throw new RuntimeException(e); } }); t.start(); return server; } private static class TrustAll implements X509TrustManager { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { return; } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { return; } @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } } } P.S: The "keystore" file in this example is the same one that's present in the openjdk repo at test/jdk/javax/net/ssl/etc/keystore, so in order to run the above code, that file can just be placed in the current working dir. -Jaikiran