Skip to content

Commit 8a911ae

Browse files
committed
8371471: HttpClient: Log HTTP/3 handshake failures if logging errors is enabled
Reviewed-by: djelinski, jpai
1 parent 1327aa6 commit 8a911ae

File tree

3 files changed

+228
-3
lines changed

3 files changed

+228
-3
lines changed

src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* 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) {
391391

392392
public static void logError(Throwable t) {
393393
if (errors()) {
394-
String s = Utils.stackTrace(t);
395-
logger.log(Level.INFO, "ERROR: " + s);
394+
logger.log(Level.INFO, "ERROR: " + t, t);
396395
}
397396
}
398397

src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,11 @@ public void failHandshakeCFs(final Throwable cause) {
591591
SSLHandshakeException sslHandshakeException = null;
592592
if (!handshakeCF.isDone()) {
593593
sslHandshakeException = sslHandshakeException(cause);
594+
if (Log.errors()) {
595+
Log.logError("%s QUIC handshake failed: %s"
596+
.formatted(logTag(), cause));
597+
Log.logError(cause);
598+
}
594599
handshakeCF.completeExceptionally(sslHandshakeException);
595600
}
596601
if (!handshakeReachedPeerCF.isDone()) {
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
/*
2+
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
import java.io.IOException;
25+
import java.net.BindException;
26+
import java.net.ServerSocket;
27+
import java.net.Socket;
28+
import java.net.URI;
29+
import java.net.http.HttpClient;
30+
import java.net.http.HttpOption;
31+
import java.net.http.HttpRequest;
32+
import java.net.http.HttpResponse;
33+
import java.net.http.HttpResponse.BodyHandlers;
34+
import java.util.Arrays;
35+
import java.util.Optional;
36+
import java.util.concurrent.CopyOnWriteArrayList;
37+
import java.util.logging.Level;
38+
import java.util.logging.LogRecord;
39+
import java.util.logging.Logger;
40+
import java.util.logging.Handler;
41+
42+
import javax.net.ssl.SSLContext;
43+
44+
import jdk.httpclient.test.lib.common.HttpServerAdapters;
45+
import jdk.internal.net.http.quic.QuicConnectionImpl;
46+
import jdk.test.lib.net.SimpleSSLContext;
47+
import org.testng.Assert;
48+
import org.testng.annotations.AfterClass;
49+
import org.testng.annotations.BeforeClass;
50+
import org.testng.annotations.Test;
51+
52+
import static java.net.http.HttpClient.Builder.NO_PROXY;
53+
import static java.net.http.HttpClient.Version.HTTP_3;
54+
import static java.net.http.HttpOption.H3_DISCOVERY;
55+
import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY;
56+
import static org.testng.Assert.*;
57+
58+
/*
59+
* @test
60+
* @bug 8371471
61+
* @summary Verify that HTTP/3 handshake failures are logged if
62+
* logging errors is enabled
63+
* @library /test/lib /test/jdk/java/net/httpclient/lib
64+
* @build jdk.test.lib.net.SimpleSSLContext
65+
* jdk.httpclient.test.lib.common.HttpServerAdapters
66+
* @run testng/othervm
67+
* -Djdk.httpclient.HttpClient.log=errors
68+
* H3LogHandshakeErrors
69+
*/
70+
// -Djava.security.debug=all
71+
public class H3LogHandshakeErrors implements HttpServerAdapters {
72+
73+
private SSLContext sslContext;
74+
private HttpTestServer h3Server;
75+
private ServerSocket tcpServerSocket = null;
76+
private Thread tcpServerThread = null;
77+
private String requestURI;
78+
private static Logger clientLogger;
79+
80+
@BeforeClass
81+
public void beforeClass() throws Exception {
82+
sslContext = new SimpleSSLContext().get();
83+
if (sslContext == null) {
84+
throw new AssertionError("Unexpected null sslContext");
85+
}
86+
// create an H3 only server
87+
h3Server = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext);
88+
h3Server.addHandler((exchange) -> exchange.sendResponseHeaders(200, 0), "/hello");
89+
h3Server.start();
90+
System.out.println("Server started at " + h3Server.getAddress());
91+
requestURI = "https://" + h3Server.serverAuthority() + "/hello";
92+
93+
// Attempts to bind a TCP server socket on the same port
94+
// That server will just accept and immediately close
95+
// any connection. This is to make sure that we won't target
96+
// another server when falling back to TCP.
97+
tcpServerSocket = new ServerSocket();
98+
try {
99+
tcpServerSocket.bind(h3Server.getAddress());
100+
tcpServerThread = Thread.ofPlatform().daemon().start(() -> {
101+
System.out.println("tcpServerThread started");
102+
while (true) {
103+
try (var accepted = tcpServerSocket.accept()) {
104+
// close immediately
105+
} catch (IOException x) {
106+
System.out.println("tcpServerSocket: " + x);
107+
break;
108+
}
109+
}
110+
System.out.println("tcpServerThread stopped");
111+
});
112+
} catch (BindException x) {
113+
tcpServerSocket.close();
114+
// if tcpServerSocket is null we will use
115+
// HTTP3_URI_ONLY for the request
116+
tcpServerSocket = null;
117+
}
118+
}
119+
120+
@AfterClass
121+
public void afterClass() throws Exception {
122+
if (h3Server != null) {
123+
System.out.println("Stopping server " + h3Server.getAddress());
124+
h3Server.stop();
125+
}
126+
if (tcpServerSocket != null) {
127+
tcpServerSocket.close();
128+
tcpServerThread.join();
129+
}
130+
}
131+
132+
static String format(LogRecord record) {
133+
String thrown = Optional.ofNullable(record.getThrown())
134+
.map(t -> ": " + t).orElse("");
135+
return "\n \"%s: %s %s: %s%s\"".formatted(
136+
record.getLevel(),
137+
record.getSourceClassName(),
138+
record.getSourceMethodName(),
139+
record.getMessage(),
140+
thrown);
141+
}
142+
143+
/**
144+
* Issues a GET HTTP3 requests and verifies that the
145+
* expected exception is logged.
146+
*/
147+
@Test
148+
public void testErrorLogging() throws Exception {
149+
// jdk.httpclient.HttpClient.log=errors must be enabled
150+
// for this test
151+
String logging = System.getProperty("jdk.httpclient.HttpClient.log", "");
152+
var categories = Arrays.asList(logging.split(","));
153+
assertTrue(categories.contains("errors"),
154+
"'errors' not found in " + categories);
155+
156+
// create a client without the test specific SSLContext
157+
// so that the client doesn't have the server's
158+
// certificate
159+
final HttpClient client = newClientBuilderForH3()
160+
.proxy(NO_PROXY)
161+
.build();
162+
final URI reqURI = new URI(requestURI);
163+
final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(reqURI)
164+
.version(HTTP_3);
165+
if (tcpServerSocket == null) {
166+
// could not open ServerSocket on same port, only use HTTP/3
167+
reqBuilder.setOption(H3_DISCOVERY, HTTP_3_URI_ONLY);
168+
}
169+
clientLogger = Logger.getLogger("jdk.httpclient.HttpClient");
170+
171+
CopyOnWriteArrayList<LogRecord> records = new CopyOnWriteArrayList<>();
172+
Handler handler = new Handler() {
173+
@Override
174+
public void publish(LogRecord record) {
175+
// forces the LogRecord to evaluate the caller
176+
// while in the publish() method to make sure
177+
// the source class name and source method name
178+
// are correctly evaluated.
179+
record.getSourceClassName();
180+
records.add(record);
181+
}
182+
@Override public void flush() {}
183+
@Override public void close() {
184+
}
185+
};
186+
clientLogger.addHandler(handler);
187+
try {
188+
final HttpRequest req1 = reqBuilder.copy().GET().build();
189+
System.out.println("Issuing request: " + req1);
190+
final HttpResponse<Void> resp1 = client.send(req1, BodyHandlers.discarding());
191+
fail("Unexpected response from server: " + resp1);
192+
} catch (IOException io) {
193+
System.out.println("Got expected exception: " + io);
194+
} finally {
195+
LogRecord expected = null;
196+
// this is a bit fragile and may need to be updated if the
197+
// place where we log the exception from changes.
198+
String expectedClassName = QuicConnectionImpl.class.getName()
199+
+ "$HandshakeFlow";
200+
for (var record : records) {
201+
if (record.getLevel() != Level.INFO) continue;
202+
if (!record.getMessage().contains("ERROR:")) continue;
203+
if (record.getMessage().contains("client peer")) continue;
204+
var expectedThrown = record.getThrown();
205+
if (expectedThrown == null) continue;
206+
if (!record.getSourceClassName().equals(expectedClassName)) continue;
207+
if (expectedThrown.getMessage().contains("client peer")) continue;
208+
expected = record;
209+
break;
210+
}
211+
assertNotNull(expected, "No throwable for "
212+
+ expectedClassName + " found in "
213+
+ records.stream().map(H3LogHandshakeErrors::format).toList()
214+
+ "\n ");
215+
System.out.printf("Found expected exception: %s%n\t logged at: %s %s%n",
216+
expected.getThrown(),
217+
expected.getSourceClassName(),
218+
expected.getSourceMethodName());
219+
}
220+
}
221+
}

0 commit comments

Comments
 (0)