From f125305e50599f4d9f25cc404b4ad8e6d59ec6ef Mon Sep 17 00:00:00 2001 From: Daniel Fuchs Date: Fri, 7 Nov 2025 14:14:30 +0000 Subject: [PATCH 1/6] 8371471: HttpClient: Log HTTP/3 handshake failures if logging errors is enabled --- .../jdk/internal/net/http/common/Log.java | 3 +- .../net/http/quic/QuicConnectionImpl.java | 11 ++ .../http3/H3LogHandshakeErrors.java | 172 ++++++++++++++++++ 3 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java index bc89a6e9d8e1e..e75620e19baf5 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java @@ -391,8 +391,7 @@ public static void logError(String s, Object... s1) { public static void logError(Throwable t) { if (errors()) { - String s = Utils.stackTrace(t); - logger.log(Level.INFO, "ERROR: " + s); + logger.log(Level.INFO, "ERROR: " + t, t); } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java index f05519d339b72..b5ac6441ba3c5 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java @@ -591,6 +591,11 @@ public void failHandshakeCFs(final Throwable cause) { SSLHandshakeException sslHandshakeException = null; if (!handshakeCF.isDone()) { sslHandshakeException = sslHandshakeException(cause); + if (Log.errors()) { + Log.logError("%s QUIC handshake failed: %s" + .formatted(logTag(), message(cause))); + Log.logError(cause); + } handshakeCF.completeExceptionally(sslHandshakeException); } if (!handshakeReachedPeerCF.isDone()) { @@ -608,6 +613,12 @@ private SSLHandshakeException sslHandshakeException(final Throwable cause) { return new SSLHandshakeException("QUIC connection establishment failed", cause); } + private String message(Throwable cause) { + String message = cause.getMessage(); + if (message != null && !message.isEmpty()) return message; + return cause.toString(); + } + /** * Marks the start of a handshake. * @throws IllegalStateException If handshake has already started diff --git a/test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java b/test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java new file mode 100644 index 0000000000000..27a18daf0e5f5 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; +import java.util.logging.Handler; + +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.internal.net.http.quic.QuicConnectionImpl; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import static java.net.http.HttpClient.Builder.NO_PROXY; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.testng.Assert.*; + +/* + * @test + * @bug 8371471 + * @summary Verify that HTTP/3 handshake failures are logged if + * logging errors is enabled + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @run testng/othervm + * -Djdk.httpclient.HttpClient.log=errors + * H3LogHandshakeErrors + */ +// -Djava.security.debug=all +public class H3LogHandshakeErrors implements HttpServerAdapters { + + private SSLContext sslContext; + private HttpTestServer h3Server; + private String requestURI; + + @BeforeClass + public void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + // create an H3 only server + h3Server = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3Server.addHandler((exchange) -> exchange.sendResponseHeaders(200, 0), "/hello"); + h3Server.start(); + System.out.println("Server started at " + h3Server.getAddress()); + requestURI = "https://" + h3Server.serverAuthority() + "/hello"; + } + + @AfterClass + public void afterClass() throws Exception { + if (h3Server != null) { + System.out.println("Stopping server " + h3Server.getAddress()); + h3Server.stop(); + } + } + + static String format(LogRecord record) { + String thrown = Optional.ofNullable(record.getThrown()) + .map(t -> ": " + t).orElse(""); + return "\n \"%s: %s %s: %s%s\"".formatted( + record.getLevel(), + record.getSourceClassName(), + record.getSourceMethodName(), + record.getMessage(), + thrown); + } + + + /** + * Issues various HTTP3 requests and verifies the responses are received + */ + @Test + public void testErrorLogging() throws Exception { + // create a client that doesn't have the server's + // certificate + final HttpClient client = newClientBuilderForH3() + .proxy(NO_PROXY) + .build(); + final URI reqURI = new URI(requestURI); + final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(reqURI) + .version(HTTP_3); + Logger serverLogger = Logger.getLogger("jdk.httpclient.HttpClient"); + + CopyOnWriteArrayList records = new CopyOnWriteArrayList<>(); + Handler handler = new Handler() { + @Override + public void publish(LogRecord record) { + record.getSourceClassName(); + records.add(record); + } + @Override public void flush() {} + @Override public void close() { + } + }; + serverLogger.addHandler(handler); + + try { + final HttpRequest req1 = reqBuilder.copy().GET().build(); + System.out.println("Issuing request: " + req1); + final HttpResponse resp1 = client.send(req1, BodyHandlers.discarding()); + Assert.assertEquals(resp1.statusCode(), 200, "unexpected response code for GET request"); + } catch (IOException io) { + System.out.println("Got expected exception: " + io); + } finally { + LogRecord expected = null; + // this is a bit fragile and may need to be updated if the + // place where we log the exception from changes. + String expectedClassName = QuicConnectionImpl.class.getName() + + "$HandshakeFlow"; + for (var record : records) { + if (record.getLevel() != Level.INFO) continue; + if (!record.getMessage().contains("ERROR:")) continue; + if (record.getMessage().contains("client peer")) continue; + var expectedThrown = record.getThrown(); + if (expectedThrown == null) continue; + if (!record.getSourceClassName().equals(expectedClassName)) continue; + if (expectedThrown.getMessage().contains("client peer")) continue; + expected = record; + break; + } + assertNotNull(expected, "No throwable for " + + expectedClassName + " found in " + + records.stream().map(H3LogHandshakeErrors::format).toList() + + "\n "); + System.out.printf("Found expected exception: %s%n\t logged at: %s %s%n", + expected.getThrown(), + expected.getSourceClassName(), + expected.getSourceMethodName()); + } + } +} From d1d8057b2986cefcfca8d7e84c14df7546360d4f Mon Sep 17 00:00:00 2001 From: Daniel Fuchs Date: Fri, 7 Nov 2025 14:28:32 +0000 Subject: [PATCH 2/6] Fix copyright dates --- test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java b/test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java index 27a18daf0e5f5..a4adc7389f0f7 100644 --- a/test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java +++ b/test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it From e683f2edb89ecd4b500add4941da3ce5bbd43eec Mon Sep 17 00:00:00 2001 From: Daniel Fuchs Date: Fri, 7 Nov 2025 14:42:51 +0000 Subject: [PATCH 3/6] small test cleanup --- .../httpclient/http3/H3LogHandshakeErrors.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java b/test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java index a4adc7389f0f7..4b5ffe41e5051 100644 --- a/test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java +++ b/test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java @@ -25,9 +25,9 @@ import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; -import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; +import java.util.Arrays; import java.util.Optional; import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Level; @@ -46,12 +46,8 @@ import org.testng.annotations.Test; import static java.net.http.HttpClient.Builder.NO_PROXY; -import static java.net.http.HttpClient.Version.HTTP_2; import static java.net.http.HttpClient.Version.HTTP_3; -import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; -import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; -import static java.net.http.HttpOption.H3_DISCOVERY; import static org.testng.Assert.*; /* @@ -106,12 +102,19 @@ static String format(LogRecord record) { thrown); } - /** - * Issues various HTTP3 requests and verifies the responses are received + * Issues a GET HTTP3 requests and verifies that the + * expected exception is logged. */ @Test public void testErrorLogging() throws Exception { + // jdk.httpclient.HttpClient.log=errors must be enabled + // for this test + String logging = System.getProperty("jdk.httpclient.HttpClient.log", ""); + var categories = Arrays.asList(logging.split(",")); + assertTrue(categories.contains("errors"), + "'errors' not found in " + categories); + // create a client that doesn't have the server's // certificate final HttpClient client = newClientBuilderForH3() From 238904e4a1f7ac149334dff46aa4af16f381863d Mon Sep 17 00:00:00 2001 From: Daniel Fuchs Date: Fri, 7 Nov 2025 14:46:42 +0000 Subject: [PATCH 4/6] More copyright dates --- .../share/classes/jdk/internal/net/http/common/Log.java | 2 +- test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java index e75620e19baf5..7e7ff581351b0 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it diff --git a/test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java b/test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java index 4b5ffe41e5051..502efac02ce5d 100644 --- a/test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java +++ b/test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java @@ -114,7 +114,7 @@ public void testErrorLogging() throws Exception { var categories = Arrays.asList(logging.split(",")); assertTrue(categories.contains("errors"), "'errors' not found in " + categories); - + // create a client that doesn't have the server's // certificate final HttpClient client = newClientBuilderForH3() From ff71b9e16f46029fdc8aa363e5c7194d04043a19 Mon Sep 17 00:00:00 2001 From: Daniel Fuchs Date: Fri, 7 Nov 2025 15:25:16 +0000 Subject: [PATCH 5/6] Review feedback --- .../internal/net/http/quic/QuicConnectionImpl.java | 8 +------- .../net/httpclient/http3/H3LogHandshakeErrors.java | 12 +++++++++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java index b5ac6441ba3c5..9a280c3a8a582 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java @@ -593,7 +593,7 @@ public void failHandshakeCFs(final Throwable cause) { sslHandshakeException = sslHandshakeException(cause); if (Log.errors()) { Log.logError("%s QUIC handshake failed: %s" - .formatted(logTag(), message(cause))); + .formatted(logTag(), cause)); Log.logError(cause); } handshakeCF.completeExceptionally(sslHandshakeException); @@ -613,12 +613,6 @@ private SSLHandshakeException sslHandshakeException(final Throwable cause) { return new SSLHandshakeException("QUIC connection establishment failed", cause); } - private String message(Throwable cause) { - String message = cause.getMessage(); - if (message != null && !message.isEmpty()) return message; - return cause.toString(); - } - /** * Marks the start of a handshake. * @throws IllegalStateException If handshake has already started diff --git a/test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java b/test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java index 502efac02ce5d..710cdd391f139 100644 --- a/test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java +++ b/test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java @@ -68,6 +68,7 @@ public class H3LogHandshakeErrors implements HttpServerAdapters { private SSLContext sslContext; private HttpTestServer h3Server; private String requestURI; + private static Logger clientLogger; @BeforeClass public void beforeClass() throws Exception { @@ -115,7 +116,8 @@ public void testErrorLogging() throws Exception { assertTrue(categories.contains("errors"), "'errors' not found in " + categories); - // create a client that doesn't have the server's + // create a client without the test specific SSLContext + // so that the client doesn't have the server's // certificate final HttpClient client = newClientBuilderForH3() .proxy(NO_PROXY) @@ -123,12 +125,16 @@ public void testErrorLogging() throws Exception { final URI reqURI = new URI(requestURI); final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(reqURI) .version(HTTP_3); - Logger serverLogger = Logger.getLogger("jdk.httpclient.HttpClient"); + clientLogger = Logger.getLogger("jdk.httpclient.HttpClient"); CopyOnWriteArrayList records = new CopyOnWriteArrayList<>(); Handler handler = new Handler() { @Override public void publish(LogRecord record) { + // forces the LogRecord to evaluate the caller + // while in the publish() method to make sure + // the source class name and source method name + // are correctly evaluated. record.getSourceClassName(); records.add(record); } @@ -136,7 +142,7 @@ public void publish(LogRecord record) { @Override public void close() { } }; - serverLogger.addHandler(handler); + clientLogger.addHandler(handler); try { final HttpRequest req1 = reqBuilder.copy().GET().build(); From b99dd15ff2e75ce3bcce49814356f8d0c1edb2d0 Mon Sep 17 00:00:00 2001 From: Daniel Fuchs Date: Fri, 7 Nov 2025 17:36:31 +0000 Subject: [PATCH 6/6] Review feedback - request should fail --- .../http3/H3LogHandshakeErrors.java | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java b/test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java index 710cdd391f139..32872f4fb120b 100644 --- a/test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java +++ b/test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java @@ -22,8 +22,12 @@ */ import java.io.IOException; +import java.net.BindException; +import java.net.ServerSocket; +import java.net.Socket; import java.net.URI; import java.net.http.HttpClient; +import java.net.http.HttpOption; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; @@ -47,6 +51,7 @@ import static java.net.http.HttpClient.Builder.NO_PROXY; import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; import static org.testng.Assert.*; @@ -67,6 +72,8 @@ public class H3LogHandshakeErrors implements HttpServerAdapters { private SSLContext sslContext; private HttpTestServer h3Server; + private ServerSocket tcpServerSocket = null; + private Thread tcpServerThread = null; private String requestURI; private static Logger clientLogger; @@ -82,6 +89,32 @@ public void beforeClass() throws Exception { h3Server.start(); System.out.println("Server started at " + h3Server.getAddress()); requestURI = "https://" + h3Server.serverAuthority() + "/hello"; + + // Attempts to bind a TCP server socket on the same port + // That server will just accept and immediately close + // any connection. This is to make sure that we won't target + // another server when falling back to TCP. + tcpServerSocket = new ServerSocket(); + try { + tcpServerSocket.bind(h3Server.getAddress()); + tcpServerThread = Thread.ofPlatform().daemon().start(() -> { + System.out.println("tcpServerThread started"); + while (true) { + try (var accepted = tcpServerSocket.accept()) { + // close immediately + } catch (IOException x) { + System.out.println("tcpServerSocket: " + x); + break; + } + } + System.out.println("tcpServerThread stopped"); + }); + } catch (BindException x) { + tcpServerSocket.close(); + // if tcpServerSocket is null we will use + // HTTP3_URI_ONLY for the request + tcpServerSocket = null; + } } @AfterClass @@ -90,6 +123,10 @@ public void afterClass() throws Exception { System.out.println("Stopping server " + h3Server.getAddress()); h3Server.stop(); } + if (tcpServerSocket != null) { + tcpServerSocket.close(); + tcpServerThread.join(); + } } static String format(LogRecord record) { @@ -125,6 +162,10 @@ public void testErrorLogging() throws Exception { final URI reqURI = new URI(requestURI); final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(reqURI) .version(HTTP_3); + if (tcpServerSocket == null) { + // could not open ServerSocket on same port, only use HTTP/3 + reqBuilder.setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } clientLogger = Logger.getLogger("jdk.httpclient.HttpClient"); CopyOnWriteArrayList records = new CopyOnWriteArrayList<>(); @@ -143,12 +184,11 @@ public void publish(LogRecord record) { } }; clientLogger.addHandler(handler); - try { final HttpRequest req1 = reqBuilder.copy().GET().build(); System.out.println("Issuing request: " + req1); final HttpResponse resp1 = client.send(req1, BodyHandlers.discarding()); - Assert.assertEquals(resp1.statusCode(), 200, "unexpected response code for GET request"); + fail("Unexpected response from server: " + resp1); } catch (IOException io) { System.out.println("Got expected exception: " + io); } finally {