java-topology/test/jdk/java/net/httpclient/http3/StreamLimitTest.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

266 lines
14 KiB
Java

/*
* Copyright (c) 2023, 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.io.OutputStream;
import java.net.SocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.atomic.AtomicInteger;
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.httpclient.test.lib.http3.Http3TestServer;
import jdk.httpclient.test.lib.quic.QuicServer;
import jdk.httpclient.test.lib.quic.QuicServerConnection;
import jdk.internal.net.http.quic.QuicTransportParameters;
import jdk.internal.net.http.quic.QuicTransportParameters.ParameterId;
import jdk.test.lib.net.SimpleSSLContext;
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 org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
/*
* @test
* @summary verifies that when the Quic stream limit is reached
* then HTTP3 requests are retried on newer connection.
* This test uses an HTTP/3 only test server, which is
* configured to allow the test to control when a new
* MAX_STREAMS frames is sent to the client.
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.test.lib.net.SimpleSSLContext
* jdk.httpclient.test.lib.common.HttpServerAdapters
* jdk.httpclient.test.lib.http3.Http3TestServer
* @run junit/othervm -Djdk.internal.httpclient.debug=true StreamLimitTest
*/
public class StreamLimitTest {
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
private static HttpTestServer server;
private static QuicServer quicServer;
private static URI requestURI;
private static volatile QuicServerConnection latestServerConn;
private static final class Handler implements HttpTestHandler {
@Override
public void handle(HttpTestExchange exchange) throws IOException {
final String handledBy = latestServerConn.logTag();
System.out.println(handledBy + " handling request " + exchange.getRequestURI());
final byte[] respBody;
if (handledBy == null) {
respBody = new byte[0];
} else {
respBody = handledBy.getBytes(StandardCharsets.UTF_8);
}
exchange.sendResponseHeaders(200, respBody.length == 0 ? -1 : respBody.length);
// write out the server's connection id as a response
if (respBody.length > 0) {
try (OutputStream os = exchange.getResponseBody()) {
os.write(respBody);
}
}
exchange.close();
}
}
@BeforeAll
public static void beforeClass() throws Exception {
quicServer = Http3TestServer.quicServerBuilder().sslContext(sslContext).build();
final Http3TestServer h3Server = new Http3TestServer(quicServer) {
@Override
public boolean acceptIncoming(SocketAddress source, QuicServerConnection quicConn) {
final boolean accepted = super.acceptIncoming(source, quicConn);
if (accepted) {
// keep track of the latest server connection
latestServerConn = quicConn;
}
return accepted;
}
};
server = HttpTestServer.of(h3Server);
server.addHandler(new Handler(), "/foo");
server.start();
System.out.println("Server started at " + server.serverAuthority());
requestURI = new URI("https://" + server.serverAuthority() + "/foo");
}
@AfterAll
public static void afterClass() throws Exception {
latestServerConn = null;
if (server != null) {
server.stop();
}
}
/**
* Configures different limits for max bidi stream creation by HTTP client and then verifies
* the expected behaviour by sending HTTP3 requests
*/
@Test
public void testBidiMaxStreamLimit() throws Exception {
final QuicTransportParameters transportParameters = new QuicTransportParameters();
final int intialMaxStreamLimit = 3;
final AtomicInteger maxStreamLimit = new AtomicInteger(intialMaxStreamLimit);
transportParameters.setIntParameter(ParameterId.initial_max_streams_bidi,
maxStreamLimit.get());
// set the limit so that any new server connections created with advertise this limit
// to the peer (client)
quicServer.setTransportParameters(transportParameters);
// also set a MAX_STREAMS limit computer for this server so that the created
// connections use this computer for deciding MAX_STREAMS limit
quicServer.setMaxStreamLimitComputer((ignore) -> maxStreamLimit.longValue());
final HttpClient client = HttpServerAdapters.createClientBuilderForH3()
.proxy(NO_PROXY)
.sslContext(sslContext)
.build();
final HttpRequest req = HttpRequest.newBuilder().version(HTTP_3)
.GET().uri(requestURI)
.setOption(H3_DISCOVERY, server.h3DiscoveryConfig())
.build();
String requestHandledBy = null;
System.out.println("Server has been configured with a limit for max" +
" bidi streams: " + intialMaxStreamLimit);
// issue N number of requests where N == the max bidi stream creation limit that is
// advertised to the peer. All these N requests are expected to be handled by the same
// server connection
for (int i = 1; i <= intialMaxStreamLimit; i++) {
System.out.println("Sending request " + i + " to " + requestURI);
final HttpResponse<String> resp = client.send(req,
HttpResponse.BodyHandlers.ofString());
Assertions.assertEquals(HTTP_3, resp.version(), "Unexpected response version");
Assertions.assertEquals(200, resp.statusCode(), "Unexpected response code");
final String respBody = resp.body();
System.out.println("Request " + i + " was handled by server connection: " + respBody);
if (i == 1) {
// first request; keep track the server connection id which responded
// to this request
requestHandledBy = respBody;
} else {
Assertions.assertEquals(requestHandledBy, respBody, "Request was handled by an" +
" unexpected server connection");
}
}
// at this point the limit for bidi stream creation has reached on the client.
// now issue a request so that:
// - the HTTP client implementation notices that it has hit the limit
// - HTTP client sends a STREAMS_BLOCKED frame and waits for a while (timeout derived
// internally based on request timeout) for server to increase the limit. But this server
// connection will not send any MAX_STREAMS frame upon receipt of STREAMS_BLOCKED frame
// - client notices that server connection hasn't increased the stream limit, so internally
// retries the request, which should trigger a new server connection and thus this
// request should be handled by a different server connection than the last N requests
final HttpRequest reqWithTimeout = HttpRequest.newBuilder().version(HTTP_3)
.GET().uri(requestURI)
.setOption(H3_DISCOVERY, server.h3DiscoveryConfig())
.timeout(Duration.of(10, ChronoUnit.SECONDS))
.build();
for (int i = 1; i <= intialMaxStreamLimit; i++) {
System.out.println("Sending request " + i + " (configured with timeout) to "
+ requestURI);
final HttpResponse<String> resp = client.send(reqWithTimeout,
HttpResponse.BodyHandlers.ofString());
Assertions.assertEquals(HTTP_3, resp.version(), "Unexpected response version");
Assertions.assertEquals(200, resp.statusCode(), "Unexpected response code");
final String respBody = resp.body();
System.out.println("Request " + i + " was handled by server connection: " + respBody);
if (i == 1) {
// first request after the limit was hit.
// verify that it was handled by a new connection and not the one that handled
// the previous N requests
Assertions.assertNotEquals(requestHandledBy, respBody, "Request was expected to be" +
" handled by a new server connection, but wasn't");
// keep track this new server connection id which responded to this request
requestHandledBy = respBody;
} else {
Assertions.assertEquals(requestHandledBy, respBody, "Request was handled by an" +
" unexpected server connection");
}
}
// at this point the limit for bidi stream creation has reached on the client, for
// this new server connection. we now configure this current server connection to
// increment the limit to new higher limit
maxStreamLimit.set(intialMaxStreamLimit + 2);
System.out.println("Server connection " + latestServerConn + " has now been configured to" +
" increment the bidi stream creation limit to " + maxStreamLimit.get());
// we now issue new requests, with timeout specified
// - the HTTP client implementation notices that it has hit the limit
// - HTTP client sends a STREAMS_BLOCKED frame and waits for a while (timeout derived
// internally based on request timeout) for server to increase the limit. This server
// connection, since it is configured to increment the stream limit, will send
// MAX_STREAMS frame upon receipt of STREAMS_BLOCKED frame
// - client receives the MAX_STREAMS frame (hopefully within the timeout) and
// notices that server connection has increased the stream limit, so opens a new bidi
// stream and lets the request move forward (on the same server connection)
final int numNewRequests = maxStreamLimit.get() - intialMaxStreamLimit;
for (int i = 1; i <= numNewRequests; i++) {
System.out.println("Sending request " + i + " (after stream limit has been increased)" +
" to " + requestURI);
final HttpResponse<String> resp = client.send(reqWithTimeout,
HttpResponse.BodyHandlers.ofString());
Assertions.assertEquals(HTTP_3, resp.version(), "Unexpected response version");
Assertions.assertEquals(200, resp.statusCode(), "Unexpected response code");
final String respBody = resp.body();
System.out.println("Request " + i + " was handled by server connection: " + respBody);
// all these requests should be handled by the same server connection which handled
// the previous requests
Assertions.assertEquals(requestHandledBy, respBody, "Request was handled by an" +
" unexpected server connection");
}
// at this point the newer limit for bidi stream creation has reached on the client.
// we now issue a new request without any timeout configured on the request, so that the
// client internally just immediately retries and uses a different connection on noticing
// that the stream creation limit for the current server connection has been reached.
System.out.println("Server connection " + latestServerConn + " has now been configured to" +
" not increase max stream limit for bidi streams created by the client");
final HttpRequest finalReq = HttpRequest.newBuilder()
.version(HTTP_3)
.setOption(H3_DISCOVERY, server.h3DiscoveryConfig())
.GET().uri(requestURI)
.build();
System.out.println("Sending request, without timeout, to " + requestURI);
final HttpResponse<String> finalResp = client.send(finalReq,
HttpResponse.BodyHandlers.ofString());
Assertions.assertEquals(HTTP_3, finalResp.version(), "Unexpected response version");
Assertions.assertEquals(200, finalResp.statusCode(), "Unexpected response code");
final String finalRespBody = finalResp.body();
System.out.println("Request was handled by server connection: " + finalRespBody);
// this request should have been handled by a new server connection
Assertions.assertNotEquals(requestHandledBy, finalRespBody, "Request was handled by an" +
" unexpected server connection");
}
}