java-topology/test/jdk/java/net/httpclient/http3/H3IdleExceedsQuicIdleTimeout.java
russell@unturf.com 0a580b313d undefect. CWE-407 — 63 sites patched across 27 ecosystems
Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com

Patches, unit tests, benchmarks, whitepaper, and outreach briefs.
Public domain — no copyright claimed. Use freely.
2026-03-26 17:11:57 -04:00

178 lines
8.3 KiB
Java

/*
* Copyright (c) 2025, 2026, 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.HttpOption;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import jdk.httpclient.test.lib.common.HttpServerAdapters;
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestExchange;
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestHandler;
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer;
import jdk.test.lib.net.SimpleSSLContext;
import jdk.test.lib.net.URIBuilder;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.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.Http3DiscoveryMode.HTTP_3_URI_ONLY;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/*
* @test
* @bug 8371802
* @summary verify that if a higher idle timeout is configured for a HTTP/3 connection
* then a lower negotiated QUIC idle timeout doesn't cause QUIC to
* idle terminate the connection
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.httpclient.test.lib.common.HttpServerAdapters
* jdk.test.lib.net.SimpleSSLContext
*
* @comment this test has some explicit delays to simulate a idle connection, the timeout=180
* is merely to provide some leeway to the test and prevent timing out the test on busy
* systems
* @run junit/othervm/timeout=180 -Djdk.httpclient.quic.idleTimeout=30
* -Djdk.httpclient.keepalive.timeout.h3=120
* -Djdk.httpclient.HttpClient.log=quic:hs
* ${test.main.class}
*/
class H3IdleExceedsQuicIdleTimeout {
private static final String REQ_PATH = "/8371802";
private static HttpTestServer h3Server;
private static final SSLContext sslCtx = SimpleSSLContext.findSSLContext();
@BeforeAll
static void beforeAll() throws Exception {
h3Server = HttpTestServer.create(HTTP_3_URI_ONLY, sslCtx);
h3Server.addHandler(new Handler(), REQ_PATH);
h3Server.start();
System.err.println("HTTP/3 server started at " + h3Server.getAddress());
}
@AfterAll
static void afterAll() throws Exception {
if (h3Server != null) {
System.err.println("stopping server at " + h3Server.getAddress());
h3Server.stop();
}
}
/*
* With QUIC idle connection timeout configured to be lower than the HTTP/3 idle timeout,
* this test issues a HTTP/3 request and expects that request to establish a QUIC connection
* and receive a successful response. The test then stays idle for a duration larger
* than the QUIC idle timeout and issues a HTTP/3 request again after that idle period.
* The test then expects that the second request too is responded by the previously opened
* connection, thus proving that the QUIC layer did the necessary work to prevent the (idle)
* connection from terminating.
*/
@Test
void testQUICKeepsConnAlive() throws Exception {
final long quicIdleTimeoutSecs = 30;
assertEquals(quicIdleTimeoutSecs,
Integer.parseInt(System.getProperty("jdk.httpclient.quic.idleTimeout")),
"unexpected QUIC idle timeout");
assertEquals(120,
Integer.parseInt(System.getProperty("jdk.httpclient.keepalive.timeout.h3")),
"unexpected HTTP/3 idle timeout");
try (final HttpClient client = HttpServerAdapters.createClientBuilderForH3()
.sslContext(sslCtx)
.proxy(NO_PROXY)
.version(HTTP_3)
.build()) {
final URI req1URI = URIBuilder.newBuilder()
.scheme("https")
.host(h3Server.getAddress().getAddress())
.port(h3Server.getAddress().getPort())
.path(REQ_PATH)
.query("i=1")
.build();
System.err.println("issuing request " + req1URI);
final HttpRequest req1 = HttpRequest.newBuilder()
.uri(req1URI)
.setOption(HttpOption.H3_DISCOVERY, HTTP_3_URI_ONLY)
.build();
final HttpResponse<Void> resp1 = client.send(req1, BodyHandlers.discarding());
assertEquals(200, resp1.statusCode(), "unexpected status code");
final String resp1ConnLabel = resp1.connectionLabel().orElse(null);
System.err.println("first request handled by connection: " + resp1ConnLabel);
assertNotNull(resp1ConnLabel, "missing connection label on response");
assertEquals(HTTP_3, resp1.version(), "unexpected response version");
// don't generate any more traffic from the HTTP/3 side for longer than the QUIC
// idle timeout
stayIdle(quicIdleTimeoutSecs + 13, TimeUnit.SECONDS);
// now send the HTTP/3 request and expect the same previous connection to handle
// respond to this request
final URI req2URI = URIBuilder.newBuilder()
.scheme("https")
.host(h3Server.getAddress().getAddress())
.port(h3Server.getAddress().getPort())
.path(REQ_PATH)
.query("i=2")
.build();
System.err.println("issuing request " + req2URI);
final HttpRequest req2 = HttpRequest.newBuilder()
.uri(req2URI)
.setOption(HttpOption.H3_DISCOVERY, HTTP_3_URI_ONLY)
.build();
final HttpResponse<Void> resp2 = client.send(req2, BodyHandlers.discarding());
assertEquals(200, resp2.statusCode(), "unexpected status code");
final String resp2ConnLabel = resp2.connectionLabel().orElse(null);
System.err.println("second request handled by connection: " + resp2ConnLabel);
assertEquals(resp1ConnLabel, resp2ConnLabel, "second request handled by a different connection");
assertEquals(HTTP_3, resp2.version(), "unexpected response version");
}
}
private static void stayIdle(final long time, final TimeUnit unit) throws InterruptedException {
// await on a CountDownLatch which no one counts down. this is merely
// to avoid using Thread.sleep(...) and other similar constructs and then
// having to deal with spurious wakeups.
final boolean countedDown = new CountDownLatch(1).await(time, unit);
assertFalse(countedDown, "wasn't expected to be counted down");
}
private static final class Handler implements HttpTestHandler {
private static final int NO_RESP_BODY = 0;
@Override
public void handle(final HttpTestExchange exchange) throws IOException {
System.err.println("handling request " + exchange.getRequestURI());
exchange.sendResponseHeaders(200, NO_RESP_BODY);
}
}
}