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..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 @@ -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..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 @@ -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(), cause)); + Log.logError(cause); + } handshakeCF.completeExceptionally(sslHandshakeException); } if (!handshakeReachedPeerCF.isDone()) { 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..32872f4fb120b --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3LogHandshakeErrors.java @@ -0,0 +1,221 @@ +/* + * 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 + * 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.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; +import java.util.Arrays; +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_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.*; + +/* + * @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 ServerSocket tcpServerSocket = null; + private Thread tcpServerThread = null; + private String requestURI; + private static Logger clientLogger; + + @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"; + + // 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 + public void afterClass() throws Exception { + if (h3Server != null) { + System.out.println("Stopping server " + h3Server.getAddress()); + h3Server.stop(); + } + if (tcpServerSocket != null) { + tcpServerSocket.close(); + tcpServerThread.join(); + } + } + + 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 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 without the test specific SSLContext + // so that the client 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); + 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<>(); + 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); + } + @Override public void flush() {} + @Override public void close() { + } + }; + 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()); + fail("Unexpected response from server: " + resp1); + } 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()); + } + } +}