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.
This commit is contained in:
russell@unturf.com 2026-03-26 17:11:57 -04:00
commit 0a580b313d
70422 changed files with 17213626 additions and 0 deletions

View file

@ -0,0 +1,334 @@
/*
* Copyright (c) 2018, 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.
*/
/*
* @test
* @bug 8303965 8354276
* @summary This test verifies the behaviour of the HttpClient when presented
* with a HEADERS frame followed by CONTINUATION frames, and when presented
* with bad header fields.
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext
* @run junit/othervm -Djdk.internal.httpclient.debug=true BadHeadersTest
*/
import jdk.internal.net.http.common.HttpHeadersBuilder;
import jdk.internal.net.http.frame.ContinuationFrame;
import jdk.internal.net.http.frame.HeaderFrame;
import jdk.internal.net.http.frame.HeadersFrame;
import jdk.internal.net.http.frame.Http2Frame;
import jdk.test.lib.net.SimpleSSLContext;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ProtocolException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.ExecutionException;
import java.util.function.BiFunction;
import jdk.httpclient.test.lib.http2.Http2TestServer;
import jdk.httpclient.test.lib.http2.Http2TestExchange;
import jdk.httpclient.test.lib.http2.Http2TestExchangeImpl;
import jdk.httpclient.test.lib.http2.Http2Handler;
import jdk.httpclient.test.lib.http2.BodyOutputStream;
import jdk.httpclient.test.lib.http2.Http2TestServerConnection;
import static java.util.List.of;
import static java.util.Map.entry;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
// Code copied from ContinuationFrameTest
public class BadHeadersTest {
private static final List<List<Entry<String, String>>> BAD_HEADERS = of(
of(entry(":status", "200"), entry(":hello", "GET")), // Unknown pseudo-header
of(entry(":status", "200"), entry("hell o", "value")), // Space in the name
of(entry(":status", "200"), entry("hello", "line1\r\n line2\r\n")), // Multiline value
of(entry(":status", "200"), entry("hello", "DE" + ((char) 0x7F) + "L")), // Bad byte in value
of(entry(":status", "200"), entry("connection", "close")), // Prohibited connection-specific header
of(entry(":status", "200"), entry(":scheme", "https")), // Request pseudo-header in response
of(entry("hello", "world!"), entry(":status", "200")) // Pseudo header is not the first one
);
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
private static Http2TestServer http2TestServer; // HTTP/2 ( h2c )
private static Http2TestServer https2TestServer; // HTTP/2 ( h2 )
private static String http2URI;
private static String https2URI;
/**
* A function that returns a list of 1) one HEADERS frame ( with an empty
* payload ), and 2) a CONTINUATION frame with the actual headers.
*/
static BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> oneContinuation =
(Integer streamid, List<ByteBuffer> encodedHeaders) -> {
List<ByteBuffer> empty = of(ByteBuffer.wrap(new byte[0]));
HeadersFrame hf = new HeadersFrame(streamid, 0, empty);
ContinuationFrame cf = new ContinuationFrame(streamid,
HeaderFrame.END_HEADERS,
encodedHeaders);
return of(hf, cf);
};
/**
* A function that returns a list of one HEADERS frame followed by a number of
* CONTINUATION frames. Each frame contains just a single byte of payload.
*/
static BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> byteAtATime =
(Integer streamid, List<ByteBuffer> encodedHeaders) -> {
assert encodedHeaders.get(0).hasRemaining();
List<Http2Frame> frames = new ArrayList<>();
ByteBuffer hb = ByteBuffer.wrap(new byte[] {encodedHeaders.get(0).get()});
HeadersFrame hf = new HeadersFrame(streamid, 0, hb);
frames.add(hf);
for (ByteBuffer bb : encodedHeaders) {
while (bb.hasRemaining()) {
List<ByteBuffer> data = of(ByteBuffer.wrap(new byte[] {bb.get()}));
ContinuationFrame cf = new ContinuationFrame(streamid, 0, data);
frames.add(cf);
}
}
frames.get(frames.size() - 1).setFlag(HeaderFrame.END_HEADERS);
return frames;
};
static Object[][] variants() {
return new Object[][] {
{ http2URI, false, oneContinuation },
{ https2URI, false, oneContinuation },
{ http2URI, true, oneContinuation },
{ https2URI, true, oneContinuation },
{ http2URI, false, byteAtATime },
{ https2URI, false, byteAtATime },
{ http2URI, true, byteAtATime },
{ https2URI, true, byteAtATime },
};
}
@ParameterizedTest
@MethodSource("variants")
void test(String uri,
boolean sameClient,
BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> headerFramesSupplier)
throws Exception
{
CFTHttp2TestExchange.setHeaderFrameSupplier(headerFramesSupplier);
HttpClient client = null;
for (int i=0; i< BAD_HEADERS.size(); i++) {
if (!sameClient || client == null)
client = HttpClient.newBuilder().sslContext(sslContext).build();
URI uriWithQuery = URI.create(uri + "?BAD_HEADERS=" + i);
HttpRequest request = HttpRequest.newBuilder(uriWithQuery)
.POST(BodyPublishers.ofString("Hello there!"))
.build();
System.out.println("\nSending request:" + uriWithQuery);
final HttpClient cc = client;
try {
HttpResponse<String> response = cc.send(request, BodyHandlers.ofString());
fail("Expected exception, got :" + response + ", " + response.body());
} catch (IOException ioe) {
System.out.println("Got EXPECTED: " + ioe);
assertDetailMessage(ioe, i);
}
}
}
@ParameterizedTest
@MethodSource("variants")
void testAsync(String uri,
boolean sameClient,
BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> headerFramesSupplier)
{
CFTHttp2TestExchange.setHeaderFrameSupplier(headerFramesSupplier);
HttpClient client = null;
for (int i=0; i< BAD_HEADERS.size(); i++) {
if (!sameClient || client == null)
client = HttpClient.newBuilder().sslContext(sslContext).build();
URI uriWithQuery = URI.create(uri + "?BAD_HEADERS=" + i);
HttpRequest request = HttpRequest.newBuilder(uriWithQuery)
.POST(BodyPublishers.ofString("Hello there!"))
.build();
System.out.println("\nSending request:" + uriWithQuery);
final HttpClient cc = client;
Throwable t = null;
try {
HttpResponse<String> response = cc.sendAsync(request, BodyHandlers.ofString()).get();
fail("Expected exception, got :" + response + ", " + response.body());
} catch (Exception t0) {
System.out.println("Got EXPECTED: " + t0);
if (t0 instanceof ExecutionException) {
t = t0.getCause();
} else {
t = t0;
}
}
assertDetailMessage(t, i);
}
}
// Assertions based on implementation specific detail messages. Keep in
// sync with implementation.
static void assertDetailMessage(Throwable throwable, int iterationIndex) {
try {
assertInstanceOf(ProtocolException.class, throwable, "Expected ProtocolException, got " + throwable);
assertTrue(throwable.getMessage().contains("malformed response"),
"Expected \"malformed response\" in: " + throwable.getMessage());
if (iterationIndex == 0) { // unknown
assertTrue(throwable.getMessage().contains("Unknown pseudo-header"),
"Expected \"Unknown pseudo-header\" in: " + throwable.getMessage());
} else if (iterationIndex == 4) { // prohibited
assertTrue(throwable.getMessage().contains("Prohibited header name"),
"Expected \"Prohibited header name\" in: " + throwable.getMessage());
} else if (iterationIndex == 5) { // unexpected type
assertTrue(throwable.getMessage().contains("not valid in context"),
"Expected \"not valid in context\" in: " + throwable.getMessage());
} else if (iterationIndex == 6) { // unexpected sequence
assertTrue(throwable.getMessage().contains(" Unexpected pseudo-header"),
"Expected \" Unexpected pseudo-header\" in: " + throwable.getMessage());
} else {
assertTrue(throwable.getMessage().contains("Bad header"),
"Expected \"Bad header\" in: " + throwable.getMessage());
}
} catch (AssertionError e) {
System.out.println("Exception does not match expectation: " + throwable);
throwable.printStackTrace(System.out);
throw e;
}
}
@BeforeAll
static void setup() throws Exception {
http2TestServer = new Http2TestServer("localhost", false, 0);
http2TestServer.addHandler(new Http2EchoHandler(), "/http2/echo");
int port = http2TestServer.getAddress().getPort();
http2URI = "http://localhost:" + port + "/http2/echo";
https2TestServer = new Http2TestServer("localhost", true, sslContext);
https2TestServer.addHandler(new Http2EchoHandler(), "/https2/echo");
port = https2TestServer.getAddress().getPort();
https2URI = "https://localhost:" + port + "/https2/echo";
// Override the default exchange supplier with a custom one to enable
// particular test scenarios
http2TestServer.setExchangeSupplier(CFTHttp2TestExchange::new);
https2TestServer.setExchangeSupplier(CFTHttp2TestExchange::new);
http2TestServer.start();
https2TestServer.start();
}
@AfterAll
static void teardown() throws Exception {
http2TestServer.stop();
https2TestServer.stop();
}
static class Http2EchoHandler implements Http2Handler {
@Override
public void handle(Http2TestExchange t) throws IOException {
try (InputStream is = t.getRequestBody();
OutputStream os = t.getResponseBody()) {
byte[] bytes = is.readAllBytes();
// Note: strictly ordered response headers will be added within
// the custom sendResponseHeaders implementation, based upon the
// query parameter
t.sendResponseHeaders(200, bytes.length);
os.write(bytes);
}
}
}
// A custom Http2TestExchangeImpl that overrides sendResponseHeaders to
// allow headers to be sent with a number of CONTINUATION frames.
static class CFTHttp2TestExchange extends Http2TestExchangeImpl {
static volatile BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> headerFrameSupplier;
volatile int badHeadersIndex = -1;
static void setHeaderFrameSupplier(BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> hfs) {
headerFrameSupplier = hfs;
}
CFTHttp2TestExchange(int streamid, String method, HttpHeaders reqheaders,
HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is,
SSLSession sslSession, BodyOutputStream os,
Http2TestServerConnection conn, boolean pushAllowed) {
super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession,
os, conn, pushAllowed);
String query = uri.getQuery();
badHeadersIndex = Integer.parseInt(query.substring(query.indexOf("=") + 1));
assert badHeadersIndex >= 0 && badHeadersIndex < BAD_HEADERS.size() :
"Unexpected badHeadersIndex value: " + badHeadersIndex;
}
@Override
public void sendResponseHeaders(int rCode, long responseLength) throws IOException {
assert rspheadersBuilder.build().map().size() == 0;
assert badHeadersIndex >= 0 && badHeadersIndex < BAD_HEADERS.size() :
"Unexpected badHeadersIndex value: " + badHeadersIndex;
List<Entry<String,String>> headers = BAD_HEADERS.get(badHeadersIndex);
System.out.println("Server replying with bad headers: " + headers);
List<ByteBuffer> encodeHeaders = conn.encodeHeadersOrdered(headers);
List<Http2Frame> headerFrames = headerFrameSupplier.apply(streamid, encodeHeaders);
assert headerFrames.size() > 0; // there must always be at least 1
if (responseLength < 0) {
headerFrames.get(headerFrames.size() -1).setFlag(HeadersFrame.END_STREAM);
os.markClosed();
}
for (Http2Frame f : headerFrames) {
conn.addToOutputQ(f);
}
os.goodToGo();
System.err.println("Sent response headers " + rCode);
}
}
}

View file

@ -0,0 +1,181 @@
/*
* 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.
*/
/*
* @test
* @bug 8354276
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer
* @run junit/othervm
* -Djdk.internal.httpclient.debug=true
* -Djdk.httpclient.HttpClient.log=errors,requests,responses,trace
* BadPushPromiseTest
*/
import jdk.httpclient.test.lib.common.HttpServerAdapters;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ProtocolException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.net.http.HttpResponse.PushPromiseHandler;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import static java.net.http.HttpClient.Version.HTTP_2;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.List.of;
import static org.junit.jupiter.api.Assertions.*;
public class BadPushPromiseTest {
private static final List<Map<String, List<String>>> BAD_HEADERS = of(
Map.of(":hello", of("GET")), // Unknown pseudo-header
Map.of("hell o", of("value")), // Space in the name
Map.of("hello", of("line1\r\n line2\r\n")), // Multiline value
Map.of("hello", of("DE" + ((char) 0x7F) + "L")), // Bad byte in value
Map.of(":status", of("200")) // Response pseudo-header in request
);
static final String MAIN_RESPONSE_BODY = "the main response body";
static HttpServerAdapters.HttpTestServer server;
static URI uri;
@BeforeAll
static void setup() throws Exception {
server = HttpServerAdapters.HttpTestServer.create(HTTP_2);
HttpServerAdapters.HttpTestHandler handler = new ServerPushHandler(MAIN_RESPONSE_BODY);
server.addHandler(handler, "/");
server.start();
String authority = server.serverAuthority();
System.err.println("Server listening on address " + authority);
uri = new URI("http://" + authority + "/foo/a/b/c");
}
@AfterAll
static void teardown() {
server.stop();
}
/*
* Malformed push promise headers should kill the connection
*/
@Test
void test() {
HttpClient client = HttpClient.newHttpClient();
for (int i=0; i< BAD_HEADERS.size(); i++) {
URI uriWithQuery = URI.create(uri + "?BAD_HEADERS=" + i);
HttpRequest request = HttpRequest.newBuilder(uriWithQuery)
.build();
System.out.println("\nSending request:" + uriWithQuery);
final HttpClient cc = client;
try {
ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<String>>> promises
= new ConcurrentHashMap<>();
PushPromiseHandler<String> pph = PushPromiseHandler
.of((r) -> BodyHandlers.ofString(), promises);
HttpResponse<String> response = cc.sendAsync(request, BodyHandlers.ofString(), pph).join();
fail("Expected exception, got :" + response + ", " + response.body());
} catch (CompletionException ce) {
System.out.println("Got EXPECTED: " + ce);
assertDetailMessage(ce.getCause(), i);
}
}
}
// Assertions based on implementation specific detail messages. Keep in
// sync with implementation.
static void assertDetailMessage(Throwable throwable, int iterationIndex) {
try {
assertInstanceOf(ProtocolException.class, throwable, "Expected ProtocolException, got " + throwable);
if (iterationIndex == 0) { // unknown
assertTrue(throwable.getMessage().contains("Unknown pseudo-header"),
"Expected \"Unknown pseudo-header\" in: " + throwable.getMessage());
} else if (iterationIndex == 4) { // unexpected type
assertTrue(throwable.getMessage().contains("not valid in context"),
"Expected \"not valid in context\" in: " + throwable.getMessage());
} else {
assertTrue(throwable.getMessage().contains("Bad header"),
"Expected \"Bad header\" in: " + throwable.getMessage());
}
} catch (AssertionError e) {
System.out.println("Exception does not match expectation: " + throwable);
throwable.printStackTrace(System.out);
throw e;
}
}
// --- server push handler ---
static class ServerPushHandler implements HttpServerAdapters.HttpTestHandler {
private final String mainResponseBody;
public ServerPushHandler(String mainResponseBody) {
this.mainResponseBody = mainResponseBody;
}
public void handle(HttpServerAdapters.HttpTestExchange exchange) throws IOException {
System.err.println("Server: handle " + exchange);
try (InputStream is = exchange.getRequestBody()) {
is.readAllBytes();
}
pushPromise(exchange);
// response data for the main response
try (OutputStream os = exchange.getResponseBody()) {
byte[] bytes = mainResponseBody.getBytes(UTF_8);
exchange.sendResponseHeaders(200, bytes.length);
os.write(bytes);
}
}
private void pushPromise(HttpServerAdapters.HttpTestExchange exchange) throws IOException {
URI requestURI = exchange.getRequestURI();
String query = exchange.getRequestURI().getQuery();
int badHeadersIndex = Integer.parseInt(query.substring(query.indexOf("=") + 1));
URI uri = requestURI.resolve("/push/"+badHeadersIndex);
InputStream is = new ByteArrayInputStream(mainResponseBody.getBytes(UTF_8));
HttpHeaders headers = HttpHeaders.of(BAD_HEADERS.get(badHeadersIndex), (x, y) -> true);
exchange.serverPush(uri, headers, is);
System.err.println("Server: push sent");
}
}
}

View file

@ -0,0 +1,291 @@
/*
* Copyright (c) 2015, 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.
*/
/*
* @test
* @bug 8087112
* @library /test/jdk/java/net/httpclient/lib
* /test/lib
* @build jdk.httpclient.test.lib.http2.Http2TestServer
* jdk.test.lib.Asserts
* jdk.test.lib.Utils
* jdk.test.lib.net.SimpleSSLContext
* @run junit/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors BasicTest
*/
import java.io.IOException;
import java.net.*;
import javax.net.ssl.*;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.file.*;
import java.util.concurrent.*;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import jdk.httpclient.test.lib.common.HttpServerAdapters;
import jdk.httpclient.test.lib.http2.Http2TestServer;
import jdk.test.lib.net.SimpleSSLContext;
import org.junit.jupiter.api.Test;
import static java.net.http.HttpClient.Version.HTTP_2;
import static jdk.test.lib.Asserts.assertFileContentsEqual;
import static jdk.test.lib.Utils.createTempFile;
import static jdk.test.lib.Utils.createTempFileOfSize;
public class BasicTest implements HttpServerAdapters {
private static final String TEMP_FILE_PREFIX =
HttpClient.class.getPackageName() + '-' + BasicTest.class.getSimpleName() + '-';
static int httpPort, httpsPort;
static HttpTestServer httpServer, httpsServer;
static HttpClient client = null;
static ExecutorService clientExec;
static ExecutorService serverExec;
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
static String pingURIString, httpURIString, httpsURIString;
static void initialize() throws Exception {
try {
client = getClient();
httpServer = HttpTestServer.of(
new Http2TestServer(false, 0, serverExec, sslContext));
httpServer.addHandler(new HttpTestFileEchoHandler(), "/");
httpServer.addHandler(new EchoWithPingHandler(), "/ping");
httpPort = httpServer.getAddress().getPort();
httpsServer = HttpTestServer.of(
new Http2TestServer(true, 0, serverExec, sslContext));
httpsServer.addHandler(new HttpTestFileEchoHandler(), "/");
httpsPort = httpsServer.getAddress().getPort();
httpURIString = "http://" + httpServer.serverAuthority() + "/foo/";
pingURIString = "http://" + httpServer.serverAuthority() + "/ping/";
httpsURIString = "https://" + httpsServer.serverAuthority() + "/bar/";
httpServer.start();
httpsServer.start();
} catch (Throwable e) {
System.err.println("Throwing now");
e.printStackTrace();
throw e;
}
}
static List<CompletableFuture<Long>> cfs = Collections
.synchronizedList( new LinkedList<>());
static CompletableFuture<Long> currentCF;
static class EchoWithPingHandler extends HttpTestFileEchoHandler {
private final Object lock = new Object();
@Override
public void handle(HttpTestExchange exchange) throws IOException {
// for now only one ping active at a time. don't want to saturate
synchronized(lock) {
CompletableFuture<Long> cf = currentCF;
if (cf == null || cf.isDone()) {
cf = exchange.sendPing();
assert cf != null;
cfs.add(cf);
currentCF = cf;
}
}
super.handle(exchange);
}
}
@Test
void test() throws Exception {
try {
initialize();
warmup(false);
warmup(true);
simpleTest(false, false);
simpleTest(false, true);
simpleTest(true, false);
streamTest(false);
streamTest(true);
paramsTest();
CompletableFuture.allOf(cfs.toArray(new CompletableFuture[0])).join();
synchronized (cfs) {
for (CompletableFuture<Long> cf : cfs) {
System.out.printf("Ping ack received in %d millisec\n", cf.get());
}
}
} catch (Throwable tt) {
System.err.println("tt caught");
tt.printStackTrace();
throw tt;
} finally {
httpServer.stop();
httpsServer.stop();
//clientExec.shutdown();
}
}
static HttpClient getClient() {
if (client == null) {
serverExec = Executors.newCachedThreadPool();
clientExec = Executors.newCachedThreadPool();
client = HttpClient.newBuilder()
.executor(clientExec)
.sslContext(sslContext)
.version(HTTP_2)
.build();
}
return client;
}
static URI getURI(boolean secure) {
return getURI(secure, false);
}
static URI getURI(boolean secure, boolean ping) {
if (secure)
return URI.create(httpsURIString);
else
return URI.create(ping ? pingURIString: httpURIString);
}
static void checkStatus(int expected, int found) throws Exception {
if (expected != found) {
System.err.printf ("Test failed: wrong status code %d/%d\n",
expected, found);
throw new RuntimeException("Test failed");
}
}
static void checkStrings(String expected, String found) throws Exception {
if (!expected.equals(found)) {
System.err.printf ("Test failed: wrong string %s/%s\n",
expected, found);
throw new RuntimeException("Test failed");
}
}
static final String SIMPLE_STRING = "Hello world Goodbye world";
static final int LOOPS = 13;
static final int FILESIZE = 64 * 1024 + 200;
static void streamTest(boolean secure) throws Exception {
URI uri = getURI(secure);
System.err.printf("streamTest %b to %s\n" , secure, uri);
HttpClient client = getClient();
Path src = createTempFileOfSize(TEMP_FILE_PREFIX, null, FILESIZE * 4);
HttpRequest req = HttpRequest.newBuilder(uri)
.POST(BodyPublishers.ofFile(src))
.build();
Path dest = Paths.get("streamtest.txt");
dest.toFile().delete();
CompletableFuture<Path> response = client.sendAsync(req, BodyHandlers.ofFile(dest))
.thenApply(resp -> {
if (resp.statusCode() != 200)
throw new RuntimeException();
return resp.body();
});
response.join();
assertFileContentsEqual(src, dest);
System.err.println("streamTest: DONE");
}
static void paramsTest() throws Exception {
httpsServer.addHandler(((HttpTestExchange t) -> {
SSLSession s = t.getSSLSession();
String prot = s.getProtocol();
if (prot.equals("TLSv1.2") || prot.equals("TLSv1.3")) {
t.sendResponseHeaders(200, HttpTestExchange.RSPBODY_EMPTY);
} else {
System.err.printf("Protocols =%s\n", prot);
t.sendResponseHeaders(500, HttpTestExchange.RSPBODY_EMPTY);
}
}), "/");
URI u = new URI("https://" + httpsServer.serverAuthority() + "/foo");
HttpClient client = getClient();
HttpRequest req = HttpRequest.newBuilder(u).build();
HttpResponse<String> resp = client.send(req, BodyHandlers.ofString());
int stat = resp.statusCode();
if (stat != 200) {
throw new RuntimeException("paramsTest failed "
+ Integer.toString(stat));
}
System.err.println("paramsTest: DONE");
}
static void warmup(boolean secure) throws Exception {
URI uri = getURI(secure);
System.err.println("Request to " + uri);
// Do a simple warmup request
HttpClient client = getClient();
HttpRequest req = HttpRequest.newBuilder(uri)
.POST(BodyPublishers.ofString(SIMPLE_STRING))
.build();
HttpResponse<String> response = client.send(req, BodyHandlers.ofString());
checkStatus(200, response.statusCode());
String responseBody = response.body();
HttpHeaders h = response.headers();
checkStrings(SIMPLE_STRING, responseBody);
checkStrings(h.firstValue("x-hello").get(), "world");
checkStrings(h.firstValue("x-bye").get(), "universe");
}
static void simpleTest(boolean secure, boolean ping) throws Exception {
URI uri = getURI(secure, ping);
System.err.println("Request to " + uri);
// Do loops asynchronously
CompletableFuture[] responses = new CompletableFuture[LOOPS];
final Path source = createTempFileOfSize(TEMP_FILE_PREFIX, null, FILESIZE);
HttpRequest request = HttpRequest.newBuilder(uri)
.POST(BodyPublishers.ofFile(source))
.build();
for (int i = 0; i < LOOPS; i++) {
responses[i] = client.sendAsync(request, BodyHandlers.ofFile(createTempFile(TEMP_FILE_PREFIX, null)))
//.thenApply(resp -> compareFiles(resp.body(), source));
.thenApply(resp -> {
Path body = resp.body();
System.out.printf("Resp status %d body size %d\n",
resp.statusCode(), body.toFile().length());
assertFileContentsEqual(body, source);
return null;
});
Thread.sleep(100);
}
CompletableFuture.allOf(responses).join();
System.err.println("simpleTest: DONE");
}
}

View file

@ -0,0 +1,208 @@
/*
* 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.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.net.ssl.SSLSession;
import jdk.httpclient.test.lib.http2.BodyOutputStream;
import jdk.httpclient.test.lib.http2.Http2Handler;
import jdk.httpclient.test.lib.http2.Http2TestExchange;
import jdk.httpclient.test.lib.http2.Http2TestExchangeSupplier;
import jdk.httpclient.test.lib.http2.Http2TestServer;
import jdk.httpclient.test.lib.http2.Http2TestServerConnection;
import jdk.internal.net.http.HttpClientImplAccess;
import jdk.internal.net.http.common.HttpHeadersBuilder;
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_2;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*
* @test
* @bug 8326498 8361091
* @summary verify that the HttpClient does not leak connections when dealing with
* sudden rush of HTTP/2 requests
* @library /test/lib /test/jdk/java/net/httpclient/lib ../access
* @build jdk.test.lib.net.SimpleSSLContext
* jdk.httpclient.test.lib.http2.Http2TestServer
* jdk.httpclient.test.lib.http2.Http2Handler
* jdk.httpclient.test.lib.http2.Http2TestExchange
* jdk.httpclient.test.lib.http2.Http2TestExchangeSupplier
* java.net.http/jdk.internal.net.http.HttpClientImplAccess
* @run junit ${test.main.class}
*/
class BurstyRequestsTest {
private static final String HANDLER_PATH = "/8326498/";
// we use a h2c server but it doesn't matter if it is h2c or h2
private static Http2TestServer http2Server;
@BeforeAll
static void beforeAll() throws Exception {
http2Server = new Http2TestServer(false, 0);
http2Server.setExchangeSupplier(new ExchangeSupplier());
http2Server.addHandler(new Handler(), HANDLER_PATH);
http2Server.start();
System.err.println("started HTTP/2 server " + http2Server.getAddress());
}
@AfterAll
static void afterAll() {
if (http2Server != null) {
System.err.println("stopping server " + http2Server.getAddress());
http2Server.stop();
}
}
/*
* Issues a burst of HTTP/2 requests to the same server (host/port) and expects all of
* them to complete normally.
* Once these requests have completed, the test then peeks into an internal field of the
* HttpClientImpl to verify that the client is holding on to at most 1 connection.
*/
@Test
void testOpenConnections() throws Exception {
final URI reqURI = URIBuilder.newBuilder()
.scheme("http")
.host(http2Server.getAddress().getAddress())
.port(http2Server.getAddress().getPort())
.path(HANDLER_PATH)
.build();
final HttpRequest req = HttpRequest.newBuilder().uri(reqURI).build();
final int numRequests = 20;
// latch for the tasks to wait on, before issuing the requests
final CountDownLatch startLatch = new CountDownLatch(numRequests);
final List<Future<Void>> futures = new ArrayList<>();
try (final ExecutorService executor = Executors.newCachedThreadPool();
final HttpClient client = HttpClient.newBuilder()
.proxy(NO_PROXY)
.version(HTTP_2)
.build()) {
// our test needs to peek into the internal field of jdk.internal.net.http.HttpClientImpl
final Set<?> openedConnections = HttpClientImplAccess.getOpenedConnections(client);
assertNotNull(openedConnections, "HttpClientImpl#openedConnections field is null or not available");
for (int i = 0; i < numRequests; i++) {
final Future<Void> f = executor.submit(new RequestIssuer(startLatch, client, req));
futures.add(f);
}
// wait for the requests to complete
for (final Future<Void> f : futures) {
f.get();
}
System.err.println("all " + numRequests + " requests completed successfully");
// the request completion happens asynchronously to the closing of the HTTP/2 Stream
// as well as the HTTP/2 connection. we wait for at most 1 connection to be retained
// by HttpClientImpl.
System.err.println("waiting for at least " + (numRequests - 1) + " connections to be closed");
// now verify that the current open TCP connections is not more than 1.
// we let the test timeout if we never reach that count.
int size = openedConnections.size();
System.err.println("currently " + size + " open connections: " + openedConnections);
while (size > 1) {
// wait
Thread.sleep(100);
final int prev = size;
size = openedConnections.size();
if (prev != size) {
System.err.println("currently " + size + " open connections: " + openedConnections);
}
}
// we expect at most 1 connection will stay open
assertTrue((size == 0 || size == 1),
"unexpected number of current open connections: " + size);
}
}
private static final class RequestIssuer implements Callable<Void> {
private final CountDownLatch startLatch;
private final HttpClient client;
private final HttpRequest request;
private RequestIssuer(final CountDownLatch startLatch, final HttpClient client,
final HttpRequest request) {
this.startLatch = startLatch;
this.client = client;
this.request = request;
}
@Override
public Void call() throws Exception {
this.startLatch.countDown(); // announce our arrival
this.startLatch.await(); // wait for other threads to arrive
// issue the request
final HttpResponse<Void> resp = this.client.send(request, BodyHandlers.discarding());
if (resp.statusCode() != 200) {
throw new AssertionError("unexpected response status code: " + resp.statusCode());
}
return null;
}
}
private static final class Handler implements Http2Handler {
private static final int NO_RESP_BODY = -1;
@Override
public void handle(final Http2TestExchange exchange) throws IOException {
System.err.println("handling request " + exchange.getRequestURI());
exchange.sendResponseHeaders(200, NO_RESP_BODY);
}
}
private static final class ExchangeSupplier implements Http2TestExchangeSupplier {
@Override
public Http2TestExchange get(int streamid, String method, HttpHeaders reqheaders,
HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is,
SSLSession sslSession, BodyOutputStream os,
Http2TestServerConnection conn, boolean pushAllowed) {
// don't close the connection when/if the client sends a GOAWAY
conn.closeConnOnIncomingGoAway = false;
return Http2TestExchangeSupplier.ofDefault().get(streamid, method, reqheaders,
rspheadersBuilder, uri, is, sslSession, os, conn, pushAllowed);
}
}
}

View file

@ -0,0 +1,343 @@
/*
* Copyright (c) 2024, 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.
*/
/*
* @test
* @bug 8342075
* @summary checks connection flow control
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext
* @run junit/othervm -Djdk.internal.httpclient.debug=err
* -Djdk.httpclient.connectionWindowSize=65535
* -Djdk.httpclient.windowsize=16384
* ConnectionFlowControlTest
*/
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ProtocolException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer;
import jdk.httpclient.test.lib.http2.BodyOutputStream;
import jdk.httpclient.test.lib.http2.Http2Handler;
import jdk.httpclient.test.lib.http2.Http2TestExchange;
import jdk.httpclient.test.lib.http2.Http2TestExchangeImpl;
import jdk.httpclient.test.lib.http2.Http2TestServer;
import jdk.httpclient.test.lib.http2.Http2TestServerConnection;
import jdk.internal.net.http.common.HttpHeadersBuilder;
import jdk.internal.net.http.frame.SettingsFrame;
import jdk.test.lib.Utils;
import jdk.test.lib.net.SimpleSSLContext;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.*;
public class ConnectionFlowControlTest {
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
private static HttpTestServer http2TestServer; // HTTP/2 ( h2c )
private static HttpTestServer https2TestServer; // HTTP/2 ( h2 )
private static String http2URI;
private static String https2URI;
private final AtomicInteger reqid = new AtomicInteger();
static Object[][] variants() {
return new Object[][] {
{ http2URI },
{ https2URI },
};
}
@ParameterizedTest
@MethodSource("variants")
void test(String uri) throws Exception {
System.out.printf("%ntesting %s%n", uri);
ConcurrentHashMap<String, CompletableFuture<String>> responseSent = new ConcurrentHashMap<>();
ConcurrentHashMap<String, HttpResponse<InputStream>> responses = new ConcurrentHashMap<>();
FCHttp2TestExchange.setResponseSentCB((s) -> responseSent.get(s).complete(s));
int connectionWindowSize = Math.max(Integer.getInteger(
"jdk.httpclient.connectionWindowSize", 65535), 65535);
int windowSize = Math.max(Integer.getInteger(
"jdk.httpclient.windowsize", 65535), 16384);
int max = connectionWindowSize / windowSize + 2;
System.out.printf("connection window: %s, stream window: %s, will make %s requests%n",
connectionWindowSize, windowSize, max);
try (HttpClient client = HttpClient.newBuilder().sslContext(sslContext).build()) {
String label = null;
Throwable t = null;
try {
String[] keys = new String[max];
for (int i = 0; i < max; i++) {
String query = "reqId=" + reqid.incrementAndGet();
keys[i] = query;
URI uriWithQuery = URI.create(uri + "?" + query);
CompletableFuture<String> sent = new CompletableFuture<>();
responseSent.put(query, sent);
HttpRequest request = HttpRequest.newBuilder(uriWithQuery)
.POST(BodyPublishers.ofString("Hello there!"))
.build();
System.out.println("\nSending request:" + uriWithQuery);
final HttpClient cc = client;
var response = cc.send(request, BodyHandlers.ofInputStream());
responses.put(query, response);
String ckey = response.connectionLabel().get();
if (label == null) label = ckey;
try {
if (i < max - 1) {
// the connection window might be exceeded at i == max - 2, which
// means that the last request could go on a new connection.
assertEquals(label, ckey, "Unexpected key for " + query);
}
} catch (AssertionError ass) {
// since we won't pull all responses, the client
// will not exit unless we ask it to shutdown now.
client.shutdownNow();
throw ass;
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException ie) {
// ignore
}
CompletableFuture<?> allsent = CompletableFuture.allOf(responseSent.values().stream()
.toArray(CompletableFuture<?>[]::new));
allsent.get();
for (int i = 0; i < max; i++) {
try {
String query = keys[i];
var response = responses.get(keys[i]);
String ckey = response.connectionLabel().get();
if (label == null) label = ckey;
if (i < max - 1) {
// the connection window might be exceeded at i == max - 2, which
// means that the last request could go on a new connection.
assertEquals(label, ckey, "Unexpected key for " + query);
}
int wait = uri.startsWith("https://") ? 500 : 250;
try (InputStream is = response.body()) {
Thread.sleep(Utils.adjustTimeout(wait));
is.readAllBytes();
}
System.out.printf("%s did not fail: %s%n", query, response.statusCode());
} catch (AssertionError t1) {
// since we won't pull all responses, the client
// will not exit unless we ask it to shutdown now.
client.shutdownNow();
throw t1;
} catch (Throwable t0) {
System.out.println("Got EXPECTED: " + t0);
if (t0 instanceof ExecutionException) {
t0 = t0.getCause();
}
t = t0;
try {
assertDetailMessage(t0, i);
} catch (AssertionError e) {
// since we won't pull all responses, the client
// will not exit unless we ask it to shutdown now.
client.shutdownNow();
throw e;
}
}
}
} catch (Throwable t0) {
System.out.println("Got EXPECTED: " + t0);
if (t0 instanceof ExecutionException) {
t0 = t0.getCause();
}
t = t0;
}
if (t == null) {
// we could fail here if we haven't waited long enough
fail("Expected exception, got all responses, should sleep time be raised?");
} else {
assertDetailMessage(t, max);
}
String query = "reqId=" + reqid.incrementAndGet();
URI uriWithQuery = URI.create(uri + "?" + query);
CompletableFuture<String> sent = new CompletableFuture<>();
responseSent.put(query, sent);
HttpRequest request = HttpRequest.newBuilder(uriWithQuery)
.POST(BodyPublishers.ofString("Hello there!"))
.build();
System.out.println("\nSending last request:" + uriWithQuery);
var response = client.send(request, BodyHandlers.ofString());
if (label != null) {
String ckey = response.connectionLabel().get();
assertNotEquals(label, ckey);
System.out.printf("last request %s sent on different connection as expected:" +
"\n\tlast: %s\n\tprevious: %s%n", query, ckey, label);
}
}
}
// Assertions based on implementation specific detail messages. Keep in
// sync with implementation.
static void assertDetailMessage(Throwable throwable, int iterationIndex) {
try {
Throwable cause = throwable;
while (cause != null) {
if (cause instanceof ProtocolException) {
if (cause.getMessage().contains("connection window exceeded")) {
System.out.println("Found expected exception: " + cause);
return;
}
}
cause = cause.getCause();
}
throw new AssertionError(
"ProtocolException(\"protocol error: connection window exceeded\") not found",
throwable);
} catch (AssertionError e) {
System.out.println("Exception does not match expectation: " + throwable);
throwable.printStackTrace(System.out);
throw e;
}
}
@BeforeAll
static void setup() throws Exception {
var http2TestServerLocal = new Http2TestServer("localhost", false, 0);
http2TestServerLocal.addHandler(new Http2TestHandler(), "/http2/");
http2TestServer = HttpTestServer.of(http2TestServerLocal);
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/x";
var https2TestServerLocal = new Http2TestServer("localhost", true, sslContext);
https2TestServerLocal.addHandler(new Http2TestHandler(), "/https2/");
https2TestServer = HttpTestServer.of(https2TestServerLocal);
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/x";
// Override the default exchange supplier with a custom one to enable
// particular test scenarios
http2TestServerLocal.setExchangeSupplier(FCHttp2TestExchange::new);
https2TestServerLocal.setExchangeSupplier(FCHttp2TestExchange::new);
http2TestServer.start();
https2TestServer.start();
}
@AfterAll
static void teardown() throws Exception {
http2TestServer.stop();
https2TestServer.stop();
}
static class Http2TestHandler implements Http2Handler {
@Override
public void handle(Http2TestExchange t) throws IOException {
String query = t.getRequestURI().getRawQuery();
try (InputStream is = t.getRequestBody();
OutputStream os = t.getResponseBody()) {
byte[] bytes = is.readAllBytes();
System.out.println("Server " + t.getLocalAddress() + " received:\n"
+ t.getRequestURI() + ": " + new String(bytes, StandardCharsets.UTF_8));
if (bytes.length == 0) bytes = "no request body!".getBytes(StandardCharsets.UTF_8);
int window = Math.max(16384, Integer.getInteger("jdk.httpclient.windowsize", 2*16*1024));
final int maxChunkSize;
if (t instanceof FCHttp2TestExchange fct) {
maxChunkSize = Math.min(window, fct.conn.getMaxFrameSize());
} else {
maxChunkSize = Math.min(window, SettingsFrame.MAX_FRAME_SIZE);
}
byte[] resp = bytes.length < maxChunkSize
? bytes
: Arrays.copyOfRange(bytes, 0, maxChunkSize);
int max = (window / resp.length);
// send in chunks
t.sendResponseHeaders(200, 0);
int sent = 0;
for (int i=0; i<=max; i++) {
int len = Math.min(resp.length, window - sent);
if (len <= 0) break;
if (os instanceof BodyOutputStream bos) {
try {
// we don't wait for the stream window, but we want
// to wait for the connection window
bos.waitForStreamWindow(len);
} catch (InterruptedException ie) {
// ignore and continue...
}
}
((BodyOutputStream) os).writeUncontrolled(resp, 0, len);
sent += len;
}
if (sent != window) fail("should have sent %s, sent %s".formatted(window, sent));
}
if (t instanceof FCHttp2TestExchange fct) {
fct.responseSent(query);
} else {
fail("Exchange is not %s but %s"
.formatted(FCHttp2TestExchange.class.getName(), t.getClass().getName()));
}
}
}
// A custom Http2TestExchangeImpl that overrides sendResponseHeaders to
// allow headers to be sent with a number of CONTINUATION frames.
static class FCHttp2TestExchange extends Http2TestExchangeImpl {
static volatile Consumer<String> responseSentCB;
static void setResponseSentCB(Consumer<String> responseSentCB) {
FCHttp2TestExchange.responseSentCB = responseSentCB;
}
final Http2TestServerConnection conn;
FCHttp2TestExchange(int streamid, String method, HttpHeaders reqheaders,
HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is,
SSLSession sslSession, BodyOutputStream os,
Http2TestServerConnection conn, boolean pushAllowed) {
super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession, os, conn, pushAllowed);
this.conn = conn;
}
public void responseSent(String query) {
System.out.println("Server: response sent for " + query);
responseSentCB.accept(query);
}
}
}

View file

@ -0,0 +1,181 @@
/*
* 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.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import javax.net.ssl.SSLContext;
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.IPSupport;
import jdk.test.lib.net.SimpleSSLContext;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static java.net.http.HttpClient.Version.HTTP_2;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/*
* @test
* @bug 8305906
* @summary verify that the HttpClient pools and reuses a connection for HTTP/2 requests
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.test.lib.net.SimpleSSLContext
* jdk.test.lib.net.IPSupport
* jdk.httpclient.test.lib.common.HttpServerAdapters
*
* @run junit ConnectionReuseTest
* @run junit/othervm -Djava.net.preferIPv6Addresses=true
* -Djdk.internal.httpclient.debug=true ConnectionReuseTest
*/
public class ConnectionReuseTest {
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
private static HttpTestServer http2_Server; // h2 server over HTTP
private static HttpTestServer https2_Server; // h2 server over HTTPS
@BeforeAll
public static void beforeAll() throws Exception {
if (IPSupport.preferIPv6Addresses()) {
IPSupport.printPlatformSupport(System.err); // for debug purposes
// this test is run with -Djava.net.preferIPv6Addresses=true, so skip (all) tests
// if IPv6 isn't supported on this host
Assumptions.assumeTrue(IPSupport.hasIPv6(), "Skipping tests - IPv6 is not supported");
}
http2_Server = HttpTestServer.create(HTTP_2);
http2_Server.addHandler(new Handler(), "/");
http2_Server.start();
System.out.println("Started HTTP v2 server at " + http2_Server.serverAuthority());
https2_Server = HttpTestServer.create(HTTP_2, sslContext);
https2_Server.addHandler(new Handler(), "/");
https2_Server.start();
System.out.println("Started HTTPS v2 server at " + https2_Server.serverAuthority());
}
@AfterAll
public static void afterAll() {
if (https2_Server != null) {
System.out.println("Stopping server " + https2_Server);
https2_Server.stop();
}
if (http2_Server != null) {
System.out.println("Stopping server " + http2_Server);
http2_Server.stop();
}
}
private static Stream<Arguments> requestURIs() throws Exception {
final List<Arguments> arguments = new ArrayList<>();
// h2 over HTTPS
arguments.add(Arguments.of(new URI("https://" + https2_Server.serverAuthority() + "/")));
// h2 over HTTP
arguments.add(Arguments.of(new URI("http://" + http2_Server.serverAuthority() + "/")));
if (IPSupport.preferIPv6Addresses()) {
if (https2_Server.getAddress().getAddress().isLoopbackAddress()) {
// h2 over HTTPS, use the short form of the host, in the request URI
arguments.add(Arguments.of(new URI("https://[::1]:" +
https2_Server.getAddress().getPort() + "/")));
}
if (http2_Server.getAddress().getAddress().isLoopbackAddress()) {
// h2 over HTTP, use the short form of the host, in the request URI
arguments.add(Arguments.of(new URI("http://[::1]:" +
http2_Server.getAddress().getPort() + "/")));
}
}
return arguments.stream();
}
/**
* Uses a single instance of a HttpClient and issues multiple requests to {@code requestURI}
* and expects that each of the request internally uses the same connection
*/
@ParameterizedTest
@MethodSource("requestURIs")
public void testConnReuse(final URI requestURI) throws Exception {
final HttpClient.Builder builder = HttpClient.newBuilder()
.proxy(NO_PROXY).sslContext(sslContext);
final HttpRequest req = HttpRequest.newBuilder().uri(requestURI)
.GET().version(HTTP_2).build();
try (final HttpClient client = builder.build()) {
String clientConnAddr = null;
for (int i = 1; i <= 5; i++) {
System.out.println("Issuing request(" + i + ") " + req);
final HttpResponse<String> resp = client.send(req, BodyHandlers.ofString());
assertEquals(200, resp.statusCode(), "unexpected response code");
final String respBody = resp.body();
System.out.println("Server side handler responded to a request from " + respBody);
assertNotEquals(Handler.UNKNOWN_CLIENT_ADDR, respBody,
"server handler couldn't determine client address in request");
if (i == 1) {
// for the first request we just keep track of the client connection address
// that got used for this request
clientConnAddr = respBody;
} else {
// verify that the client connection used to issue the request is the same
// as the previous request's client connection
assertEquals(clientConnAddr, respBody, "HttpClient unexpectedly used a" +
" different connection for request(" + i + ")");
}
}
}
}
private static final class Handler implements HttpTestHandler {
private static final String UNKNOWN_CLIENT_ADDR = "unknown";
@Override
public void handle(final HttpTestExchange t) throws IOException {
final InetSocketAddress clientAddr = t.getRemoteAddress();
System.out.println("Handling request " + t.getRequestURI() + " from " + clientAddr);
// we write out the client address into the response body
final byte[] responseBody = clientAddr == null
? UNKNOWN_CLIENT_ADDR.getBytes(StandardCharsets.UTF_8)
: clientAddr.toString().getBytes(StandardCharsets.UTF_8);
t.sendResponseHeaders(200, responseBody.length);
try (final OutputStream os = t.getResponseBody()) {
os.write(responseBody);
}
}
}
}

View file

@ -0,0 +1,319 @@
/*
* Copyright (c) 2017, 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.
*/
/*
* @test
* @summary Test for CONTINUATION frame handling
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext
* @compile ../ReferenceTracker.java
* @run junit/othervm ContinuationFrameTest
*/
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.http.HttpHeaders;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
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 jdk.internal.net.http.common.HttpHeadersBuilder;
import jdk.internal.net.http.frame.ContinuationFrame;
import jdk.internal.net.http.frame.HeaderFrame;
import jdk.internal.net.http.frame.HeadersFrame;
import jdk.internal.net.http.frame.Http2Frame;
import jdk.httpclient.test.lib.http2.Http2TestServer;
import jdk.httpclient.test.lib.http2.Http2TestExchange;
import jdk.httpclient.test.lib.http2.Http2TestExchangeImpl;
import jdk.httpclient.test.lib.http2.Http2Handler;
import jdk.httpclient.test.lib.http2.BodyOutputStream;
import jdk.httpclient.test.lib.http2.Http2TestServerConnection;
import jdk.test.lib.net.SimpleSSLContext;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static java.lang.System.out;
import static java.net.http.HttpClient.Version.HTTP_2;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class ContinuationFrameTest {
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
private static Http2TestServer http2TestServer; // HTTP/2 ( h2c )
private static Http2TestServer https2TestServer; // HTTP/2 ( h2 )
private static String http2URI;
private static String https2URI;
private static String noBodyhttp2URI;
private static String noBodyhttps2URI;
private final static ReferenceTracker TRACKER = ReferenceTracker.INSTANCE;
/**
* A function that returns a list of 1) a HEADERS frame ( with an empty
* payload ), and 2) a CONTINUATION frame with the actual headers.
*/
static BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> oneContinuation =
(Integer streamid, List<ByteBuffer> encodedHeaders) -> {
List<ByteBuffer> empty = List.of(ByteBuffer.wrap(new byte[0]));
HeadersFrame hf = new HeadersFrame(streamid, 0, empty);
ContinuationFrame cf = new ContinuationFrame(streamid,
HeaderFrame.END_HEADERS,
encodedHeaders);
return List.of(hf, cf);
};
/**
* A function that returns a list of 1) a HEADERS frame with END_STREAM
* ( and with an empty payload ), and 2) two CONTINUATION frames,the first
* is empty and the second contains headers and the END_HEADERS flag
*/
static BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> twoContinuation =
(Integer streamid, List<ByteBuffer> encodedHeaders) -> {
List<ByteBuffer> empty = List.of(ByteBuffer.wrap(new byte[0]));
HeadersFrame hf = new HeadersFrame(streamid, HeaderFrame.END_STREAM, empty);
ContinuationFrame cf = new ContinuationFrame(streamid, 0,empty);
ContinuationFrame cf1 = new ContinuationFrame(streamid,
HeaderFrame.END_HEADERS,
encodedHeaders);
return List.of(hf, cf, cf1);
};
/**
* A function that returns a list of a HEADERS frame followed by a number of
* CONTINUATION frames. Each frame contains just a single byte of payload.
*/
static BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> byteAtATime =
(Integer streamid, List<ByteBuffer> encodedHeaders) -> {
assert encodedHeaders.get(0).hasRemaining();
List<Http2Frame> frames = new ArrayList<>();
ByteBuffer hb = ByteBuffer.wrap(new byte[] {encodedHeaders.get(0).get()});
HeadersFrame hf = new HeadersFrame(streamid, 0, hb);
frames.add(hf);
for (ByteBuffer bb : encodedHeaders) {
while (bb.hasRemaining()) {
List<ByteBuffer> data = List.of(ByteBuffer.wrap(new byte[] {bb.get()}));
ContinuationFrame cf = new ContinuationFrame(streamid, 0, data);
frames.add(cf);
}
}
frames.get(frames.size() - 1).setFlag(HeaderFrame.END_HEADERS);
return frames;
};
static Object[][] variants() {
return new Object[][] {
{ http2URI, false, oneContinuation },
{ https2URI, false, oneContinuation },
{ http2URI, true, oneContinuation },
{ https2URI, true, oneContinuation },
{ noBodyhttp2URI, false, twoContinuation },
{ noBodyhttp2URI, true, twoContinuation },
{ noBodyhttps2URI, false, twoContinuation },
{ noBodyhttps2URI, true, twoContinuation },
{ http2URI, false, byteAtATime },
{ https2URI, false, byteAtATime },
{ http2URI, true, byteAtATime },
{ https2URI, true, byteAtATime },
};
}
static final int ITERATION_COUNT = 20;
static HttpClient sharedClient;
HttpClient httpClient(boolean shared) {
if (!shared || sharedClient == null) {
var client = HttpClient.newBuilder()
.proxy(HttpClient.Builder.NO_PROXY)
.sslContext(sslContext)
.build();
if (sharedClient == null) {
sharedClient = client;
}
TRACKER.track(client);
return client;
}
return sharedClient;
}
@ParameterizedTest
@MethodSource("variants")
void test(String uri,
boolean sameClient,
BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> headerFramesSupplier)
throws Exception
{
CFTHttp2TestExchange.setHeaderFrameSupplier(headerFramesSupplier);
HttpClient client = null;
for (int i=0; i< ITERATION_COUNT; i++) {
if (!sameClient || client == null) {
client = httpClient(sameClient);
}
HttpRequest request = HttpRequest.newBuilder(URI.create(uri))
.POST(BodyPublishers.ofString("Hello there!"))
.build();
HttpResponse<String> resp;
if (i % 2 == 0) {
resp = client.send(request, BodyHandlers.ofString());
} else {
resp = client.sendAsync(request, BodyHandlers.ofString()).join();
}
if(uri.contains("nobody")) {
out.println("Got response: " + resp);
assertEquals(204, resp.statusCode(), "Expected 204, got:" + resp.statusCode());
assertEquals(HTTP_2, resp.version());
continue;
}
out.println("Got response: " + resp);
out.println("Got body: " + resp.body());
assertEquals(200, resp.statusCode(), "Expected 200, got:" + resp.statusCode());
assertEquals("Hello there!", resp.body());
assertEquals(HTTP_2, resp.version());
}
}
@BeforeAll
static void setup() throws Exception {
http2TestServer = new Http2TestServer("localhost", false, 0);
http2TestServer.addHandler(new Http2EchoHandler(), "/http2/echo");
http2TestServer.addHandler(new Http2NoBodyHandler(), "/http2/nobody");
int port = http2TestServer.getAddress().getPort();
http2URI = "http://localhost:" + port + "/http2/echo";
noBodyhttp2URI = "http://localhost:" + port + "/http2/nobody";
https2TestServer = new Http2TestServer("localhost", true, sslContext);
https2TestServer.addHandler(new Http2EchoHandler(), "/https2/echo");
https2TestServer.addHandler(new Http2NoBodyHandler(), "/https2/nobody");
port = https2TestServer.getAddress().getPort();
https2URI = "https://localhost:" + port + "/https2/echo";
noBodyhttps2URI = "https://localhost:" + port + "/https2/nobody";
// Override the default exchange supplier with a custom one to enable
// particular test scenarios
http2TestServer.setExchangeSupplier(CFTHttp2TestExchange::new);
https2TestServer.setExchangeSupplier(CFTHttp2TestExchange::new);
http2TestServer.start();
https2TestServer.start();
}
@AfterAll
static void teardown() throws Exception {
sharedClient = null;
AssertionError fail = TRACKER.check(500);
try {
http2TestServer.stop();
https2TestServer.stop();
} finally {
if (fail != null) {
throw fail;
}
}
}
static class Http2EchoHandler implements Http2Handler {
@Override
public void handle(Http2TestExchange t) throws IOException {
try (InputStream is = t.getRequestBody();
OutputStream os = t.getResponseBody()) {
byte[] bytes = is.readAllBytes();
t.getResponseHeaders().addHeader("justSome", "Noise");
t.getResponseHeaders().addHeader("toAdd", "payload in");
t.getResponseHeaders().addHeader("theHeader", "Frames");
t.sendResponseHeaders(200, bytes.length);
os.write(bytes);
}
}
}
static class Http2NoBodyHandler implements Http2Handler {
@Override
public void handle(Http2TestExchange t) throws IOException {
try (InputStream is = t.getRequestBody();
OutputStream os = t.getResponseBody()) {
byte[] bytes = is.readAllBytes();
t.sendResponseHeaders(204, -1);
}
}
}
// A custom Http2TestExchangeImpl that overrides sendResponseHeaders to
// allow headers to be sent with a number of CONTINUATION frames.
static class CFTHttp2TestExchange extends Http2TestExchangeImpl {
static volatile BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> headerFrameSupplier;
static void setHeaderFrameSupplier(BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> hfs) {
headerFrameSupplier = hfs;
}
CFTHttp2TestExchange(int streamid, String method, HttpHeaders reqheaders,
HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is,
SSLSession sslSession, BodyOutputStream os,
Http2TestServerConnection conn, boolean pushAllowed) {
super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession,
os, conn, pushAllowed);
}
@Override
public void sendResponseHeaders(int rCode, long responseLength) throws IOException {
this.responseLength = responseLength;
if (responseLength != 0 && rCode != 204) {
long clen = responseLength > 0 ? responseLength : 0;
rspheadersBuilder.setHeader("Content-length", Long.toString(clen));
}
rspheadersBuilder.setHeader(":status", Integer.toString(rCode));
HttpHeaders headers = rspheadersBuilder.build();
List<ByteBuffer> encodeHeaders = conn.encodeHeaders(headers);
List<Http2Frame> headerFrames = headerFrameSupplier.apply(streamid, encodeHeaders);
assert headerFrames.size() > 0; // there must always be at least 1
if(headerFrames.get(0).getFlag(HeaderFrame.END_STREAM))
os.markClosed();
for (Http2Frame f : headerFrames) {
conn.addToOutputQ(f);
}
os.goodToGo();
System.err.println("Sent response headers " + rCode);
}
}
}

View file

@ -0,0 +1,129 @@
/*
* Copyright (c) 2015, 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.
*/
/*
* @test
* @bug 8157105
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.httpclient.test.lib.http2.Http2TestServer
* jdk.test.lib.Asserts
* jdk.test.lib.net.SimpleSSLContext
* @modules java.base/sun.net.www.http
* java.net.http/jdk.internal.net.http.common
* java.net.http/jdk.internal.net.http.frame
* java.net.http/jdk.internal.net.http.hpack
* java.base/jdk.internal.net.quic
* java.net.http/jdk.internal.net.http.quic
* java.net.http/jdk.internal.net.http.quic.packets
* java.net.http/jdk.internal.net.http.quic.frames
* java.net.http/jdk.internal.net.http.quic.streams
* java.net.http/jdk.internal.net.http.http3.streams
* java.net.http/jdk.internal.net.http.http3.frames
* java.net.http/jdk.internal.net.http.http3
* java.net.http/jdk.internal.net.http.qpack
* java.net.http/jdk.internal.net.http.qpack.readers
* java.net.http/jdk.internal.net.http.qpack.writers
* java.security.jgss
* @modules java.base/jdk.internal.util
* @run junit/othervm/timeout=60 -Djavax.net.debug=ssl -Djdk.httpclient.HttpClient.log=all ErrorTest
* @summary check exception thrown when bad TLS parameters selected
*/
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 javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import jdk.httpclient.test.lib.common.HttpServerAdapters;
import jdk.httpclient.test.lib.http2.Http2TestServer;
import jdk.test.lib.net.SimpleSSLContext;
import static java.net.http.HttpClient.Version.HTTP_2;
import org.junit.jupiter.api.Test;
/**
* When selecting an unacceptable cipher suite the TLS handshake will fail.
* But, the exception that was thrown was not being returned up to application
* causing hang problems
*/
public class ErrorTest implements HttpServerAdapters {
static final String[] CIPHER_SUITES = new String[]{ "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" };
static final String SIMPLE_STRING = "Hello world Goodbye world";
//@Test(timeOut=5000)
@Test
public void test() throws Exception {
SSLContext sslContext = SimpleSSLContext.findSSLContext();
ExecutorService exec = Executors.newCachedThreadPool();
HttpClient client = HttpClient.newBuilder()
.executor(exec)
.sslContext(sslContext)
.sslParameters(new SSLParameters(CIPHER_SUITES))
.version(HTTP_2)
.build();
HttpTestServer httpsServer = null;
try {
SSLContext serverContext = SimpleSSLContext.findSSLContext();
SSLParameters p = serverContext.getSupportedSSLParameters();
p.setApplicationProtocols(new String[]{"h2"});
Http2TestServer httpsServerImpl = new Http2TestServer(true,
0,
exec,
serverContext);
httpsServer = HttpTestServer.of(httpsServerImpl);
httpsServer.addHandler(new HttpTestFileEchoHandler(), "/");
int httpsPort = httpsServer.getAddress().getPort();
String httpsURIString = "https://localhost:" + httpsPort + "/bar/";
httpsServer.start();
URI uri = URI.create(httpsURIString);
System.err.println("Request to " + uri);
HttpRequest req = HttpRequest.newBuilder(uri)
.POST(BodyPublishers.ofString(SIMPLE_STRING))
.build();
HttpResponse response;
try {
response = client.send(req, BodyHandlers.discarding());
throw new RuntimeException("Unexpected response: " + response);
} catch (IOException e) {
System.err.println("Caught Expected IOException: " + e);
}
System.err.println("DONE");
} finally {
if (httpsServer != null ) { httpsServer.stop(); }
exec.shutdownNow();
}
}
}

View file

@ -0,0 +1,263 @@
/*
* Copyright (c) 2015, 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.
*/
/*
* @test
* @bug 8087112 8177935
* @library /test/jdk/java/net/httpclient/lib
* /test/lib
* @build jdk.httpclient.test.lib.http2.Http2TestServer
* jdk.test.lib.Asserts
* jdk.test.lib.Utils
* jdk.test.lib.net.SimpleSSLContext
* @run junit/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors FixedThreadPoolTest
*/
import java.net.*;
import java.net.http.*;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse.BodyHandlers;
import javax.net.ssl.*;
import java.nio.file.*;
import java.util.concurrent.*;
import jdk.httpclient.test.lib.common.HttpServerAdapters;
import jdk.httpclient.test.lib.http2.Http2TestExchange;
import jdk.httpclient.test.lib.http2.Http2TestServer;
import jdk.test.lib.net.SimpleSSLContext;
import static java.net.http.HttpClient.Version.HTTP_2;
import static jdk.test.lib.Asserts.assertFileContentsEqual;
import static jdk.test.lib.Utils.createTempFile;
import static jdk.test.lib.Utils.createTempFileOfSize;
import org.junit.jupiter.api.Test;
public class FixedThreadPoolTest implements HttpServerAdapters {
private static final String TEMP_FILE_PREFIX =
HttpClient.class.getPackageName() + '-' + FixedThreadPoolTest.class.getSimpleName() + '-';
static int httpPort, httpsPort;
static HttpTestServer httpServer, httpsServer;
static HttpClient client = null;
static ExecutorService exec;
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
static String httpURIString, httpsURIString;
static void initialize() throws Exception {
try {
client = getClient();
httpServer = HttpTestServer.of(
new Http2TestServer(false, 0, exec, sslContext));
httpServer.addHandler(new HttpTestFileEchoHandler(), "/");
httpPort = httpServer.getAddress().getPort();
httpsServer = HttpTestServer.of(
new Http2TestServer(true, 0, exec, sslContext));
httpsServer.addHandler(new HttpTestFileEchoHandler(), "/");
httpsPort = httpsServer.getAddress().getPort();
httpURIString = "http://" + httpServer.serverAuthority() + "/foo/";
httpsURIString = "https://" + httpsServer.serverAuthority() + "/bar/";
httpServer.start();
httpsServer.start();
} catch (Throwable e) {
System.err.println("Throwing now");
e.printStackTrace();
throw e;
}
}
@Test
public void test() throws Exception {
try {
initialize();
simpleTest(false);
simpleTest(true);
streamTest(false);
streamTest(true);
paramsTest();
Thread.sleep(1000 * 4);
} catch (Exception | Error tt) {
tt.printStackTrace();
throw tt;
} finally {
httpServer.stop();
httpsServer.stop();
exec.shutdownNow();
}
}
static HttpClient getClient() {
if (client == null) {
exec = Executors.newCachedThreadPool();
// Executor e1 = Executors.newFixedThreadPool(1);
// Executor e = (Runnable r) -> e1.execute(() -> {
// System.out.println("[" + Thread.currentThread().getName()
// + "] Executing: "
// + r.getClass().getName());
// r.run();
// });
client = HttpClient.newBuilder()
.executor(Executors.newFixedThreadPool(2))
.sslContext(sslContext)
.version(HTTP_2)
.build();
}
return client;
}
static URI getURI(boolean secure) {
if (secure)
return URI.create(httpsURIString);
else
return URI.create(httpURIString);
}
static void checkStatus(int expected, int found) throws Exception {
if (expected != found) {
System.err.printf ("Test failed: wrong status code %d/%d\n",
expected, found);
throw new RuntimeException("Test failed");
}
}
static void checkStrings(String expected, String found) throws Exception {
if (!expected.equals(found)) {
System.err.printf ("Test failed: wrong string %s/%s\n",
expected, found);
throw new RuntimeException("Test failed");
}
}
static final String SIMPLE_STRING = "Hello world Goodbye world";
static final int LOOPS = 32;
static final int FILESIZE = 64 * 1024 + 200;
static void streamTest(boolean secure) throws Exception {
URI uri = getURI(secure);
System.err.printf("streamTest %b to %s\n" , secure, uri);
HttpClient client = getClient();
Path src = createTempFileOfSize(TEMP_FILE_PREFIX, null, FILESIZE * 4);
HttpRequest req = HttpRequest.newBuilder(uri)
.POST(BodyPublishers.ofFile(src))
.build();
Path dest = Paths.get("streamtest.txt");
dest.toFile().delete();
CompletableFuture<Path> response = client.sendAsync(req, BodyHandlers.ofFile(dest))
.thenApply(resp -> {
if (resp.statusCode() != 200)
throw new RuntimeException();
return resp.body();
});
response.join();
assertFileContentsEqual(src, dest);
System.err.println("DONE");
}
// expect highest supported version we know about
static String expectedTLSVersion(SSLContext ctx) {
SSLParameters params = ctx.getSupportedSSLParameters();
String[] protocols = params.getProtocols();
for (String prot : protocols) {
if (prot.equals("TLSv1.3"))
return "TLSv1.3";
}
return "TLSv1.2";
}
static void paramsTest() throws Exception {
System.err.println("paramsTest");
Http2TestServer server = new Http2TestServer(true, 0, exec, sslContext);
server.addHandler(((Http2TestExchange t) -> {
SSLSession s = t.getSSLSession();
String prot = s.getProtocol();
if (prot.equals(expectedTLSVersion(sslContext))) {
t.sendResponseHeaders(200, -1);
} else {
System.err.printf("Protocols =%s\n", prot);
t.sendResponseHeaders(500, -1);
}
}), "/");
server.start();
int port = server.getAddress().getPort();
URI u = new URI("https://localhost:"+port+"/foo");
HttpClient client = getClient();
HttpRequest req = HttpRequest.newBuilder(u).build();
HttpResponse<String> resp = client.sendAsync(req, BodyHandlers.ofString()).get();
int stat = resp.statusCode();
if (stat != 200) {
throw new RuntimeException("paramsTest failed "
+ Integer.toString(stat));
}
}
static void simpleTest(boolean secure) throws Exception {
URI uri = getURI(secure);
System.err.println("Request to " + uri);
// Do a simple warmup request
HttpClient client = getClient();
HttpRequest req = HttpRequest.newBuilder(uri)
.POST(BodyPublishers.ofString(SIMPLE_STRING))
.build();
HttpResponse<String> response = client.sendAsync(req, BodyHandlers.ofString()).get();
HttpHeaders h = response.headers();
checkStatus(200, response.statusCode());
String responseBody = response.body();
checkStrings(SIMPLE_STRING, responseBody);
checkStrings(h.firstValue("x-hello").get(), "world");
checkStrings(h.firstValue("x-bye").get(), "universe");
// Do loops asynchronously
CompletableFuture[] responses = new CompletableFuture[LOOPS];
final Path source = createTempFileOfSize(TEMP_FILE_PREFIX, null, FILESIZE);
HttpRequest request = HttpRequest.newBuilder(uri)
.POST(BodyPublishers.ofFile(source))
.build();
for (int i = 0; i < LOOPS; i++) {
responses[i] = client.sendAsync(request, BodyHandlers.ofFile(createTempFile(TEMP_FILE_PREFIX, null)))
//.thenApply(resp -> compareFiles(resp.body(), source));
.thenApply(resp -> {
Path body = resp.body();
System.out.printf("Resp status %d body size %d\n",
resp.statusCode(), body.toFile().length());
assertFileContentsEqual(body, source);
return null;
});
}
CompletableFuture.allOf(responses).join();
System.err.println("DONE");
}
}

View file

@ -0,0 +1,338 @@
/*
* Copyright (c) 2024, 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.io.OutputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import javax.net.ssl.SSLContext;
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.Version.HTTP_2;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
/*
* @test
* @bug 8335181
* @summary verify that the HttpClient correctly handles incoming GOAWAY frames and
* retries any unprocessed requests on a new connection
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.httpclient.test.lib.common.HttpServerAdapters
* jdk.test.lib.net.SimpleSSLContext
* @run junit H2GoAwayTest
*/
public class H2GoAwayTest {
private static final String REQ_PATH = "/test";
private static HttpTestServer server;
private static String REQ_URI_BASE;
private static final SSLContext sslCtx = SimpleSSLContext.findSSLContext();
@BeforeAll
static void beforeAll() throws Exception {
server = HttpTestServer.create(HTTP_2, sslCtx);
server.addHandler(new Handler(), REQ_PATH);
server.start();
System.out.println("Server started at " + server.getAddress());
REQ_URI_BASE = URIBuilder.newBuilder().scheme("https")
.loopback()
.port(server.getAddress().getPort())
.path(REQ_PATH)
.build().toString();
}
@AfterAll
static void afterAll() {
if (server != null) {
System.out.println("Stopping server at " + server.getAddress());
server.stop();
}
}
/**
* Verifies that when several requests are sent using send() and the server
* connection is configured to send a GOAWAY after processing only a few requests, then
* the remaining requests are retried on a different connection
*/
@Test
public void testSequential() throws Exception {
final LimitedPerConnRequestApprover reqApprover = new LimitedPerConnRequestApprover();
server.setRequestApprover(reqApprover::allowNewRequest);
try (final HttpClient client = HttpClient.newBuilder().version(HTTP_2)
.sslContext(sslCtx).build()) {
final String[] reqMethods = {"HEAD", "GET", "POST"};
for (final String reqMethod : reqMethods) {
final int numReqs = LimitedPerConnRequestApprover.MAX_REQS_PER_CONN + 3;
final Set<String> connectionKeys = new LinkedHashSet<>();
for (int i = 1; i <= numReqs; i++) {
final URI reqURI = new URI(REQ_URI_BASE + "?seq&" + reqMethod + "=" + i);
final HttpRequest req = HttpRequest.newBuilder()
.uri(reqURI)
.method(reqMethod, HttpRequest.BodyPublishers.noBody())
.build();
System.out.println("initiating request " + req);
final HttpResponse<String> resp = client.send(req, BodyHandlers.ofString());
final String respBody = resp.body();
System.out.println("received response: " + respBody);
assertEquals(200, resp.statusCode(),
"unexpected status code for request " + resp.request());
// response body is the logical key of the connection on which the
// request was handled
connectionKeys.add(respBody);
}
System.out.println("connections involved in handling the requests: "
+ connectionKeys);
// all requests have finished, we now just do a basic check that
// more than one connection was involved in processing these requests
assertEquals(2, connectionKeys.size(),
"unexpected number of connections " + connectionKeys);
}
} finally {
server.setRequestApprover(null); // reset
}
}
/**
* Verifies that when a server responds with a GOAWAY and then never processes the new retried
* requests on a new connection too, then the application code receives the request failure.
* This tests the send() API of the HttpClient.
*/
@Test
public void testUnprocessedRaisesException() throws Exception {
try (final HttpClient client = HttpClient.newBuilder().version(HTTP_2)
.sslContext(sslCtx).build()) {
final Random random = new Random();
final String[] reqMethods = {"HEAD", "GET", "POST"};
for (final String reqMethod : reqMethods) {
final int maxAllowedReqs = 2;
final int numReqs = maxAllowedReqs + 3; // 3 more requests than max allowed
// configure the approver
final LimitedRequestApprover reqApprover = new LimitedRequestApprover(maxAllowedReqs);
server.setRequestApprover(reqApprover::allowNewRequest);
try {
int numSuccess = 0;
int numFailed = 0;
for (int i = 1; i <= numReqs; i++) {
final String reqQueryPart = "?sync&" + reqMethod + "=" + i;
final URI reqURI = new URI(REQ_URI_BASE + reqQueryPart);
final HttpRequest req = HttpRequest.newBuilder()
.uri(reqURI)
.method(reqMethod, HttpRequest.BodyPublishers.noBody())
.build();
System.out.println("initiating request " + req);
if (i <= maxAllowedReqs) {
// expected to successfully complete
numSuccess++;
final HttpResponse<String> resp = client.send(req, BodyHandlers.ofString());
final String respBody = resp.body();
System.out.println("received response: " + respBody);
assertEquals(200, resp.statusCode(),
"unexpected status code for request " + resp.request());
} else {
// expected to fail as unprocessed
try {
final HttpResponse<String> resp = client.send(req, BodyHandlers.ofString());
fail("Request was expected to fail as unprocessed,"
+ " but got response: " + resp.body() + ", status code: "
+ resp.statusCode());
} catch (IOException ioe) {
// verify it failed for the right reason
if (ioe.getMessage() == null
|| !ioe.getMessage().contains("request not processed by peer")) {
// propagate the original failure
throw ioe;
}
numFailed++; // failed due to right reason
System.out.println("received expected failure: " + ioe
+ ", for request " + reqURI);
}
}
}
// verify the correct number of requests succeeded/failed
assertEquals(maxAllowedReqs, numSuccess, "unexpected number of requests succeeded");
assertEquals((numReqs - maxAllowedReqs), numFailed, "unexpected number of requests failed");
} finally {
server.setRequestApprover(null); // reset
}
}
}
}
/**
* Verifies that when a server responds with a GOAWAY and then never processes the new retried
* requests on a new connection too, then the application code receives the request failure.
* This tests the sendAsync() API of the HttpClient.
*/
@Test
public void testUnprocessedRaisesExceptionAsync() throws Throwable {
try (final HttpClient client = HttpClient.newBuilder().version(HTTP_2)
.sslContext(sslCtx).build()) {
final Random random = new Random();
final String[] reqMethods = {"HEAD", "GET", "POST"};
for (final String reqMethod : reqMethods) {
final int maxAllowedReqs = 2;
final int numReqs = maxAllowedReqs + 3; // 3 more requests than max allowed
// configure the approver
final LimitedRequestApprover reqApprover = new LimitedRequestApprover(maxAllowedReqs);
server.setRequestApprover(reqApprover::allowNewRequest);
try {
final List<Future<HttpResponse<String>>> futures = new ArrayList<>();
for (int i = 1; i <= numReqs; i++) {
final URI reqURI = new URI(REQ_URI_BASE + "?async&" + reqMethod + "=" + i);
final HttpRequest req = HttpRequest.newBuilder()
.uri(reqURI)
.method(reqMethod, HttpRequest.BodyPublishers.noBody())
.build();
System.out.println("initiating request " + req);
final Future<HttpResponse<String>> f = client.sendAsync(req, BodyHandlers.ofString());
futures.add(f);
}
// wait for responses
int numFailed = 0;
int numSuccess = 0;
for (int i = 1; i <= numReqs; i++) {
final String reqQueryPart = "?async&" + reqMethod + "=" + i;
try {
System.out.println("waiting response of request "
+ REQ_URI_BASE + reqQueryPart);
final HttpResponse<String> resp = futures.get(i - 1).get();
numSuccess++;
final String respBody = resp.body();
System.out.println("request: " + resp.request()
+ ", received response: " + respBody);
assertEquals(200, resp.statusCode(),
"unexpected status code for request " + resp.request());
} catch (ExecutionException ee) {
final Throwable cause = ee.getCause();
if (!(cause instanceof IOException ioe)) {
System.err.println("unexpected exception: " + cause
+ ", for request " + REQ_URI_BASE + reqQueryPart);
throw cause;
}
// verify it failed for the right reason
if (ioe.getMessage() == null
|| !ioe.getMessage().contains("request not processed by peer")) {
System.err.println("unexpected exception message: " + ioe.getMessage()
+ ", for request " + REQ_URI_BASE + reqQueryPart);
// propagate the original failure
throw ioe;
}
numFailed++; // failed due to the right reason
System.out.println("received expected failure: " + ioe
+ ", for request " + REQ_URI_BASE + reqQueryPart);
}
}
// verify the correct number of requests succeeded/failed
assertEquals(maxAllowedReqs, numSuccess, "unexpected number of requests succeeded");
assertEquals((numReqs - maxAllowedReqs), numFailed, "unexpected number of requests failed");
} finally {
server.setRequestApprover(null); // reset
}
}
}
}
// only allows fixed number of requests, irrespective of which server connection handles
// it. requests that are rejected will either be sent a GOAWAY on the connection
// or a RST_FRAME with a REFUSED_STREAM on the stream
private static final class LimitedRequestApprover {
private final int maxAllowedReqs;
private final AtomicInteger numApproved = new AtomicInteger();
private LimitedRequestApprover(final int maxAllowedReqs) {
this.maxAllowedReqs = maxAllowedReqs;
}
public boolean allowNewRequest(final String serverConnKey) {
final int approved = numApproved.incrementAndGet();
return approved <= maxAllowedReqs;
}
}
// allows a certain number of requests per server connection.
// requests that are rejected will either be sent a GOAWAY on the connection
// or a RST_FRAME with a REFUSED_STREAM on the stream
private static final class LimitedPerConnRequestApprover {
private static final int MAX_REQS_PER_CONN = 6;
private final Map<String, AtomicInteger> numApproved =
new ConcurrentHashMap<>();
private final Map<String, AtomicInteger> numDisapproved =
new ConcurrentHashMap<>();
public boolean allowNewRequest(final String serverConnKey) {
final AtomicInteger approved = numApproved.computeIfAbsent(serverConnKey,
(k) -> new AtomicInteger());
int curr = approved.get();
while (curr < MAX_REQS_PER_CONN) {
if (approved.compareAndSet(curr, curr + 1)) {
return true; // new request allowed
}
curr = approved.get();
}
final AtomicInteger disapproved = numDisapproved.computeIfAbsent(serverConnKey,
(k) -> new AtomicInteger());
final int numUnprocessed = disapproved.incrementAndGet();
System.out.println(approved.get() + " processed, "
+ numUnprocessed + " unprocessed requests on connection " + serverConnKey);
return false;
}
}
private static final class Handler implements HttpTestHandler {
@Override
public void handle(final HttpTestExchange exchange) throws IOException {
final String connectionKey = exchange.getConnectionKey();
System.out.println("responding to request: " + exchange.getRequestURI()
+ " on connection " + connectionKey);
final byte[] response = connectionKey.getBytes(UTF_8);
exchange.sendResponseHeaders(200, response.length);
try (final OutputStream os = exchange.getResponseBody()) {
os.write(response);
}
}
}
}

View file

@ -0,0 +1,224 @@
/*
* 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.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.HashSet;
import java.util.Set;
import java.util.stream.Stream;
import javax.net.ssl.SSLContext;
import jdk.httpclient.test.lib.common.HttpServerAdapters;
import jdk.test.lib.net.SimpleSSLContext;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static java.net.http.HttpClient.Version.HTTP_2;
/*
* @test id=default
* @bug 8372159
* @summary Verifies whether `SelectorManager` uses virtual threads
* as expected when no explicit configuration is provided
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.test.lib.net.SimpleSSLContext
* jdk.httpclient.test.lib.common.HttpServerAdapters
* @run junit/othervm
* -Djdk.httpclient.HttpClient.log=requests,responses,headers,errors
* H2SelectorVTTest
*/
/*
* @test id=never
* @bug 8372159
* @summary Verifies that `SelectorManager` does *not* use virtual threads
when explicitly configured to "never" use them
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.test.lib.net.SimpleSSLContext
* jdk.httpclient.test.lib.common.HttpServerAdapters
* @run junit/othervm
* -Djdk.internal.httpclient.tcp.selector.useVirtualThreads=never
* -Djdk.httpclient.HttpClient.log=requests,responses,headers,errors
* H2SelectorVTTest
*/
/*
* @test id=always
* @bug 8372159
* @summary Verifies that `SelectorManager` does *always* use virtual threads
when explicitly configured to "always" use them
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.test.lib.net.SimpleSSLContext
* jdk.httpclient.test.lib.common.HttpServerAdapters
* @run junit/othervm
* -Djdk.internal.httpclient.tcp.selector.useVirtualThreads=always
* -Djdk.httpclient.HttpClient.log=requests,responses,headers,errors
* H2SelectorVTTest
*/
/*
* @test id=explicit-default
* @bug 8372159
* @summary Verifies whether `SelectorManager` uses virtual threads
* as expected when `default` is explicitly configured
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.test.lib.net.SimpleSSLContext
* jdk.httpclient.test.lib.common.HttpServerAdapters
* @run junit/othervm
* -Djdk.internal.httpclient.tcp.selector.useVirtualThreads=default
* -Djdk.httpclient.HttpClient.log=requests,responses,headers,errors
* H2SelectorVTTest
*/
/*
* @test id=garbage
* @bug 8372159
* @summary Verifies whether `SelectorManager` uses virtual threads when
it is configured using an invalid value
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.test.lib.net.SimpleSSLContext
* jdk.httpclient.test.lib.common.HttpServerAdapters
* @run junit/othervm
* -Djdk.internal.httpclient.tcp.selector.useVirtualThreads=garbage
* -Djdk.httpclient.HttpClient.log=requests,responses,headers,errors
* H2SelectorVTTest
*/
// -Djava.security.debug=all
class H2SelectorVTTest implements HttpServerAdapters {
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
private static HttpTestServer h2Server;
private static String requestURI;
enum UseVTForSelector { ALWAYS, NEVER, DEFAULT }
private static final String PROP_NAME = "jdk.internal.httpclient.tcp.selector.useVirtualThreads";
private static final UseVTForSelector USE_VT_FOR_SELECTOR;
static {
String useVtForSelector =
System.getProperty(PROP_NAME, "default");
USE_VT_FOR_SELECTOR = Stream.of(UseVTForSelector.values())
.filter((v) -> v.name().equalsIgnoreCase(useVtForSelector))
.findFirst().orElse(UseVTForSelector.DEFAULT);
}
private static boolean isTCPSelectorThreadVirtual() {
return switch (USE_VT_FOR_SELECTOR) {
case ALWAYS -> true;
case NEVER -> false;
default -> true;
};
}
@BeforeAll
static void beforeClass() throws Exception {
// create a h2 server
h2Server = HttpTestServer.create(HTTP_2, sslContext);
h2Server.addHandler((exchange) -> exchange.sendResponseHeaders(200, 0), "/hello");
h2Server.start();
System.out.println("Server started at " + h2Server.getAddress());
requestURI = "https://" + h2Server.serverAuthority() + "/hello";
}
@AfterAll
static void afterClass() throws Exception {
if (h2Server != null) {
System.out.println("Stopping server " + h2Server.getAddress());
h2Server.stop();
}
}
/**
* Issues various HTTP/2 requests and verifies the responses are received
*/
@Test
void testBasicRequests() throws Exception {
try (final HttpClient client = HttpClient.newBuilder()
.proxy(NO_PROXY)
.sslContext(sslContext).build()) {
final URI reqURI = new URI(requestURI);
final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(reqURI);
// GET
final HttpRequest req1 = reqBuilder.copy().GET().build();
System.out.println("\nIssuing request: " + req1);
final HttpResponse<?> resp1 = client.send(req1, BodyHandlers.ofString());
assertEquals(200, resp1.statusCode(), "unexpected response code for GET request");
assertSelectorThread(client);
// POST
final HttpRequest req2 = reqBuilder.copy().POST(BodyPublishers.ofString("foo")).build();
System.out.println("\nIssuing request: " + req2);
final HttpResponse<?> resp2 = client.send(req2, BodyHandlers.ofString());
assertEquals(200, resp2.statusCode(), "unexpected response code for POST request");
assertSelectorThread(client);
// HEAD
final HttpRequest req3 = reqBuilder.copy().HEAD().build();
System.out.println("\nIssuing request: " + req3);
final HttpResponse<?> resp3 = client.send(req3, BodyHandlers.ofString());
assertEquals(200, resp3.statusCode(), "unexpected response code for HEAD request");
assertSelectorThread(client);
}
}
// This method attempts to determine whether the selector thread
// is a platform thread or a virtual thread, and throws if expectations
// ar not met.
// Since we don't have access to the selector thread, the method
// uses a roundabout way to figure this out: it enumerates all
// platform threads, and if it finds a thread whose name matches
// the expected name of the selector thread it concludes that the
// selector thread is a platform thread. Otherwise, it assumes
// that the thread is virtual.
private static void assertSelectorThread(HttpClient client) {
String cname = client.toString();
String clientId = cname.substring(cname.indexOf('(') + 1, cname.length() -1);
String name = "HttpClient-" + clientId + "-SelectorManager";
Set<String> threads = new HashSet<>(Thread.getAllStackTraces().keySet().stream()
.map(Thread::getName)
.toList());
boolean found = threads.contains(name);
String status = found == isTCPSelectorThreadVirtual() ? "ERROR" : "SUCCESS";
String propval = System.getProperty(PROP_NAME);
if (propval == null) {
System.out.printf("%s not defined, virtual=%s, thread found=%s%n",
PROP_NAME, isTCPSelectorThreadVirtual(), found);
} else {
System.out.printf("%s=%s, virtual=%s, thread found=%s%n",
PROP_NAME, propval, isTCPSelectorThreadVirtual(), found);
}
final String msg;
if (found) {
msg = "%s found in %s".formatted(name, threads);
System.out.printf("%s: %s%n", status, msg);
} else {
msg = "%s not found in %s".formatted(name, threads);
System.out.printf("%s: %s%n", status, msg);
}
assertEquals(!isTCPSelectorThreadVirtual(), found, msg);
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2017, 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.
*/
/*
* @test
* @bug 8153353
* @modules java.net.http/jdk.internal.net.http.hpack
* @key randomness
* @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java
* @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java
* @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java
* @run junit/othervm/timeout=240 java.net.http/jdk.internal.net.http.hpack.BinaryPrimitivesTest
*/
public class HpackBinaryTestDriver { }

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2016, 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.
*/
/*
* @test
* @bug 8153353
* @modules java.net.http/jdk.internal.net.http.hpack
* @key randomness
* @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java
* @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java
* @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java
* @run junit/othervm java.net.http/jdk.internal.net.http.hpack.CircularBufferTest
*/
public class HpackCircularBufferDriver { }

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2016, 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.
*/
/*
* @test
* @bug 8153353
* @modules java.net.http/jdk.internal.net.http.hpack
* @key randomness
* @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java
* @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java
* @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java
* @run junit/othervm java.net.http/jdk.internal.net.http.hpack.DecoderTest
*/
public class HpackDecoderDriver { }

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2016, 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.
*/
/*
* @test
* @bug 8153353
* @modules java.net.http/jdk.internal.net.http.hpack
* @key randomness
* @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java
* @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java
* @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java
* @run junit/othervm java.net.http/jdk.internal.net.http.hpack.EncoderTest
*/
public class HpackEncoderDriver { }

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2016, 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.
*/
/*
* @test
* @bug 8153353
* @modules java.net.http/jdk.internal.net.http.hpack
* jdk.localedata
* @key randomness
* @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java
* @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java
* @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java
* @run junit/othervm java.net.http/jdk.internal.net.http.hpack.HeaderTableTest
*/
public class HpackHeaderTableDriver { }

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2016, 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.
*/
/*
* @test
* @bug 8153353
* @modules java.net.http/jdk.internal.net.http.hpack
* @key randomness
* @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java
* @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java
* @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java
* @run junit/othervm/timeout=300 java.net.http/jdk.internal.net.http.hpack.HuffmanTest
*/
public class HpackHuffmanDriver { }

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2016, 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.
*/
/*
* @test
* @bug 8153353
* @modules java.net.http/jdk.internal.net.http.hpack
* @key randomness
* @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java
* @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java
* @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java
* @run junit/othervm java.net.http/jdk.internal.net.http.hpack.TestHelper
*/
public class HpackTestHelperDriver { }

View file

@ -0,0 +1,179 @@
/*
* 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.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import jdk.httpclient.test.lib.common.HttpServerAdapters;
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestExchange;
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer;
import jdk.internal.net.http.common.Utils;
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_2;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/*
* @test
* @bug 8312433
* @summary verify that the HttpClient's HTTP2 idle connection management doesn't close a connection
* when that connection has been handed out from the pool to a caller
* @library /test/jdk/java/net/httpclient/lib
* /test/lib
* @build jdk.httpclient.test.lib.common.HttpServerAdapters
* jdk.test.lib.Asserts
* @run junit/othervm -Djdk.internal.httpclient.debug=true
* -Djdk.httpclient.keepalive.timeout.h2=3
* IdlePooledConnectionTest
*/
public class IdlePooledConnectionTest {
private static final String ALL_OK_PATH = "/allOK";
private static HttpTestServer h2Server;
private static URI allOKUri;
private static final String H2_KEEPALIVE_TIMEOUT_PROP = "jdk.httpclient.keepalive.timeout.h2";
private static final String KEEPALIVE_TIMEOUT_PROP = "jdk.httpclient.keepalive.timeout";
@BeforeAll
static void beforeAll() throws Exception {
h2Server = HttpTestServer.create(HTTP_2);
h2Server.addHandler(new AllOKHandler(), ALL_OK_PATH);
h2Server.start();
System.err.println("Started H2 server at " + h2Server.serverAuthority());
allOKUri = new URI("http://" + h2Server.serverAuthority() + ALL_OK_PATH);
}
@AfterAll
static void afterAll() throws Exception {
if (h2Server != null) {
System.err.println("Stopping h2 server: " + h2Server.serverAuthority());
h2Server.stop();
}
}
// just returns a 200 HTTP response for all requests
private static final class AllOKHandler implements HttpServerAdapters.HttpTestHandler {
@Override
public void handle(final HttpTestExchange exchange) throws IOException {
System.err.println("Responding with 200 response code for request "
+ exchange.getRequestURI());
exchange.sendResponseHeaders(200, 0);
}
}
/*
* Issues a HTTP2 request against a server and expects it to succeed.
* The connection that was used is internally pooled by the HttpClient implementation.
* Then waits for the H2 idle connection timeout, before again firing several concurrent HTTP2
* requests against the same server. It is expected that all these requests complete
* successfully without running into a race condition where the H2 idle connection management
* closes the (pooled) connection during the time connection has been handed out to a caller
* and a new stream hasn't yet been created.
*/
@Test
public void testPooledConnection() throws Exception {
final Duration h2TimeoutDuration = getEffectiveH2IdleTimeoutDuration();
assertNotNull(h2TimeoutDuration, "H2 idle connection timeout cannot be null");
// the wait time, which represents the time to wait before firing off additional requests,
// is intentionally a few milliseconds smaller than the h2 idle connection timeout,
// to allow for the requests to reach the place where connection checkout from the pool
// happens and thus allow the code to race with the idle connection timer task
// closing the connection.
final long waitTimeMillis = TimeUnit.of(ChronoUnit.MILLIS).convert(h2TimeoutDuration) - 5;
try (final HttpClient client = HttpClient.newBuilder().proxy(NO_PROXY).build()) {
final HttpRequest request = HttpRequest.newBuilder(allOKUri)
.GET().version(HTTP_2).build();
// keep ready the additional concurrent requests that we will fire later.
// we do this now so that when it's time to fire off these additional requests,
// this main thread does as little work as possible to increase the chances of a
// race condition in idle connection management closing a pooled connection
// and new requests being fired
final Callable<HttpResponse<Void>> task = () -> client.send(request,
BodyHandlers.discarding());
final List<Callable<HttpResponse<Void>>> tasks = new ArrayList<>();
final int numAdditionalReqs = 20;
for (int i = 0; i < numAdditionalReqs; i++) {
tasks.add(task);
}
// issue the first request
System.err.println("issuing first request: " + request);
final HttpResponse<Void> firstResp = client.send(request, BodyHandlers.discarding());
assertEquals(200, firstResp.statusCode(), "unexpected response code for request "
+ request);
System.err.println("waiting for " + waitTimeMillis + " milli seconds" +
" before issuing additional requests");
Thread.sleep(waitTimeMillis);
// issue additional concurrent requests
final List<Future<HttpResponse<Void>>> responses;
try (final ExecutorService executor = Executors.newFixedThreadPool(numAdditionalReqs)) {
responses = executor.invokeAll(tasks);
}
System.err.println("All " + responses.size() + " requests completed, now" +
" verifying each response");
// verify all requests succeeded
for (final Future<HttpResponse<Void>> future : responses) {
final HttpResponse<Void> rsp = future.get();
assertEquals(200, rsp.statusCode(), "unexpected response code for request "
+ request);
}
}
}
// returns the effective idle timeout duration of a HTTP2 connection
private static Duration getEffectiveH2IdleTimeoutDuration() {
final long keepAliveTimeoutInSecs = getNetProp(KEEPALIVE_TIMEOUT_PROP, 30);
final long h2TimeoutInSecs = getNetProp(H2_KEEPALIVE_TIMEOUT_PROP, keepAliveTimeoutInSecs);
return Duration.of(h2TimeoutInSecs, ChronoUnit.SECONDS);
}
private static long getNetProp(final String prop, final long def) {
final String s = Utils.getNetProperty(prop);
if (s == null) {
return def;
}
try {
final long timeoutVal = Long.parseLong(s);
return timeoutVal >= 0 ? timeoutVal : def;
} catch (NumberFormatException ignored) {
return def;
}
}
}

View file

@ -0,0 +1,186 @@
/*
* Copyright (c) 2018, 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.
*/
/*
* @test
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer
* @run junit/othervm
* -Djdk.internal.httpclient.debug=true
* -Djdk.httpclient.HttpClient.log=errors,requests,responses,trace
* ImplicitPushCancel
*/
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.net.http.HttpResponse.PushPromiseHandler;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import jdk.httpclient.test.lib.http2.Http2TestServer;
import jdk.httpclient.test.lib.http2.Http2TestExchange;
import jdk.httpclient.test.lib.http2.Http2Handler;
import static java.nio.charset.StandardCharsets.UTF_8;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class ImplicitPushCancel {
static final Map<String,String> PUSH_PROMISES = Map.of(
"/x/y/z/1", "the first push promise body",
"/x/y/z/2", "the second push promise body",
"/x/y/z/3", "the third push promise body",
"/x/y/z/4", "the fourth push promise body",
"/x/y/z/5", "the fifth push promise body",
"/x/y/z/6", "the sixth push promise body",
"/x/y/z/7", "the seventh push promise body",
"/x/y/z/8", "the eight push promise body",
"/x/y/z/9", "the ninth push promise body"
);
static final String MAIN_RESPONSE_BODY = "the main response body";
private static Http2TestServer server;
private static URI uri;
@BeforeAll
public static void setup() throws Exception {
server = new Http2TestServer(false, 0);
Http2Handler handler = new ServerPushHandler(MAIN_RESPONSE_BODY,
PUSH_PROMISES);
server.addHandler(handler, "/");
server.start();
int port = server.getAddress().getPort();
System.err.println("Server listening on port " + port);
uri = new URI("http://localhost:" + port + "/foo/a/b/c");
}
@AfterAll
public static void teardown() {
server.stop();
}
static final <T> HttpResponse<T> assert200ResponseCode(HttpResponse<T> response) {
assertEquals(200, response.statusCode());
return response;
}
/*
* With a handler not capable of accepting push promises, then all push
* promises should be rejected / cancelled, without interfering with the
* main response.
*/
@Test
public void test() throws Exception {
HttpClient client = HttpClient.newHttpClient();
client.sendAsync(HttpRequest.newBuilder(uri).build(), BodyHandlers.ofString())
.thenApply(ImplicitPushCancel::assert200ResponseCode)
.thenApply(HttpResponse::body)
.thenAccept(body -> body.equals(MAIN_RESPONSE_BODY))
.join();
ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<String>>> promises
= new ConcurrentHashMap<>();
PushPromiseHandler<String> pph = PushPromiseHandler
.of((r) -> BodyHandlers.ofString(), promises);
HttpResponse<String> main = client.sendAsync(
HttpRequest.newBuilder(uri).build(),
BodyHandlers.ofString(),
pph)
.join();
promises.entrySet().stream().forEach(e -> System.out.println(e.getKey() + ":" + e.getValue().join().body()));
promises.putIfAbsent(main.request(), CompletableFuture.completedFuture(main));
promises.entrySet().stream().forEach(entry -> {
HttpRequest request = entry.getKey();
HttpResponse<String> response = entry.getValue().join();
assertEquals(200, response.statusCode());
if (PUSH_PROMISES.containsKey(request.uri().getPath())) {
assertEquals(PUSH_PROMISES.get(request.uri().getPath()), response.body());
} else {
assertEquals(MAIN_RESPONSE_BODY, response.body());
}
} );
}
// --- server push handler ---
static class ServerPushHandler implements Http2Handler {
private final String mainResponseBody;
private final Map<String,String> promises;
public ServerPushHandler(String mainResponseBody,
Map<String,String> promises)
throws Exception
{
Objects.requireNonNull(promises);
this.mainResponseBody = mainResponseBody;
this.promises = promises;
}
public void handle(Http2TestExchange exchange) throws IOException {
System.err.println("Server: handle " + exchange);
try (InputStream is = exchange.getRequestBody()) {
is.readAllBytes();
}
if (exchange.serverPushAllowed()) {
pushPromises(exchange);
}
// response data for the main response
try (OutputStream os = exchange.getResponseBody()) {
byte[] bytes = mainResponseBody.getBytes(UTF_8);
exchange.sendResponseHeaders(200, bytes.length);
os.write(bytes);
}
}
private void pushPromises(Http2TestExchange exchange) throws IOException {
URI requestURI = exchange.getRequestURI();
for (Map.Entry<String,String> promise : promises.entrySet()) {
URI uri = requestURI.resolve(promise.getKey());
InputStream is = new ByteArrayInputStream(promise.getValue().getBytes(UTF_8));
HttpHeaders headers = HttpHeaders.of(Collections.emptyMap(), (x, y) -> true);
exchange.serverPush(uri, headers, is);
}
System.err.println("Server: All pushes sent");
}
}
}

View file

@ -0,0 +1,220 @@
/*
* Copyright (c) 2015, 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.
*/
/*
* @test
* @bug 8087112
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer
* @run junit/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors
* -Djdk.internal.httpclient.debug=true
* NoBodyTest
*/
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.*;
import javax.net.ssl.*;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
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.concurrent.*;
import jdk.httpclient.test.lib.http2.Http2TestServer;
import jdk.httpclient.test.lib.http2.Http2TestExchange;
import jdk.httpclient.test.lib.http2.Http2Handler;
import jdk.test.lib.net.SimpleSSLContext;
import static java.net.http.HttpClient.Version.HTTP_2;
import org.junit.jupiter.api.Test;
public class NoBodyTest {
static int httpPort, httpsPort;
static Http2TestServer httpServer, httpsServer;
static HttpClient client = null;
static ExecutorService clientExec;
static ExecutorService serverExec;
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
static String TEST_STRING = "The quick brown fox jumps over the lazy dog ";
static String httpURIString, httpsURIString;
static void initialize() throws Exception {
try {
client = getClient();
httpServer = new Http2TestServer(false, 0, serverExec, sslContext);
httpServer.addHandler(new Handler(), "/");
httpPort = httpServer.getAddress().getPort();
httpsServer = new Http2TestServer(true, 0, serverExec, sslContext);
httpsServer.addHandler(new Handler(), "/");
httpsPort = httpsServer.getAddress().getPort();
httpURIString = "http://localhost:" + httpPort + "/foo/";
httpsURIString = "https://localhost:" + httpsPort + "/bar/";
httpServer.start();
httpsServer.start();
} catch (Throwable e) {
System.err.println("Throwing now");
e.printStackTrace(System.err);
throw e;
}
}
@Test
public void runtest() throws Exception {
try {
initialize();
warmup(false);
warmup(true);
test(false);
test(true);
} catch (Throwable tt) {
System.err.println("tt caught");
tt.printStackTrace(System.err);
throw tt;
} finally {
httpServer.stop();
httpsServer.stop();
}
}
static HttpClient getClient() {
if (client == null) {
serverExec = Executors.newCachedThreadPool();
clientExec = Executors.newCachedThreadPool();
client = HttpClient.newBuilder()
.executor(clientExec)
.sslContext(sslContext)
.version(HTTP_2)
.build();
}
return client;
}
static URI getURI(boolean secure) {
if (secure)
return URI.create(httpsURIString);
else
return URI.create(httpURIString);
}
static void checkStatus(int expected, int found) throws Exception {
if (expected != found) {
System.err.printf ("Test failed: wrong status code %d/%d\n",
expected, found);
throw new RuntimeException("Test failed");
}
}
static void checkStrings(String expected, String found) throws Exception {
if (!expected.equals(found)) {
System.err.printf ("Test failed: wrong string %s/%s\n",
expected, found);
throw new RuntimeException("Test failed");
}
}
static final int LOOPS = 13;
static void warmup(boolean secure) throws Exception {
URI uri = getURI(secure);
String type = secure ? "https" : "http";
System.err.println("Request to " + uri);
// Do a simple warmup request
HttpClient client = getClient();
HttpRequest req = HttpRequest.newBuilder(uri)
.POST(BodyPublishers.ofString("Random text"))
.build();
HttpResponse<String> response = client.send(req, BodyHandlers.ofString());
checkStatus(200, response.statusCode());
String responseBody = response.body();
HttpHeaders h = response.headers();
checkStrings(TEST_STRING + type, responseBody);
}
static void test(boolean secure) throws Exception {
URI uri = getURI(secure);
String type = secure ? "https" : "http";
System.err.println("Request to " + uri);
HttpRequest request = HttpRequest.newBuilder(uri)
.POST(BodyPublishers.ofString(TEST_STRING))
.build();
for (int i = 0; i < LOOPS; i++) {
System.out.println("Loop " + i);
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
int expectedResponse = (i % 2) == 0 ? 204 : 200;
if (response.statusCode() != expectedResponse)
throw new RuntimeException("wrong response code " + Integer.toString(response.statusCode()));
if (expectedResponse == 200 && !response.body().equals(TEST_STRING + type)) {
System.err.printf("response received/expected %s/%s\n", response.body(), TEST_STRING + type);
throw new RuntimeException("wrong response body");
}
}
System.err.println("test: DONE");
}
static class Handler implements Http2Handler {
public Handler() {}
volatile int invocation = 0;
@Override
public void handle(Http2TestExchange t)
throws IOException {
try {
URI uri = t.getRequestURI();
System.err.printf("Handler received request to %s from %s\n",
uri, t.getRemoteAddress());
String type = uri.getScheme().toLowerCase();
InputStream is = t.getRequestBody();
while (is.read() != -1);
is.close();
// every second response is 204.
if ((invocation++ % 2) == 1) {
System.err.println("Server sending 204");
t.sendResponseHeaders(204, -1);
} else {
String body = TEST_STRING + type;
t.sendResponseHeaders(200, body.length());
OutputStream os = t.getResponseBody();
os.write(body.getBytes());
os.close();
}
} catch (Throwable e) {
e.printStackTrace(System.err);
throw new IOException(e);
}
}
}
}

View file

@ -0,0 +1,171 @@
/*
* 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.
*/
/*
* @test
* @bug 8293786
* @summary Checks to see if the HttpClient can process a request to cancel a transmission from a remote if the server
* does not process any data. The client should read all data from the server and close the connection.
* @library /test/jdk/java/net/httpclient/lib
* @build jdk.httpclient.test.lib.http2.Http2TestServer
* @run junit/othervm/timeout=50 -Djdk.httpclient.HttpClient.log=all
* PostPutTest
*/
import jdk.httpclient.test.lib.http2.Http2Handler;
import jdk.httpclient.test.lib.http2.Http2TestExchange;
import jdk.httpclient.test.lib.http2.Http2TestServer;
import java.io.IOException;
import java.io.PrintStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import static java.net.http.HttpClient.Version.HTTP_2;
import static java.net.http.HttpRequest.BodyPublishers.ofByteArray;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
public class PostPutTest {
private static Http2TestServer http2TestServer;
private static URI warmupURI, testHandlerBasicURI, testHandlerCloseBosURI, testHandleNegativeContentLengthURI;
static PrintStream testLog = System.err;
// As per jdk.internal.net.http.WindowController.DEFAULT_INITIAL_WINDOW_SIZE
private static final int DEFAULT_INITIAL_WINDOW_SIZE = (64 * 1024) - 1;
// Add on a small amount of arbitrary bytes to see if client hangs when receiving RST_STREAM
private static byte[] data = new byte[DEFAULT_INITIAL_WINDOW_SIZE + 10];
@BeforeAll
public static void setup() throws Exception {
http2TestServer = new Http2TestServer(false, 0);
http2TestServer.addHandler(new WarmupHandler(), "/Warmup");
http2TestServer.addHandler(new TestHandlerBasic(), "/TestHandlerBasic");
http2TestServer.addHandler(new TestHandlerCloseBos(), "/TestHandlerCloseBos");
http2TestServer.addHandler(new TestHandleNegativeContentLength(), "/TestHandleNegativeContentLength");
http2TestServer.start();
testLog.println("PostPutTest.setup(): Starting server");
warmupURI = new URI("http://" + http2TestServer.serverAuthority() + "/Warmup");
testHandlerBasicURI = new URI("http://" + http2TestServer.serverAuthority() + "/TestHandlerBasic");
testHandlerCloseBosURI = new URI("http://" + http2TestServer.serverAuthority() + "/TestHandlerCloseBos");
testHandleNegativeContentLengthURI = new URI("http://" + http2TestServer.serverAuthority() + "/TestHandleNegativeContentLength");
testLog.println("PostPutTest.setup(): warmupURI: " + warmupURI);
testLog.println("PostPutTest.setup(): testHandlerBasicURI: " + testHandlerBasicURI);
testLog.println("PostPutTest.setup(): testHandlerCloseBosURI: " + testHandlerCloseBosURI);
testLog.println("PostPutTest.setup(): testHandleNegativeContentLengthURI: " + testHandleNegativeContentLengthURI);
}
@AfterAll
public static void teardown() {
testLog.println("PostPutTest.teardown(): Stopping server");
http2TestServer.stop();
data = null;
}
public static Object[][] variants() {
HttpRequest over64kPost, over64kPut, over64kPostCloseBos, over64kPutCloseBos, over64kPostNegativeContentLength, over64kPutNegativeContentLength;
over64kPost = HttpRequest.newBuilder().version(HTTP_2).POST(ofByteArray(data)).uri(testHandlerBasicURI).build();
over64kPut = HttpRequest.newBuilder().version(HTTP_2).PUT(ofByteArray(data)).uri(testHandlerBasicURI).build();
over64kPostCloseBos = HttpRequest.newBuilder().version(HTTP_2).POST(ofByteArray(data)).uri(testHandlerCloseBosURI).build();
over64kPutCloseBos = HttpRequest.newBuilder().version(HTTP_2).PUT(ofByteArray(data)).uri(testHandlerCloseBosURI).build();
over64kPostNegativeContentLength = HttpRequest.newBuilder().version(HTTP_2).POST(ofByteArray(data)).uri(testHandleNegativeContentLengthURI).build();
over64kPutNegativeContentLength = HttpRequest.newBuilder().version(HTTP_2).PUT(ofByteArray(data)).uri(testHandleNegativeContentLengthURI).build();
return new Object[][] {
{ over64kPost, "POST data over 64k bytes" },
{ over64kPut, "PUT data over 64k bytes" },
{ over64kPostCloseBos, "POST data over 64k bytes with close bos" },
{ over64kPutCloseBos, "PUT data over 64k bytes with close bos" },
{ over64kPostNegativeContentLength, "POST data over 64k bytes with negative content length" },
{ over64kPutNegativeContentLength, "PUT data over 64k bytes with negative content length" }
};
}
public HttpRequest getWarmupReq() {
return HttpRequest.newBuilder()
.GET()
.uri(warmupURI)
.build();
}
@ParameterizedTest
@MethodSource("variants")
public void testOver64kPUT(HttpRequest req, String testMessage) {
testLog.println("PostPutTest: Performing test: " + testMessage);
HttpClient hc = HttpClient.newBuilder().version(HTTP_2).build();
hc.sendAsync(getWarmupReq(), HttpResponse.BodyHandlers.ofString()).join();
hc.sendAsync(req, HttpResponse.BodyHandlers.ofString()).join();
/*
If this test fails in timeout, it is likely due to one of two reasons:
- The responseSubscriber is null, so no incoming frames are being processed by the client
(See Stream::schedule)
- The test server is for some reason not sending a RST_STREAM with the NO_ERROR flag set after
sending an empty DATA frame with the END_STREAM flag set.
*/
}
private static class TestHandlerBasic implements Http2Handler {
@Override
public void handle(Http2TestExchange exchange) throws IOException {
// The input stream is not read in this bug as this will trigger window updates for the server. This bug
// concerns the case where no updates are sent and the server instead tells the client to abort the transmission.
exchange.sendResponseHeaders(200, 0);
}
}
private static class TestHandlerCloseBos implements Http2Handler {
@Override
public void handle(Http2TestExchange exchange) throws IOException {
// This case does actually cause the test to hang due to the body input stream being closed before it can send
// the RST_STREAM frame.
exchange.sendResponseHeaders(200, 0);
exchange.getResponseBody().close();
}
}
private static class TestHandleNegativeContentLength implements Http2Handler {
@Override
public void handle(Http2TestExchange exchange) throws IOException {
exchange.sendResponseHeaders(200, -1);
}
}
private static class WarmupHandler implements Http2Handler {
@Override
public void handle(Http2TestExchange exchange) throws IOException {
exchange.sendResponseHeaders(200, 0);
}
}
}

View file

@ -0,0 +1,323 @@
/*
* Copyright (c) 2017, 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 com.sun.net.httpserver.HttpsConfigurator;
import com.sun.net.httpserver.HttpsParameters;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import jdk.httpclient.test.lib.http2.Http2TestServer;
import jdk.httpclient.test.lib.http2.Http2TestExchange;
import jdk.httpclient.test.lib.http2.Http2Handler;
import jdk.test.lib.net.SimpleSSLContext;
import java.util.concurrent.*;
/**
* @test
* @bug 8181422
* @summary Verifies that you can access an HTTP/2 server over HTTPS by
* tunnelling through an HTTP/1.1 proxy.
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer
* @run main/othervm ProxyTest2
* @author danielfuchs
*/
public class ProxyTest2 {
static {
SSLContext.setDefault(SimpleSSLContext.findSSLContext());
}
static final String RESPONSE = "<html><body><p>Hello World!</body></html>";
static final String PATH = "/foo/";
static Http2TestServer createHttpsServer(ExecutorService exec) throws Exception {
Http2TestServer server = new Http2TestServer(true, 0, exec, SSLContext.getDefault());
server.addHandler(new Http2Handler() {
@Override
public void handle(Http2TestExchange he) throws IOException {
he.getResponseHeaders().addHeader("encoding", "UTF-8");
he.sendResponseHeaders(200, RESPONSE.length());
he.getResponseBody().write(RESPONSE.getBytes(StandardCharsets.UTF_8));
he.close();
}
}, PATH);
return server;
}
public static void main(String[] args)
throws Exception
{
ExecutorService exec = Executors.newCachedThreadPool();
Http2TestServer server = createHttpsServer(exec);
server.start();
try {
// Http2TestServer over HTTPS does not support HTTP/1.1
// => only test with a HTTP/2 client
test(server, HttpClient.Version.HTTP_2);
} finally {
server.stop();
exec.shutdown();
System.out.println("Server stopped");
}
}
public static void test(Http2TestServer server, HttpClient.Version version)
throws Exception
{
System.out.println("Server is: " + server.getAddress().toString());
URI uri = new URI("https://localhost:" + server.getAddress().getPort() + PATH + "x");
TunnelingProxy proxy = new TunnelingProxy(server);
proxy.start();
try {
System.out.println("Proxy started");
Proxy p = new Proxy(Proxy.Type.HTTP,
InetSocketAddress.createUnresolved("localhost", proxy.getAddress().getPort()));
System.out.println("Setting up request with HttpClient for version: "
+ version.name() + "URI=" + uri);
ProxySelector ps = ProxySelector.of(
InetSocketAddress.createUnresolved("localhost", proxy.getAddress().getPort()));
HttpClient client = HttpClient.newBuilder()
.version(version)
.proxy(ps)
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(uri)
.GET()
.build();
System.out.println("Sending request with HttpClient");
HttpResponse<String> response
= client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Got response");
String resp = response.body();
System.out.println("Received: " + resp);
if (!RESPONSE.equals(resp)) {
throw new AssertionError("Unexpected response");
}
} finally {
System.out.println("Stopping proxy");
proxy.stop();
System.out.println("Proxy stopped");
}
}
static class TunnelingProxy {
final Thread accept;
final ServerSocket ss;
final boolean DEBUG = false;
final Http2TestServer serverImpl;
final CopyOnWriteArrayList<CompletableFuture<Void>> connectionCFs
= new CopyOnWriteArrayList<>();
private volatile boolean stopped;
TunnelingProxy(Http2TestServer serverImpl) throws IOException {
this.serverImpl = serverImpl;
ss = new ServerSocket();
accept = new Thread(this::accept);
accept.setDaemon(true);
}
void start() throws IOException {
ss.setReuseAddress(false);
ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
accept.start();
}
// Pipe the input stream to the output stream.
private synchronized Thread pipe(InputStream is, OutputStream os,
char tag, CompletableFuture<Void> end) {
return new Thread("TunnelPipe("+tag+")") {
@Override
public void run() {
try {
try {
int len;
byte[] buf = new byte[16 * 1024];
while ((len = is.read(buf)) != -1) {
os.write(buf, 0, len);
os.flush();
// if DEBUG prints a + or a - for each transferred
// character.
if (DEBUG) System.out.print(String.valueOf(tag).repeat(len));
}
is.close();
} finally {
os.close();
}
} catch (IOException ex) {
if (DEBUG) ex.printStackTrace(System.out);
} finally {
end.complete(null);
}
}
};
}
public InetSocketAddress getAddress() {
return new InetSocketAddress( InetAddress.getLoopbackAddress(), ss.getLocalPort());
}
// This is a bit shaky. It doesn't handle continuation
// lines, but our client shouldn't send any.
// Read a line from the input stream, swallowing the final
// \r\n sequence. Stops at the first \n, doesn't complain
// if it wasn't preceded by '\r'.
//
String readLine(InputStream r) throws IOException {
StringBuilder b = new StringBuilder();
int c;
while ((c = r.read()) != -1) {
if (c == '\n') break;
b.appendCodePoint(c);
}
if (b.codePointAt(b.length() -1) == '\r') {
b.delete(b.length() -1, b.length());
}
return b.toString();
}
public void accept() {
Socket clientConnection = null;
try {
while (!stopped) {
System.out.println("Tunnel: Waiting for client");
Socket toClose;
try {
toClose = clientConnection = ss.accept();
} catch (IOException io) {
if (DEBUG) io.printStackTrace(System.out);
break;
}
System.out.println("Tunnel: Client accepted");
Socket targetConnection = null;
InputStream ccis = clientConnection.getInputStream();
OutputStream ccos = clientConnection.getOutputStream();
Writer w = new OutputStreamWriter(ccos, "UTF-8");
PrintWriter pw = new PrintWriter(w);
System.out.println("Tunnel: Reading request line");
String requestLine = readLine(ccis);
System.out.println("Tunnel: Request status line: " + requestLine);
if (requestLine.startsWith("CONNECT ")) {
// We should probably check that the next word following
// CONNECT is the host:port of our HTTPS serverImpl.
// Some improvement for a followup!
// Read all headers until we find the empty line that
// signals the end of all headers.
while(!requestLine.equals("")) {
System.out.println("Tunnel: Reading header: "
+ (requestLine = readLine(ccis)));
}
// Open target connection
targetConnection = new Socket(
InetAddress.getLoopbackAddress(),
serverImpl.getAddress().getPort());
// Then send the 200 OK response to the client
System.out.println("Tunnel: Sending "
+ "HTTP/1.1 200 OK\r\n\r\n");
pw.print("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
pw.flush();
} else {
// This should not happen. If it does then just print an
// error - both on out and err, and close the accepted
// socket
System.out.println("WARNING: Tunnel: Unexpected status line: "
+ requestLine + " received by "
+ ss.getLocalSocketAddress()
+ " from "
+ toClose.getRemoteSocketAddress()
+ " - closing accepted socket");
// Print on err
System.err.println("WARNING: Tunnel: Unexpected status line: "
+ requestLine + " received by "
+ ss.getLocalSocketAddress()
+ " from "
+ toClose.getRemoteSocketAddress());
// close accepted socket.
toClose.close();
System.err.println("Tunnel: accepted socket closed.");
continue;
}
// Pipe the input stream of the client connection to the
// output stream of the target connection and conversely.
// Now the client and target will just talk to each other.
System.out.println("Tunnel: Starting tunnel pipes");
CompletableFuture<Void> end, end1, end2;
Thread t1 = pipe(ccis, targetConnection.getOutputStream(), '+',
end1 = new CompletableFuture<>());
Thread t2 = pipe(targetConnection.getInputStream(), ccos, '-',
end2 = new CompletableFuture<>());
end = CompletableFuture.allOf(end1, end2);
end.whenComplete(
(r,t) -> {
try { toClose.close(); } catch (IOException x) { }
finally {connectionCFs.remove(end);}
});
connectionCFs.add(end);
t1.start();
t2.start();
}
} catch (Throwable ex) {
try {
ss.close();
} catch (IOException ex1) {
ex.addSuppressed(ex1);
}
ex.printStackTrace(System.err);
} finally {
System.out.println("Tunnel: exiting (stopped=" + stopped + ")");
connectionCFs.forEach(cf -> cf.complete(null));
}
}
public void stop() throws IOException {
stopped = true;
ss.close();
}
}
}

View file

@ -0,0 +1,367 @@
/*
* Copyright (c) 2022, 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.
*/
/*
* @test
* @bug 8263031
* @summary Tests that the HttpClient can correctly receive a Push Promise
* Frame with the END_HEADERS flag unset followed by one or more
* Continuation Frames.
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer
* jdk.httpclient.test.lib.http2.BodyOutputStream
* jdk.httpclient.test.lib.http2.OutgoingPushPromise
* @run junit/othervm PushPromiseContinuation
*/
import javax.net.ssl.SSLSession;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ProtocolException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.BiPredicate;
import jdk.httpclient.test.lib.http2.Http2TestServer;
import jdk.httpclient.test.lib.http2.Http2TestExchange;
import jdk.httpclient.test.lib.http2.Http2TestExchangeImpl;
import jdk.httpclient.test.lib.http2.Http2Handler;
import jdk.httpclient.test.lib.http2.BodyOutputStream;
import jdk.httpclient.test.lib.http2.OutgoingPushPromise;
import jdk.httpclient.test.lib.http2.Http2TestServerConnection;
import jdk.internal.net.http.common.HttpHeadersBuilder;
import jdk.internal.net.http.frame.ContinuationFrame;
import jdk.internal.net.http.frame.HeaderFrame;
import static java.nio.charset.StandardCharsets.UTF_8;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class PushPromiseContinuation {
static volatile HttpHeaders testHeaders;
static volatile HttpHeadersBuilder testHeadersBuilder;
static volatile int continuationCount;
static final String mainPromiseBody = "Main Promise Body";
static final String mainResponseBody = "Main Response Body";
private static Http2TestServer server;
private static URI uri;
// Set up simple client-side push promise handler
ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<String>>> pushPromiseMap = new ConcurrentHashMap<>();
HttpResponse.PushPromiseHandler<String> pph = (initial, pushRequest, acceptor) -> {
HttpResponse.BodyHandler<String> s = HttpResponse.BodyHandlers.ofString(UTF_8);
pushPromiseMap.put(pushRequest, acceptor.apply(s));
};
@BeforeEach
public void beforeMethod() {
pushPromiseMap = new ConcurrentHashMap<>();
}
@BeforeAll
public static void setup() throws Exception {
server = new Http2TestServer(false, 0);
server.addHandler(new ServerPushHandler(), "/");
// Need to have a custom exchange supplier to manage the server's push
// promise with continuation flow
server.setExchangeSupplier(Http2PushPromiseContinuationExchangeImpl::new);
System.err.println("PushPromiseContinuation: Server listening on port " + server.getAddress().getPort());
server.start();
int port = server.getAddress().getPort();
uri = new URI("http://localhost:" + port + "/");
}
@AfterAll
public static void teardown() {
server.stop();
}
/**
* Tests that when the client receives PushPromise Frame with the END_HEADERS
* flag set to 0x0 and subsequently receives a continuation frame, no exception
* is thrown and all headers from the PushPromise and Continuation Frames sent
* by the server arrive at the client.
*/
@Test
public void testOneContinuation() {
continuationCount = 1;
HttpClient client = HttpClient.newHttpClient();
// Carry out request
HttpRequest hreq = HttpRequest.newBuilder(uri).version(HttpClient.Version.HTTP_2).GET().build();
CompletableFuture<HttpResponse<String>> cf =
client.sendAsync(hreq, HttpResponse.BodyHandlers.ofString(UTF_8), pph);
HttpResponse<String> resp = cf.join();
// Verify results
verify(resp);
}
/**
* Same as above, but tests for the case where two Continuation Frames are sent
* with the END_HEADERS flag set only on the last frame.
*/
@Test
public void testTwoContinuations() {
continuationCount = 2;
HttpClient client = HttpClient.newHttpClient();
// Carry out request
HttpRequest hreq = HttpRequest.newBuilder(uri).version(HttpClient.Version.HTTP_2).GET().build();
CompletableFuture<HttpResponse<String>> cf =
client.sendAsync(hreq, HttpResponse.BodyHandlers.ofString(UTF_8), pph);
HttpResponse<String> resp = cf.join();
// Verify results
verify(resp);
}
@Test
public void testThreeContinuations() {
continuationCount = 3;
HttpClient client = HttpClient.newHttpClient();
// Carry out request
HttpRequest hreq = HttpRequest.newBuilder(uri).version(HttpClient.Version.HTTP_2).GET().build();
CompletableFuture<HttpResponse<String>> cf =
client.sendAsync(hreq, HttpResponse.BodyHandlers.ofString(UTF_8), pph);
HttpResponse<String> resp = cf.join();
// Verify results
verify(resp);
}
@Test
public void testSendHeadersOnPushPromiseStream() throws Exception {
// This test server sends a push promise that should be followed by a continuation but
// incorrectly sends on Response Headers while the client awaits the continuation.
Http2TestServer faultyServer = new Http2TestServer(false, 0);
faultyServer.addHandler(new ServerPushHandler(), "/");
faultyServer.setExchangeSupplier(Http2PushPromiseHeadersExchangeImpl::new);
System.err.println("PushPromiseContinuation: FaultyServer listening on port " + faultyServer.getAddress().getPort());
faultyServer.start();
int faultyPort = faultyServer.getAddress().getPort();
URI faultyUri = new URI("http://localhost:" + faultyPort + "/");
HttpClient client = HttpClient.newHttpClient();
// Server is making a request to an incorrect URI
HttpRequest hreq = HttpRequest.newBuilder(faultyUri).version(HttpClient.Version.HTTP_2).GET().build();
CompletableFuture<HttpResponse<String>> cf =
client.sendAsync(hreq, HttpResponse.BodyHandlers.ofString(UTF_8), pph);
CompletionException t = assertThrows(CompletionException.class, () -> cf.join());
assertEquals(ProtocolException.class, t.getCause().getClass(),
"Expected a ProtocolException but got " + t.getCause());
System.err.println("Client received the following expected exception: " + t.getCause());
faultyServer.stop();
}
private void verify(HttpResponse<String> resp) {
assertEquals(200, resp.statusCode());
assertEquals(mainResponseBody, resp.body());
if (pushPromiseMap.size() > 1) {
System.err.println(pushPromiseMap.entrySet());
fail("Results map size is greater than 1");
} else {
// This will only iterate once
for (HttpRequest r : pushPromiseMap.keySet()) {
HttpResponse<String> serverPushResp = pushPromiseMap.get(r).join();
// Received headers should be the same as the combined PushPromise
// frame headers combined with the Continuation frame headers
assertEquals(r.headers(), testHeaders);
// Check status code and push promise body are as expected
assertEquals(200, serverPushResp.statusCode());
assertEquals(mainPromiseBody, serverPushResp.body());
}
}
}
static class Http2PushPromiseHeadersExchangeImpl extends Http2TestExchangeImpl {
Http2PushPromiseHeadersExchangeImpl(int streamid, String method, HttpHeaders reqheaders,
HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is,
SSLSession sslSession, BodyOutputStream os,
Http2TestServerConnection conn, boolean pushAllowed) {
super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession, os, conn, pushAllowed);
}
@Override
public void serverPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream content) {
HttpHeadersBuilder headersBuilder = new HttpHeadersBuilder();
headersBuilder.setHeader(":method", "GET");
headersBuilder.setHeader(":scheme", uri.getScheme());
headersBuilder.setHeader(":authority", uri.getAuthority());
headersBuilder.setHeader(":path", uri.getPath());
for (Map.Entry<String,List<String>> entry : reqHeaders.map().entrySet()) {
for (String value : entry.getValue())
headersBuilder.addHeader(entry.getKey(), value);
}
HttpHeaders combinedHeaders = headersBuilder.build();
OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, combinedHeaders, rspHeaders, content);
// Indicates to the client that a continuation should be expected
pp.setFlag(0x0);
try {
conn.addToOutputQ(pp);
// writeLoop will spin up thread to read the InputStream
} catch (IOException ex) {
System.err.println("TestServer: pushPromise exception: " + ex);
}
}
}
static class Http2PushPromiseContinuationExchangeImpl extends Http2TestExchangeImpl {
HttpHeadersBuilder pushPromiseHeadersBuilder;
List<ContinuationFrame> cfs;
Http2PushPromiseContinuationExchangeImpl(int streamid, String method, HttpHeaders reqheaders,
HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is,
SSLSession sslSession, BodyOutputStream os,
Http2TestServerConnection conn, boolean pushAllowed) {
super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession, os, conn, pushAllowed);
}
private void setPushHeaders(String name, String value) {
pushPromiseHeadersBuilder.setHeader(name, value);
testHeadersBuilder.setHeader(name, value);
}
private void assembleContinuations() {
for (int i = 0; i < continuationCount; i++) {
HttpHeadersBuilder builder = new HttpHeadersBuilder();
for (int j = 0; j < 10; j++) {
String name = "x-cont-" + i + "-" + j;
builder.setHeader(name, "data_" + j);
testHeadersBuilder.setHeader(name, "data_" + j);
}
ContinuationFrame cf = new ContinuationFrame(streamid, 0x0, conn.encodeHeaders(builder.build()));
// If this is the last Continuation Frame, set the END_HEADERS flag.
if (i >= continuationCount - 1) {
cf.setFlag(HeaderFrame.END_HEADERS);
}
cfs.add(cf);
}
}
@Override
public void serverPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream content) {
pushPromiseHeadersBuilder = new HttpHeadersBuilder();
testHeadersBuilder = new HttpHeadersBuilder();
cfs = new ArrayList<>();
setPushHeaders(":method", "GET");
setPushHeaders(":scheme", uri.getScheme());
setPushHeaders(":authority", uri.getAuthority());
setPushHeaders(":path", uri.getPath());
for (Map.Entry<String,List<String>> entry : reqHeaders.map().entrySet()) {
for (String value : entry.getValue()) {
setPushHeaders(entry.getKey(), value);
}
}
for (int i = 0; i < 10; i++) {
setPushHeaders("x-push-header-" + i, "data_" + i);
}
// Create the Continuation Frame/s, done before Push Promise Frame for test purposes
// as testHeaders contains all headers used in all frames
assembleContinuations();
HttpHeaders pushPromiseHeaders = pushPromiseHeadersBuilder.build();
testHeaders = testHeadersBuilder.build();
// Create the Push Promise Frame
OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, pushPromiseHeaders, rspHeaders, content, cfs);
// Indicates to the client that a continuation should be expected
pp.setFlag(0x0);
try {
// Schedule push promise and continuation for sending
conn.addToOutputQ(pp);
System.err.println("Server: Scheduled a Push Promise to Send");
} catch (IOException ex) {
System.err.println("Server: pushPromise exception: " + ex);
}
}
}
static class ServerPushHandler implements Http2Handler {
public void handle(Http2TestExchange exchange) throws IOException {
System.err.println("Server: handle " + exchange);
try (InputStream is = exchange.getRequestBody()) {
is.readAllBytes();
}
if (exchange.serverPushAllowed()) {
pushPromise(exchange);
}
// response data for the main response
try (OutputStream os = exchange.getResponseBody()) {
byte[] bytes = mainResponseBody.getBytes(UTF_8);
exchange.sendResponseHeaders(200, bytes.length);
os.write(bytes);
}
}
static final BiPredicate<String,String> ACCEPT_ALL = (x, y) -> true;
private void pushPromise(Http2TestExchange exchange) throws IOException {
URI requestURI = exchange.getRequestURI();
URI uri = requestURI.resolve("/promise");
InputStream is = new ByteArrayInputStream(mainPromiseBody.getBytes(UTF_8));
Map<String, List<String>> map = new HashMap<>();
map.put("x-promise", List.of("promise-header"));
HttpHeaders headers = HttpHeaders.of(map, ACCEPT_ALL);
exchange.serverPush(uri, headers, is);
System.err.println("Server: Push Promise complete");
}
}
}

View file

@ -0,0 +1,252 @@
/*
* Copyright (c) 2015, 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.
*/
/*
* @test
* @bug 8156514
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.httpclient.test.lib.http2.Http2TestExchange
* jdk.httpclient.test.lib.http2.Http2TestServer
* jdk.httpclient.test.lib.http2.Http2RedirectHandler
* jdk.test.lib.Asserts
* jdk.test.lib.net.SimpleSSLContext
* @run junit/othervm
* -Djdk.httpclient.HttpClient.log=frames,ssl,requests,responses,errors
* -Djdk.internal.httpclient.debug=true
* RedirectTest
*/
import java.net.InetSocketAddress;
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.concurrent.*;
import java.util.function.*;
import java.util.Arrays;
import java.util.Iterator;
import jdk.httpclient.test.lib.common.HttpServerAdapters;
import jdk.httpclient.test.lib.http2.Http2TestServer;
import jdk.httpclient.test.lib.http2.Http2TestExchange;
import jdk.httpclient.test.lib.http2.Http2RedirectHandler;
import static java.net.http.HttpClient.Version.HTTP_2;
import org.junit.jupiter.api.Test;
public class RedirectTest implements HttpServerAdapters {
static int httpPort;
static HttpTestServer httpServer;
static HttpClient client;
static String httpURIString, altURIString1, altURIString2;
static URI httpURI, altURI1, altURI2;
static Supplier<String> sup(String... args) {
Iterator<String> i = Arrays.asList(args).iterator();
// need to know when to stop calling it.
return () -> i.next();
}
static class Redirector extends Http2RedirectHandler {
private InetSocketAddress remoteAddr;
private boolean error = false;
Redirector(Supplier<String> supplier) {
super(supplier);
}
protected synchronized void examineExchange(Http2TestExchange ex) {
InetSocketAddress addr = ex.getRemoteAddress();
if (remoteAddr == null) {
remoteAddr = addr;
return;
}
// check that the client addr/port stays the same, proving
// that the connection didn't get dropped.
if (!remoteAddr.equals(addr)) {
System.err.printf("Error %s/%s\n", remoteAddr.toString(),
addr.toString());
error = true;
}
}
@Override
protected int redirectCode() {
return 308; // we need to use a code that preserves the body
}
public synchronized boolean error() {
return error;
}
}
static void initialize() throws Exception {
try {
client = getClient();
Http2TestServer http2ServerImpl =
new Http2TestServer(false, 0, null, null);
httpServer = HttpTestServer.of(http2ServerImpl);
httpPort = httpServer.getAddress().getPort();
// urls are accessed in sequence below. The first two are on
// different servers. Third on same server as second. So, the
// client should use the same http connection.
httpURIString = "http://" + httpServer.serverAuthority() + "/foo/";
httpURI = URI.create(httpURIString);
altURIString1 = "http://" + httpServer.serverAuthority() + "/redir";
altURI1 = URI.create(altURIString1);
altURIString2 = "http://" + httpServer.serverAuthority() + "/redir_again";
altURI2 = URI.create(altURIString2);
// TODO: remove dependency on Http2RedirectHandler
Redirector r = new Redirector(sup(altURIString1, altURIString2));
http2ServerImpl.addHandler(r, "/foo");
http2ServerImpl.addHandler(r, "/redir");
httpServer.addHandler(new HttpTestFileEchoHandler(), "/redir_again");
httpServer.start();
} catch (Throwable e) {
System.err.println("Throwing now");
e.printStackTrace();
throw e;
}
}
@Test
public void test() throws Exception {
try {
initialize();
simpleTest();
} finally {
httpServer.stop();
}
}
static HttpClient getClient() {
if (client == null) {
client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS)
.version(HTTP_2)
.build();
}
return client;
}
static URI getURI() {
return URI.create(httpURIString);
}
static void checkStatus(int expected, int found) throws Exception {
if (expected != found) {
System.err.printf ("Test failed: wrong status code %d/%d\n",
expected, found);
throw new RuntimeException("Test failed");
}
}
static void checkURIs(URI expected, URI found) throws Exception {
System.out.printf ("Expected: %s, Found: %s\n", expected.toString(), found.toString());
if (!expected.equals(found)) {
System.err.printf ("Test failed: wrong URI %s/%s\n",
expected.toString(), found.toString());
throw new RuntimeException("Test failed");
}
}
static void checkStrings(String expected, String found) throws Exception {
if (!expected.equals(found)) {
System.err.printf ("Test failed: wrong string %s/%s\n",
expected, found);
throw new RuntimeException("Test failed");
}
}
static void check(boolean cond, Object... msg) {
if (cond)
return;
StringBuilder sb = new StringBuilder();
for (Object o : msg)
sb.append(o);
throw new RuntimeException(sb.toString());
}
static final String SIMPLE_STRING = "Hello world Goodbye world";
static void simpleTest() throws Exception {
URI uri = getURI();
System.err.println("Request to " + uri);
HttpClient client = getClient();
HttpRequest req = HttpRequest.newBuilder(uri)
.POST(BodyPublishers.ofString(SIMPLE_STRING))
.build();
CompletableFuture<HttpResponse<String>> cf = client.sendAsync(req, BodyHandlers.ofString());
HttpResponse<String> response = cf.join();
checkStatus(200, response.statusCode());
String responseBody = response.body();
checkStrings(SIMPLE_STRING, responseBody);
checkURIs(response.uri(), altURI2);
// check two previous responses
HttpResponse<String> prev = response.previousResponse()
.orElseThrow(() -> new RuntimeException("no previous response"));
checkURIs(prev.uri(), altURI1);
prev = prev.previousResponse()
.orElseThrow(() -> new RuntimeException("no previous response"));
checkURIs(prev.uri(), httpURI);
checkPreviousRedirectResponses(req, response);
System.err.println("DONE");
}
static void checkPreviousRedirectResponses(HttpRequest initialRequest,
HttpResponse<?> finalResponse) {
// there must be at least one previous response
finalResponse.previousResponse()
.orElseThrow(() -> new RuntimeException("no previous response"));
HttpResponse<?> response = finalResponse;
do {
URI uri = response.uri();
response = response.previousResponse().get();
check(300 <= response.statusCode() && response.statusCode() <= 309,
"Expected 300 <= code <= 309, got:" + response.statusCode());
check(response.body() == null, "Unexpected body: " + response.body());
String locationHeader = response.headers().firstValue("Location")
.orElseThrow(() -> new RuntimeException("no previous Location"));
check(uri.toString().endsWith(locationHeader),
"URI: " + uri + ", Location: " + locationHeader);
} while (response.previousResponse().isPresent());
// initial
check(initialRequest.equals(response.request()),
"Expected initial request [%s] to equal last prev req [%s]",
initialRequest, response.request());
}
}

View file

@ -0,0 +1,312 @@
/*
* Copyright (c) 2015, 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.
*/
/*
* @test
* @bug 8087112 8159814
* @library /test/jdk/java/net/httpclient/lib
* /test/lib
* @build jdk.httpclient.test.lib.http2.Http2TestServer
* jdk.httpclient.test.lib.http2.PushHandler
* jdk.test.lib.Utils
* @run junit/othervm
* -Djdk.httpclient.HttpClient.log=errors,requests,responses
* ServerPush
*/
import java.io.*;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.file.*;
import java.net.http.*;
import java.net.http.HttpResponse.BodyHandler;
import java.net.http.HttpResponse.BodyHandlers;
import java.net.http.HttpResponse.BodySubscribers;
import java.net.http.HttpResponse.PushPromiseHandler;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Consumer;
import jdk.httpclient.test.lib.http2.Http2TestServer;
import jdk.httpclient.test.lib.http2.PushHandler;
import static java.nio.charset.StandardCharsets.UTF_8;
import static jdk.test.lib.Utils.createTempFileOfSize;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class ServerPush {
private static final String TEMP_FILE_PREFIX =
HttpClient.class.getPackageName() + '-' + ServerPush.class.getSimpleName() + '-';
static final int LOOPS = 13;
static final int FILE_SIZE = 512 * 1024 + 343;
static Path tempFile;
private static Http2TestServer server;
private static URI uri;
@BeforeAll
public static void setup() throws Exception {
tempFile = createTempFileOfSize(TEMP_FILE_PREFIX, null, FILE_SIZE);
server = new Http2TestServer(false, 0);
server.addHandler(new PushHandler(tempFile, LOOPS), "/");
System.out.println("Using temp file:" + tempFile);
System.err.println("Server listening on port " + server.getAddress().getPort());
server.start();
int port = server.getAddress().getPort();
uri = new URI("http://localhost:" + port + "/foo/a/b/c");
}
@AfterAll
public static void teardown() {
server.stop();
}
// Test 1 - custom written push promise handler, everything as a String
@Test
public void testTypeString() throws Exception {
String tempFileAsString = new String(Files.readAllBytes(tempFile), UTF_8);
ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<String>>>
resultMap = new ConcurrentHashMap<>();
PushPromiseHandler<String> pph = (initial, pushRequest, acceptor) -> {
BodyHandler<String> s = BodyHandlers.ofString(UTF_8);
CompletableFuture<HttpResponse<String>> cf = acceptor.apply(s);
resultMap.put(pushRequest, cf);
};
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(uri).GET().build();
CompletableFuture<HttpResponse<String>> cf =
client.sendAsync(request, BodyHandlers.ofString(UTF_8), pph);
cf.join();
resultMap.put(request, cf);
System.err.println("results.size: " + resultMap.size());
for (HttpRequest r : resultMap.keySet()) {
HttpResponse<String> response = resultMap.get(r).join();
assertEquals(200, response.statusCode());
assertEquals(tempFileAsString, response.body());
}
assertEquals(LOOPS + 1, resultMap.size());
}
// Test 2 - of(...) populating the given Map, everything as a String
@Test
public void testTypeStringOfMap() throws Exception {
String tempFileAsString = new String(Files.readAllBytes(tempFile), UTF_8);
ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<String>>>
resultMap = new ConcurrentHashMap<>();
PushPromiseHandler<String> pph =
PushPromiseHandler.of(pushPromise -> BodyHandlers.ofString(UTF_8),
resultMap);
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(uri).GET().build();
CompletableFuture<HttpResponse<String>> cf =
client.sendAsync(request, BodyHandlers.ofString(UTF_8), pph);
cf.join();
resultMap.put(request, cf);
System.err.println("results.size: " + resultMap.size());
for (HttpRequest r : resultMap.keySet()) {
HttpResponse<String> response = resultMap.get(r).join();
assertEquals(200, response.statusCode());
assertEquals(tempFileAsString, response.body());
}
assertEquals(LOOPS + 1, resultMap.size());
}
// --- Path ---
static final Path dir = Paths.get(".", "serverPush");
static BodyHandler<Path> requestToPath(HttpRequest req) {
URI u = req.uri();
Path path = Paths.get(dir.toString(), u.getPath());
try {
Files.createDirectories(path.getParent());
} catch (IOException ee) {
throw new UncheckedIOException(ee);
}
return BodyHandlers.ofFile(path);
}
// Test 3 - custom written push promise handler, everything as a Path
@Test
public void testTypePath() throws Exception {
String tempFileAsString = new String(Files.readAllBytes(tempFile), UTF_8);
ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<Path>>> resultsMap
= new ConcurrentHashMap<>();
PushPromiseHandler<Path> pushPromiseHandler = (initial, pushRequest, acceptor) -> {
BodyHandler<Path> pp = requestToPath(pushRequest);
CompletableFuture<HttpResponse<Path>> cf = acceptor.apply(pp);
resultsMap.put(pushRequest, cf);
};
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(uri).GET().build();
CompletableFuture<HttpResponse<Path>> cf =
client.sendAsync(request, requestToPath(request), pushPromiseHandler);
cf.join();
resultsMap.put(request, cf);
for (HttpRequest r : resultsMap.keySet()) {
HttpResponse<Path> response = resultsMap.get(r).join();
assertEquals(200, response.statusCode());
String fileAsString = new String(Files.readAllBytes(response.body()), UTF_8);
assertEquals(tempFileAsString, fileAsString);
}
assertEquals(LOOPS + 1, resultsMap.size());
}
// Test 4 - of(...) populating the given Map, everything as a Path
@Test
public void testTypePathOfMap() throws Exception {
String tempFileAsString = new String(Files.readAllBytes(tempFile), UTF_8);
ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<Path>>> resultsMap
= new ConcurrentHashMap<>();
PushPromiseHandler<Path> pushPromiseHandler =
PushPromiseHandler.of(pushRequest -> requestToPath(pushRequest),
resultsMap);
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(uri).GET().build();
CompletableFuture<HttpResponse<Path>> cf =
client.sendAsync(request, requestToPath(request), pushPromiseHandler);
cf.join();
resultsMap.put(request, cf);
for (HttpRequest r : resultsMap.keySet()) {
HttpResponse<Path> response = resultsMap.get(r).join();
assertEquals(200, response.statusCode());
String fileAsString = new String(Files.readAllBytes(response.body()), UTF_8);
assertEquals(tempFileAsString, fileAsString);
}
assertEquals(LOOPS + 1, resultsMap.size());
}
// --- Consumer<byte[]> ---
static class ByteArrayConsumer implements Consumer<Optional<byte[]>> {
volatile List<byte[]> listByteArrays = new ArrayList<>();
volatile byte[] accumulatedBytes;
public byte[] getAccumulatedBytes() { return accumulatedBytes; }
@Override
public void accept(Optional<byte[]> optionalBytes) {
assert accumulatedBytes == null;
if (!optionalBytes.isPresent()) {
int size = listByteArrays.stream().mapToInt(ba -> ba.length).sum();
ByteBuffer bb = ByteBuffer.allocate(size);
listByteArrays.stream().forEach(ba -> bb.put(ba));
accumulatedBytes = bb.array();
} else {
listByteArrays.add(optionalBytes.get());
}
}
}
// Test 5 - custom written handler, everything as a consumer of optional byte[]
@Test
public void testTypeByteArrayConsumer() throws Exception {
String tempFileAsString = new String(Files.readAllBytes(tempFile), UTF_8);
ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<Void>>> resultsMap
= new ConcurrentHashMap<>();
Map<HttpRequest,ByteArrayConsumer> byteArrayConsumerMap
= new ConcurrentHashMap<>();
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(uri).GET().build();
ByteArrayConsumer bac = new ByteArrayConsumer();
byteArrayConsumerMap.put(request, bac);
PushPromiseHandler<Void> pushPromiseHandler = (initial, pushRequest, acceptor) -> {
CompletableFuture<HttpResponse<Void>> cf = acceptor.apply(
(info) -> {
ByteArrayConsumer bc = new ByteArrayConsumer();
byteArrayConsumerMap.put(pushRequest, bc);
return BodySubscribers.ofByteArrayConsumer(bc); } );
resultsMap.put(pushRequest, cf);
};
CompletableFuture<HttpResponse<Void>> cf =
client.sendAsync(request, BodyHandlers.ofByteArrayConsumer(bac), pushPromiseHandler);
cf.join();
resultsMap.put(request, cf);
for (HttpRequest r : resultsMap.keySet()) {
HttpResponse<Void> response = resultsMap.get(r).join();
assertEquals(200, response.statusCode());
byte[] ba = byteArrayConsumerMap.get(r).getAccumulatedBytes();
String result = new String(ba, UTF_8);
assertEquals(tempFileAsString, result);
}
assertEquals(LOOPS + 1, resultsMap.size());
}
// Test 6 - of(...) populating the given Map, everything as a consumer of optional byte[]
@Test
public void testTypeByteArrayConsumerOfMap() throws Exception {
String tempFileAsString = new String(Files.readAllBytes(tempFile), UTF_8);
ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<Void>>> resultsMap
= new ConcurrentHashMap<>();
Map<HttpRequest,ByteArrayConsumer> byteArrayConsumerMap
= new ConcurrentHashMap<>();
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(uri).GET().build();
ByteArrayConsumer bac = new ByteArrayConsumer();
byteArrayConsumerMap.put(request, bac);
PushPromiseHandler<Void> pushPromiseHandler =
PushPromiseHandler.of(
pushRequest -> {
ByteArrayConsumer bc = new ByteArrayConsumer();
byteArrayConsumerMap.put(pushRequest, bc);
return BodyHandlers.ofByteArrayConsumer(bc);
},
resultsMap);
CompletableFuture<HttpResponse<Void>> cf =
client.sendAsync(request, BodyHandlers.ofByteArrayConsumer(bac), pushPromiseHandler);
cf.join();
resultsMap.put(request, cf);
for (HttpRequest r : resultsMap.keySet()) {
HttpResponse<Void> response = resultsMap.get(r).join();
assertEquals(200, response.statusCode());
byte[] ba = byteArrayConsumerMap.get(r).getAccumulatedBytes();
String result = new String(ba, UTF_8);
assertEquals(tempFileAsString, result);
}
assertEquals(LOOPS + 1, resultsMap.size());
}
}

View file

@ -0,0 +1,261 @@
/*
* Copyright (c) 2018, 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.
*/
/*
* @test
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer
* @run junit/othervm
* -Djdk.internal.httpclient.debug=true
* -Djdk.httpclient.HttpClient.log=errors,requests,responses
* ServerPushWithDiffTypes
*/
import java.io.*;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.file.*;
import java.net.http.*;
import java.net.http.HttpResponse.BodyHandler;
import java.net.http.HttpResponse.PushPromiseHandler;
import java.net.http.HttpResponse.BodySubscriber;
import java.net.http.HttpResponse.BodySubscribers;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.BiPredicate;
import jdk.httpclient.test.lib.http2.Http2TestServer;
import jdk.httpclient.test.lib.http2.Http2TestExchange;
import jdk.httpclient.test.lib.http2.Http2Handler;
import static java.nio.charset.StandardCharsets.UTF_8;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class ServerPushWithDiffTypes {
static Map<String,String> PUSH_PROMISES = Map.of(
"/x/y/z/1", "the first push promise body",
"/x/y/z/2", "the second push promise body",
"/x/y/z/3", "the third push promise body",
"/x/y/z/4", "the fourth push promise body",
"/x/y/z/5", "the fifth push promise body",
"/x/y/z/6", "the sixth push promise body",
"/x/y/z/7", "the seventh push promise body",
"/x/y/z/8", "the eighth push promise body",
"/x/y/z/9", "the ninth push promise body"
);
@Test
public void test() throws Exception {
Http2TestServer server = null;
try {
server = new Http2TestServer(false, 0);
Http2Handler handler =
new ServerPushHandler("the main response body",
PUSH_PROMISES);
server.addHandler(handler, "/");
server.start();
int port = server.getAddress().getPort();
System.err.println("Server listening on port " + port);
HttpClient client = HttpClient.newHttpClient();
// use multi-level path
URI uri = new URI("http://localhost:" + port + "/foo/a/b/c");
HttpRequest request = HttpRequest.newBuilder(uri).GET().build();
ConcurrentMap<HttpRequest,CompletableFuture<HttpResponse<BodyAndType<?>>>>
results = new ConcurrentHashMap<>();
PushPromiseHandler<BodyAndType<?>> bh = PushPromiseHandler.of(
(pushRequest) -> new BodyAndTypeHandler(pushRequest), results);
CompletableFuture<HttpResponse<BodyAndType<?>>> cf =
client.sendAsync(request, new BodyAndTypeHandler(request), bh);
results.put(request, cf);
cf.join();
assertEquals(PUSH_PROMISES.size() + 1, results.size());
for (HttpRequest r : results.keySet()) {
URI u = r.uri();
BodyAndType<?> body = results.get(r).get().body();
String result;
// convert all body types to String for easier comparison
if (body.type() == String.class) {
result = (String)body.getBody();
} else if (body.type() == byte[].class) {
byte[] bytes = (byte[])body.getBody();
result = new String(bytes, UTF_8);
} else if (Path.class.isAssignableFrom(body.type())) {
Path path = (Path)body.getBody();
result = new String(Files.readAllBytes(path), UTF_8);
} else {
throw new AssertionError("Unknown:" + body.type());
}
System.err.printf("%s -> %s\n", u.toString(), result.toString());
String expected = PUSH_PROMISES.get(r.uri().getPath());
if (expected == null)
expected = "the main response body";
assertEquals(expected, result);
}
} finally {
server.stop();
}
}
interface BodyAndType<T> {
Class<T> type();
T getBody();
}
static final Path WORK_DIR = Paths.get(".");
static class BodyAndTypeHandler implements BodyHandler<BodyAndType<?>> {
int count;
final HttpRequest request;
BodyAndTypeHandler(HttpRequest request) {
this.request = request;
}
@Override
public HttpResponse.BodySubscriber<BodyAndType<?>> apply(HttpResponse.ResponseInfo info) {
int whichType = count++ % 3; // real world may base this on the request metadata
switch (whichType) {
case 0: // String
return new BodyAndTypeSubscriber(BodySubscribers.ofString(UTF_8));
case 1: // byte[]
return new BodyAndTypeSubscriber(BodySubscribers.ofByteArray());
case 2: // Path
URI u = request.uri();
Path path = Paths.get(WORK_DIR.toString(), u.getPath());
try {
Files.createDirectories(path.getParent());
} catch (IOException ee) {
throw new UncheckedIOException(ee);
}
return new BodyAndTypeSubscriber(BodySubscribers.ofFile(path));
default:
throw new AssertionError("Unexpected " + whichType);
}
}
}
static class BodyAndTypeSubscriber<T>
implements HttpResponse.BodySubscriber<BodyAndType<T>>
{
private static class BodyAndTypeImpl<T> implements BodyAndType<T> {
private final Class<T> type;
private final T body;
public BodyAndTypeImpl(Class<T> type, T body) { this.type = type; this.body = body; }
@Override public Class<T> type() { return type; }
@Override public T getBody() { return body; }
}
private final BodySubscriber<?> bodySubscriber;
private final CompletableFuture<BodyAndType<T>> cf;
BodyAndTypeSubscriber(BodySubscriber bodySubscriber) {
this.bodySubscriber = bodySubscriber;
cf = new CompletableFuture<>();
bodySubscriber.getBody().whenComplete(
(r,t) -> cf.complete(new BodyAndTypeImpl(r.getClass(), r)));
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
bodySubscriber.onSubscribe(subscription);
}
@Override
public void onNext(List<ByteBuffer> item) {
bodySubscriber.onNext(item);
}
@Override
public void onError(Throwable throwable) {
bodySubscriber.onError(throwable);
cf.completeExceptionally(throwable);
}
@Override
public void onComplete() {
bodySubscriber.onComplete();
}
@Override
public CompletionStage<BodyAndType<T>> getBody() {
return cf;
}
}
// --- server push handler ---
static class ServerPushHandler implements Http2Handler {
private final String mainResponseBody;
private final Map<String,String> promises;
public ServerPushHandler(String mainResponseBody,
Map<String,String> promises)
throws Exception
{
Objects.requireNonNull(promises);
this.mainResponseBody = mainResponseBody;
this.promises = promises;
}
public void handle(Http2TestExchange exchange) throws IOException {
System.err.println("Server: handle " + exchange);
try (InputStream is = exchange.getRequestBody()) {
is.readAllBytes();
}
if (exchange.serverPushAllowed()) {
pushPromises(exchange);
}
// response data for the main response
try (OutputStream os = exchange.getResponseBody()) {
byte[] bytes = mainResponseBody.getBytes(UTF_8);
exchange.sendResponseHeaders(200, bytes.length);
os.write(bytes);
}
}
static final BiPredicate<String,String> ACCEPT_ALL = (x, y) -> true;
private void pushPromises(Http2TestExchange exchange) throws IOException {
URI requestURI = exchange.getRequestURI();
for (Map.Entry<String,String> promise : promises.entrySet()) {
URI uri = requestURI.resolve(promise.getKey());
InputStream is = new ByteArrayInputStream(promise.getValue().getBytes(UTF_8));
Map<String,List<String>> map = Map.of("X-Promise", List.of(promise.getKey()));
HttpHeaders headers = HttpHeaders.of(map, ACCEPT_ALL);
// TODO: add some check on headers, maybe
exchange.serverPush(uri, headers, is);
}
System.err.println("Server: All pushes sent");
}
}
}

View file

@ -0,0 +1,221 @@
/*
* Copyright (c) 2015, 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.
*/
/*
* @test
* @bug 8087112
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestUtil
* jdk.httpclient.test.lib.http2.Http2TestServer
* @run junit/othervm -XX:+CrashOnOutOfMemoryError SimpleGet
* @run junit/othervm -XX:+CrashOnOutOfMemoryError
* -Dsimpleget.repeat=1 -Dsimpleget.chunks=1 -Dsimpleget.requests=1000
* SimpleGet
* @run junit/othervm -Dsimpleget.requests=150
* -Dsimpleget.chunks=16384
* -Djdk.httpclient.redirects.retrylimit=5
* -Djdk.httpclient.HttpClient.log=errors
* -XX:+CrashOnOutOfMemoryError
* -XX:+HeapDumpOnOutOfMemoryError
* SimpleGet
*/
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Builder;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import javax.net.ssl.SSLContext;
import jdk.httpclient.test.lib.common.HttpServerAdapters;
import jdk.test.lib.net.SimpleSSLContext;
import static java.net.http.HttpClient.Version.HTTP_2;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class SimpleGet implements HttpServerAdapters {
static HttpTestServer httpsServer;
static HttpClient client = null;
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
static String httpsURIString;
static ExecutorService serverExec = Executors.newVirtualThreadPerTaskExecutor();
static void initialize() throws Exception {
try {
client = getClient();
httpsServer = HttpTestServer.create(HTTP_2, sslContext, serverExec);
httpsServer.addHandler(new TestHandler(), "/");
httpsURIString = "https://" + httpsServer.serverAuthority() + "/bar/";
httpsServer.start();
warmup();
} catch (Throwable e) {
System.err.println("Throwing now");
e.printStackTrace();
throw e;
}
}
private static void warmup() throws Exception {
// warmup server
try (var client2 = createClient(sslContext)) {
HttpRequest request = HttpRequest.newBuilder(URI.create(httpsURIString))
.version(HTTP_2)
.HEAD().build();
client2.send(request, BodyHandlers.discarding());
}
// warmup client
var httpsServer2 = HttpTestServer.create(HTTP_2, sslContext,
Executors.newVirtualThreadPerTaskExecutor());
httpsServer2.addHandler(new TestHandler(), "/");
var httpsURIString2 = "https://" + httpsServer2.serverAuthority() + "/bar/";
httpsServer2.start();
try {
HttpRequest request = HttpRequest.newBuilder(URI.create(httpsURIString2))
.version(HTTP_2)
.HEAD().build();
client.send(request, BodyHandlers.discarding());
} finally {
httpsServer2.stop();
}
}
public static void main(String[] args) throws Exception {
new SimpleGet().test();
}
@Test
public void test() throws Exception {
try {
long prestart = System.nanoTime();
initialize();
long done = System.nanoTime();
System.out.println("Stat: Initialization and warmup took " + TimeUnit.NANOSECONDS.toMillis(done - prestart) + " millis");
HttpRequest request = HttpRequest.newBuilder(URI.create(httpsURIString))
.version(HTTP_2)
.GET().build();
long start = System.nanoTime();
var resp = client.send(request, BodyHandlers.ofByteArrayConsumer(b -> {}));
assertEquals(200, resp.statusCode());
long elapsed = System.nanoTime() - start;
System.out.println("Stat: First request took: " + elapsed + " nanos (" + TimeUnit.NANOSECONDS.toMillis(elapsed) + " ms)");
final int max = property("simpleget.requests", 50);
;
List<CompletableFuture<HttpResponse<Void>>> list = new ArrayList<>(max);
Set<String> connections = new ConcurrentSkipListSet<>();
long start2 = System.nanoTime();
for (int i = 0; i < max; i++) {
var cf = client.sendAsync(request, BodyHandlers.ofByteArrayConsumer(b -> {}))
.whenComplete((r, t) -> Optional.ofNullable(r)
.flatMap(HttpResponse::connectionLabel)
.ifPresent(connections::add));
list.add(cf);
//cf.get(); // uncomment to test with serial instead of concurrent requests
}
try {
CompletableFuture.allOf(list.toArray(new CompletableFuture[0])).join();
} finally {
long elapsed2 = System.nanoTime() - start2;
long completed = list.stream().filter(CompletableFuture::isDone)
.filter(Predicate.not(CompletableFuture::isCompletedExceptionally)).count();
connections.forEach(System.out::println);
if (completed > 0) {
System.out.println("Stat: Next " + completed + " requests took: " + elapsed2 + " nanos ("
+ TimeUnit.NANOSECONDS.toMillis(elapsed2) + "ms for " + completed + " requests): "
+ elapsed2 / completed + " nanos per request ("
+ TimeUnit.NANOSECONDS.toMillis(elapsed2) / completed + " ms) on "
+ connections.size() + " connections");
}
}
list.forEach((cf) -> assertEquals(200, cf.join().statusCode()));
} catch (Throwable tt) {
System.err.println("tt caught");
tt.printStackTrace();
throw tt;
} finally {
httpsServer.stop();
}
}
static HttpClient createClient(SSLContext sslContext) {
return HttpClient.newBuilder()
.sslContext(sslContext)
.version(HTTP_2)
.proxy(Builder.NO_PROXY)
.executor(Executors.newVirtualThreadPerTaskExecutor())
.build();
}
static HttpClient getClient() {
if (client == null) {
client = createClient(sslContext);
}
return client;
}
static int property(String name, int defaultValue) {
return Integer.parseInt(System.getProperty(name, String.valueOf(defaultValue)));
}
// 32 * 32 * 1024 * 10 chars = 10Mb responses
// 50 requests => 500Mb
// 100 requests => 1Gb
// 1000 requests => 10Gb
private final static int REPEAT = property("simpleget.repeat", 32);
private final static String RESPONSE = "abcdefghij".repeat(property("simpleget.chunks", 1024*32));
private final static byte[] RESPONSE_BYTES = RESPONSE.getBytes(StandardCharsets.UTF_8);
private static class TestHandler implements HttpTestHandler {
@Override
public void handle(HttpTestExchange t) throws IOException {
try (var in = t.getRequestBody()) {
byte[] input = in.readAllBytes();
t.sendResponseHeaders(200, RESPONSE_BYTES.length * REPEAT);
try (var out = t.getResponseBody()) {
if (t.getRequestMethod().equals("HEAD")) return;
for (int i=0; i<REPEAT; i++) {
out.write(RESPONSE_BYTES);
}
}
}
}
}
}

View file

@ -0,0 +1,394 @@
/*
* Copyright (c) 2024, 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.
*/
/*
* @test
* @bug 8342075 8343855
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext
* @run junit/othervm -Djdk.internal.httpclient.debug=true
* -Djdk.httpclient.connectionWindowSize=65535
* -Djdk.httpclient.windowsize=16384
* StreamFlowControlTest
*/
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.ProtocolException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpHeadOrGetHandler;
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer;
import jdk.httpclient.test.lib.http2.BodyOutputStream;
import jdk.httpclient.test.lib.http2.Http2Handler;
import jdk.httpclient.test.lib.http2.Http2TestExchange;
import jdk.httpclient.test.lib.http2.Http2TestExchangeImpl;
import jdk.httpclient.test.lib.http2.Http2TestServer;
import jdk.httpclient.test.lib.http2.Http2TestServerConnection;
import jdk.internal.net.http.common.HttpHeadersBuilder;
import jdk.internal.net.http.frame.SettingsFrame;
import jdk.test.lib.Utils;
import jdk.test.lib.net.SimpleSSLContext;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class StreamFlowControlTest {
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
private static HttpTestServer http2TestServer; // HTTP/2 ( h2c )
private static HttpTestServer https2TestServer; // HTTP/2 ( h2 )
private static String http2URI;
private static String https2URI;
private static final AtomicInteger reqid = new AtomicInteger();
final static int WINDOW =
Integer.getInteger("jdk.httpclient.windowsize", 2 * 16 * 1024);
public static Object[][] variants() {
return new Object[][] {
{ http2URI, false },
{ https2URI, false },
{ http2URI, true },
{ https2URI, true },
};
}
static void sleep(long wait) throws InterruptedException {
if (wait <= 0) return;
long remaining = Utils.adjustTimeout(wait);
long start = System.nanoTime();
while (remaining > 0) {
Thread.sleep(remaining);
long end = System.nanoTime();
remaining = remaining - NANOSECONDS.toMillis(end - start);
}
System.out.printf("Waited %s ms%n",
NANOSECONDS.toMillis(System.nanoTime() - start));
}
@ParameterizedTest
@MethodSource("variants")
void test(String uri,
boolean sameClient)
throws Exception
{
System.out.printf("%ntesting test(%s, %s)%n", uri, sameClient);
ConcurrentHashMap<String, CompletableFuture<String>> responseSent = new ConcurrentHashMap<>();
FCHttp2TestExchange.setResponseSentCB((s) -> responseSent.get(s).complete(s));
HttpClient client = null;
try {
int max = sameClient ? 10 : 3;
String label = null;
for (int i = 0; i < max; i++) {
if (!sameClient || client == null)
client = HttpClient.newBuilder().sslContext(sslContext).build();
String query = "reqId=" + reqid.incrementAndGet();
URI uriWithQuery = URI.create(uri + "?" + query);
CompletableFuture<String> sent = new CompletableFuture<>();
responseSent.put(query, sent);
HttpRequest request = HttpRequest.newBuilder(uriWithQuery)
.GET()
.build();
System.out.println("\nSending request:" + uriWithQuery);
final HttpClient cc = client;
try {
HttpResponse<InputStream> response = cc.send(request, BodyHandlers.ofInputStream());
if (sameClient) {
String key = response.headers().firstValue("X-Connection-Key").get();
if (label == null) label = key;
assertEquals(label, key, "Unexpected key for " + query);
}
sent.join();
// we have to pull to get the exception, but slow enough
// so that DataFrames are buffered up to the point that
// the window is exceeded...
long wait = uri.startsWith("https://") ? 800 : 500;
try (InputStream is = response.body()) {
byte[] discard = new byte[WINDOW/4];
for (int j=0; j<2; j++) {
sleep(wait);
if (is.read(discard) < 0) break;
}
is.readAllBytes();
}
// we could fail here if we haven't waited long enough
fail("Expected exception, got :" + response + ", should sleep time be raised?");
} catch (IOException ioe) {
System.out.println("Got EXPECTED: " + ioe);
assertDetailMessage(ioe, i);
} finally {
if (!sameClient && client != null) {
client.close();
client = null;
}
}
}
} finally {
if (sameClient && client != null) client.close();
}
}
@ParameterizedTest
@MethodSource("variants")
void testAsync(String uri,
boolean sameClient)
{
System.out.printf("%ntesting testAsync(%s, %s)%n", uri, sameClient);
ConcurrentHashMap<String, CompletableFuture<String>> responseSent = new ConcurrentHashMap<>();
FCHttp2TestExchange.setResponseSentCB((s) -> responseSent.get(s).complete(s));
HttpClient client = null;
try {
int max = sameClient ? 5 : 3;
String label = null;
for (int i = 0; i < max; i++) {
if (!sameClient || client == null)
client = HttpClient.newBuilder().sslContext(sslContext).build();
String query = "reqId=" + reqid.incrementAndGet();
URI uriWithQuery = URI.create(uri + "?" + query);
CompletableFuture<String> sent = new CompletableFuture<>();
responseSent.put(query, sent);
HttpRequest request = HttpRequest.newBuilder(uriWithQuery)
.GET()
.build();
System.out.println("\nSending request:" + uriWithQuery);
final HttpClient cc = client;
Throwable t = null;
try {
HttpResponse<InputStream> response = cc.sendAsync(request, BodyHandlers.ofInputStream()).get();
if (sameClient) {
String key = response.headers().firstValue("X-Connection-Key").get();
if (label == null) label = key;
assertEquals(label, key, "Unexpected key for " + query);
}
sent.join();
long wait = uri.startsWith("https://") ? 800 : 350;
try (InputStream is = response.body()) {
byte[] discard = new byte[WINDOW/4];
for (int j=0; j<2; j++) {
sleep(wait);
if (is.read(discard) < 0) break;
}
is.readAllBytes();
}
// we could fail here if we haven't waited long enough
fail("Expected exception, got :" + response + ", should sleep time be raised?");
} catch (Throwable t0) {
System.out.println("Got EXPECTED: " + t0);
if (t0 instanceof ExecutionException) {
t0 = t0.getCause();
}
t = t0;
} finally {
if (!sameClient && client != null) {
client.close();
client = null;
}
}
assertDetailMessage(t, i);
}
} finally {
if (sameClient && client != null) client.close();
}
}
// Assertions based on implementation specific detail messages. Keep in
// sync with implementation.
static void assertDetailMessage(Throwable throwable, int iterationIndex) {
try {
Throwable cause = throwable;
while (cause != null) {
if (cause instanceof ProtocolException) {
if (cause.getMessage().matches("stream [0-9]+ flow control window exceeded")) {
System.out.println("Found expected exception: " + cause);
return;
}
}
cause = cause.getCause();
}
throw new AssertionError(
"ProtocolException(\"stream X flow control window exceeded\") not found",
throwable);
} catch (AssertionError e) {
System.out.println("Exception does not match expectation: " + throwable);
throwable.printStackTrace(System.out);
throw e;
}
}
@BeforeAll
public static void setup() throws Exception {
var http2TestServerImpl = new Http2TestServer("localhost", false, 0);
http2TestServerImpl.addHandler(new Http2TestHandler(), "/http2/");
http2TestServer = HttpTestServer.of(http2TestServerImpl);
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/x";
var https2TestServerImpl = new Http2TestServer("localhost", true, sslContext);
https2TestServerImpl.addHandler(new Http2TestHandler(), "/https2/");
https2TestServer = HttpTestServer.of(https2TestServerImpl);
https2TestServer.addHandler(new HttpHeadOrGetHandler(), "/https2/head/");
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/x";
String h2Head = "https://" + https2TestServer.serverAuthority() + "/https2/head/z";
// Override the default exchange supplier with a custom one to enable
// particular test scenarios
http2TestServerImpl.setExchangeSupplier(FCHttp2TestExchange::new);
https2TestServerImpl.setExchangeSupplier(FCHttp2TestExchange::new);
http2TestServer.start();
https2TestServer.start();
// warmup to eliminate delay due to SSL class loading and initialization.
try (var client = HttpClient.newBuilder().sslContext(sslContext).build()) {
var request = HttpRequest.newBuilder(URI.create(h2Head)).HEAD().build();
var resp = client.send(request, BodyHandlers.discarding());
assertEquals(200, resp.statusCode());
}
}
@AfterAll
public static void teardown() throws Exception {
http2TestServer.stop();
https2TestServer.stop();
}
static class Http2TestHandler implements Http2Handler {
@Override
public void handle(Http2TestExchange t) throws IOException {
String query = t.getRequestURI().getRawQuery();
try (InputStream is = t.getRequestBody();
OutputStream os = t.getResponseBody()) {
byte[] bytes = is.readAllBytes();
if (bytes.length != 0) {
System.out.println("Server " + t.getLocalAddress() + " received:\n"
+ t.getRequestURI() + ": " + new String(bytes, StandardCharsets.UTF_8));
} else {
System.out.println("No request body for " + t.getRequestMethod());
}
t.getResponseHeaders().setHeader("X-Connection-Key", t.getConnectionKey());
if (bytes.length == 0) {
bytes = "no request body!"
.repeat(100).getBytes(StandardCharsets.UTF_8);
}
final int maxChunkSize;
if (t instanceof FCHttp2TestExchange fct) {
maxChunkSize = Math.min(WINDOW, fct.conn.getMaxFrameSize());
} else {
maxChunkSize = Math.min(WINDOW, SettingsFrame.MAX_FRAME_SIZE);
}
byte[] resp = bytes.length <= maxChunkSize
? bytes
: Arrays.copyOfRange(bytes, 0, maxChunkSize);
int max = (WINDOW / resp.length) + 2;
// send in chunks
t.sendResponseHeaders(200, 0);
for (int i = 0; i <= max; i++) {
if (t instanceof FCHttp2TestExchange fct) {
try {
// we don't wait for the stream window, but we want
// to wait for the connection window
fct.conn.obtainConnectionWindow(resp.length);
} catch (InterruptedException ie) {
var ioe = new InterruptedIOException(ie.toString());
ioe.initCause(ie);
throw ioe;
}
}
try {
((BodyOutputStream) os).writeUncontrolled(resp, 0, resp.length);
} catch (IOException x) {
if (t instanceof FCHttp2TestExchange fct) {
fct.conn.updateConnectionWindow(resp.length);
}
throw x;
}
}
} finally {
if (t instanceof FCHttp2TestExchange fct) {
fct.responseSent(query);
} else {
fail("Exchange is not %s but %s"
.formatted(FCHttp2TestExchange.class.getName(), t.getClass().getName()));
}
}
}
}
// A custom Http2TestExchangeImpl that overrides sendResponseHeaders to
// allow headers to be sent with a number of CONTINUATION frames.
static class FCHttp2TestExchange extends Http2TestExchangeImpl {
static volatile Consumer<String> responseSentCB;
static void setResponseSentCB(Consumer<String> responseSentCB) {
FCHttp2TestExchange.responseSentCB = responseSentCB;
}
final Http2TestServerConnection conn;
FCHttp2TestExchange(int streamid, String method, HttpHeaders reqheaders,
HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is,
SSLSession sslSession, BodyOutputStream os,
Http2TestServerConnection conn, boolean pushAllowed) {
super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession, os, conn, pushAllowed);
this.conn = conn;
}
public void responseSent(String query) {
System.out.println("Server: response sent for " + query);
responseSentCB.accept(query);
}
}
}

View file

@ -0,0 +1,277 @@
/*
* Copyright (c) 2016, 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.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.Security;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse.BodyHandlers;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSession;
import jdk.httpclient.test.lib.http2.Http2TestServer;
import jdk.httpclient.test.lib.http2.Http2TestExchange;
import jdk.httpclient.test.lib.http2.Http2Handler;
/*
* @test
* @bug 8150769 8157107 8371887
* @library /test/jdk/java/net/httpclient/lib
* @build jdk.httpclient.test.lib.http2.Http2TestServer
* @summary Checks that SSL parameters can be set for HTTP/2 connection
* @run main/othervm
* -Djdk.internal.httpclient.debug=true
* -Djdk.httpclient.HttpClient.log=all
* TLSConnection
*/
public class TLSConnection {
private static final String KEYSTORE = System.getProperty("test.src")
+ File.separator + "keystore.p12";
private static final String PASSWORD = "password";
private static final SSLParameters USE_DEFAULT_SSL_PARAMETERS = new SSLParameters();
// expect highest supported version we know about
static String expectedTLSVersion(SSLContext ctx) throws Exception {
if (ctx == null)
ctx = SSLContext.getDefault();
SSLParameters params = ctx.getSupportedSSLParameters();
String[] protocols = params.getProtocols();
for (String prot : protocols) {
if (prot.equals("TLSv1.3"))
return "TLSv1.3";
}
return "TLSv1.2";
}
public static void main(String[] args) throws Exception {
// re-enable 3DES
Security.setProperty("jdk.tls.disabledAlgorithms", "");
// enable all logging
System.setProperty("jdk.httpclient.HttpClient.log", "all,frames:all");
// initialize JSSE
System.setProperty("javax.net.ssl.keyStore", KEYSTORE);
System.setProperty("javax.net.ssl.keyStorePassword", PASSWORD);
System.setProperty("javax.net.ssl.trustStore", KEYSTORE);
System.setProperty("javax.net.ssl.trustStorePassword", PASSWORD);
Handler handler = new Handler();
try (Http2TestServer server = new Http2TestServer("localhost", true, 0)) {
server.addHandler(handler, "/");
server.start();
int port = server.getAddress().getPort();
String uriString = "https://localhost:" + Integer.toString(port);
// run test cases
boolean success = true;
SSLParameters parameters = null;
success &= expectFailure(
"---\nTest #1: SSL parameters is null, expect NPE",
() -> connect(uriString, parameters),
NullPointerException.class);
success &= expectSuccess(
"---\nTest #2: default SSL parameters, "
+ "expect successful connection",
() -> connect(uriString, USE_DEFAULT_SSL_PARAMETERS));
success &= checkProtocol(handler.getSSLSession(), expectedTLSVersion(null));
// set SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA cipher suite
// which has less priority in default cipher suite list
success &= expectSuccess(
"---\nTest #3: SSL parameters with "
+ "SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA cipher suite, "
+ "expect successful connection",
() -> connect(uriString, new SSLParameters(
new String[] { "SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA" },
new String[] { "TLSv1.2" })));
success &= checkProtocol(handler.getSSLSession(), "TLSv1.2");
success &= checkCipherSuite(handler.getSSLSession(),
"SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA");
// set TLS_RSA_WITH_AES_128_CBC_SHA cipher suite
// which has less priority in default cipher suite list
// also set TLSv1.2 protocol
success &= expectSuccess(
"---\nTest #4: SSL parameters with "
+ "TLS_RSA_WITH_AES_128_CBC_SHA cipher suite,"
+ " expect successful connection",
() -> connect(uriString, new SSLParameters(
new String[] { "TLS_RSA_WITH_AES_128_CBC_SHA" },
new String[] { "TLSv1.2" })));
success &= checkProtocol(handler.getSSLSession(), "TLSv1.2");
success &= checkCipherSuite(handler.getSSLSession(),
"TLS_RSA_WITH_AES_128_CBC_SHA");
success &= expectSuccess(
"---\nTest #5: empty SSL parameters, "
+ "expect successful connection",
() -> connect(uriString, new SSLParameters()));
success &= checkProtocol(handler.getSSLSession(), expectedTLSVersion(null));
if (success) {
System.out.println("Test passed");
} else {
throw new RuntimeException("At least one test case failed");
}
}
}
private static interface Test {
public void run() throws Exception;
}
private static class Handler implements Http2Handler {
private static final byte[] BODY = "Test response".getBytes();
private volatile SSLSession sslSession;
@Override
public void handle(Http2TestExchange t) throws IOException {
System.out.println("Handler: received request to "
+ t.getRequestURI());
try (InputStream is = t.getRequestBody()) {
byte[] body = is.readAllBytes();
System.out.println("Handler: read " + body.length
+ " bytes of body: ");
System.out.println(new String(body));
}
sslSession = t.getSSLSession();
try (OutputStream os = t.getResponseBody()) {
t.sendResponseHeaders(200, BODY.length);
os.write(BODY);
}
}
SSLSession getSSLSession() {
return sslSession;
}
}
private static void connect(String uriString, SSLParameters sslParameters)
throws URISyntaxException, IOException, InterruptedException
{
HttpClient.Builder builder = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2);
if (sslParameters != USE_DEFAULT_SSL_PARAMETERS)
builder.sslParameters(sslParameters);
HttpClient client = builder.build();
HttpRequest request = HttpRequest.newBuilder(new URI(uriString))
.POST(BodyPublishers.ofString("body"))
.build();
String body = client.send(request, BodyHandlers.ofString()).body();
System.out.println("Response: " + body);
}
private static boolean checkProtocol(SSLSession session, String protocol) {
if (session == null) {
System.out.println("Check protocol: no session provided");
return false;
}
System.out.println("Check protocol: negotiated protocol: "
+ session.getProtocol());
System.out.println("Check protocol: expected protocol: "
+ protocol);
if (!protocol.equals(session.getProtocol())) {
System.out.println("Check protocol: unexpected negotiated protocol");
return false;
}
return true;
}
private static boolean checkCipherSuite(SSLSession session, String ciphersuite) {
if (session == null) {
System.out.println("Check protocol: no session provided");
return false;
}
System.out.println("Check protocol: negotiated ciphersuite: "
+ session.getCipherSuite());
System.out.println("Check protocol: expected ciphersuite: "
+ ciphersuite);
if (!ciphersuite.equals(session.getCipherSuite())) {
System.out.println("Check protocol: unexpected negotiated ciphersuite");
return false;
}
return true;
}
private static boolean expectSuccess(String message, Test test) {
System.out.println(message);
try {
test.run();
System.out.println("Passed");
return true;
} catch (Exception e) {
System.out.println("Failed: unexpected exception:");
e.printStackTrace(System.out);
return false;
}
}
private static boolean expectFailure(String message, Test test,
Class<? extends Throwable> expectedException) {
System.out.println(message);
try {
test.run();
System.out.println("Failed: unexpected successful connection");
return false;
} catch (Exception e) {
System.out.println("Got an exception:");
e.printStackTrace(System.out);
if (expectedException != null
&& !expectedException.isAssignableFrom(e.getClass())) {
System.out.printf("Failed: expected %s, but got %s%n",
expectedException.getName(),
e.getClass().getName());
return false;
}
System.out.println("Passed: expected exception");
return true;
}
}
}

View file

@ -0,0 +1,156 @@
/*
* Copyright (c) 2016, 2018, 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.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
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.net.http.HttpTimeoutException;
import java.time.Duration;
import java.util.concurrent.CompletionException;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocket;
/*
* @test
* @bug 8156710
* @summary Check if HttpTimeoutException is thrown if a server doesn't reply
* @run main/othervm Timeout
*/
public class Timeout {
private static final int RANDOM_PORT = 0;
private static final int TIMEOUT = 3 * 1000; // in millis
private static final String KEYSTORE = System.getProperty("test.src")
+ File.separator + "keystore.p12";
private static final String PASSWORD = "password";
// indicates if server is ready to accept connections
private static volatile boolean ready = false;
public static void main(String[] args) throws Exception {
test(false);
test(true);
}
public static void test(boolean async) throws Exception {
System.setProperty("javax.net.ssl.keyStore", KEYSTORE);
System.setProperty("javax.net.ssl.keyStorePassword", PASSWORD);
System.setProperty("javax.net.ssl.trustStore", KEYSTORE);
System.setProperty("javax.net.ssl.trustStorePassword", PASSWORD);
SSLServerSocketFactory factory =
(SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
try (SSLServerSocket ssocket =
(SSLServerSocket) factory.createServerSocket()) {
ssocket.setReuseAddress(false);
ssocket.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), RANDOM_PORT));
// start server
Thread server = new Thread(() -> {
while (true) {
System.out.println("server: ready");
SSLParameters params = ssocket.getSSLParameters();
params.setApplicationProtocols(new String[]{"h2"});
ssocket.setSSLParameters(params);
ready = true;
try (SSLSocket socket = (SSLSocket) ssocket.accept()) {
// just read forever
System.out.println("server: accepted");
while (true) {
socket.getInputStream().read();
}
} catch (IOException e) {
// ignore exceptions on server side
System.out.println("server: exception: " + e);
}
}
});
server.setDaemon(true);
server.start();
// wait for server is ready
do {
Thread.sleep(1000);
} while (!ready);
String uri = "https://localhost:" + ssocket.getLocalPort();
if (async) {
System.out.println(uri + ": Trying to connect asynchronously");
connectAsync(uri);
} else {
System.out.println(uri + ": Trying to connect synchronously");
connect(uri);
}
}
}
private static void connect(String server) throws Exception {
try {
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.build();
HttpRequest request = HttpRequest.newBuilder(new URI(server))
.timeout(Duration.ofMillis(TIMEOUT))
.POST(BodyPublishers.ofString("body"))
.build();
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
System.out.println("Received unexpected reply: " + response.statusCode());
throw new RuntimeException("unexpected successful connection");
} catch (HttpTimeoutException e) {
System.out.println("expected exception: " + e);
}
}
private static void connectAsync(String server) throws Exception {
try {
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.build();
HttpRequest request = HttpRequest.newBuilder(new URI(server))
.timeout(Duration.ofMillis(TIMEOUT))
.POST(BodyPublishers.ofString("body"))
.build();
HttpResponse<String> response = client.sendAsync(request, BodyHandlers.ofString()).join();
System.out.println("Received unexpected reply: " + response.statusCode());
throw new RuntimeException("unexpected successful connection");
} catch (CompletionException e) {
if (e.getCause() instanceof HttpTimeoutException) {
System.out.println("expected exception: " + e.getCause());
} else {
throw new RuntimeException("Unexpected exception received: " + e.getCause(), e);
}
}
}
}

View file

@ -0,0 +1,324 @@
/*
* 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.
*/
/*
@test
* @summary Trailing headers should be ignored by the client when using HTTP/2
* and not affect the rest of the exchange.
* @bug 8296410
* @library /test/jdk/java/net/httpclient/lib
* @build jdk.httpclient.test.lib.http2.Http2TestServer
* @run junit/othervm -Djdk.httpclient.HttpClient.log=all TrailingHeadersTest
*/
import jdk.httpclient.test.lib.http2.OutgoingPushPromise;
import jdk.internal.net.http.common.HttpHeadersBuilder;
import jdk.internal.net.http.frame.DataFrame;
import jdk.internal.net.http.frame.HeaderFrame;
import jdk.internal.net.http.frame.HeadersFrame;
import javax.net.ssl.SSLSession;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.BiPredicate;
import jdk.httpclient.test.lib.http2.Http2TestServer;
import jdk.httpclient.test.lib.http2.Http2TestServerConnection;
import jdk.httpclient.test.lib.http2.Http2TestExchangeImpl;
import jdk.httpclient.test.lib.http2.Http2TestExchange;
import jdk.httpclient.test.lib.http2.Http2Handler;
import jdk.httpclient.test.lib.http2.BodyOutputStream;
import static java.net.http.HttpClient.Version.HTTP_2;
import static java.nio.charset.StandardCharsets.UTF_8;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class TrailingHeadersTest {
private static Http2TestServer http2TestServer;
private static URI trailingURI, trailng1xxURI, trailingPushPromiseURI, warmupURI;
static PrintStream testLog = System.err;
// Set up simple client-side push promise handler
private static ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<String>>> pushPromiseMap = new ConcurrentHashMap<>();
@BeforeEach
public void beforeMethod() {
pushPromiseMap = new ConcurrentHashMap<>();
}
@BeforeAll
public static void setup() throws Exception {
Properties props = new Properties();
// For triggering trailing headers to send after Push Promise Response headers are sent
props.setProperty("sendTrailingHeadersAfterPushPromise", "1");
http2TestServer = new Http2TestServer("Test_Server",
false,
0,
null,
0,
props,
null);
http2TestServer.setExchangeSupplier(TrailingHeadersExchange::new);
http2TestServer.addHandler(new ResponseTrailersHandler(), "/ResponseTrailingHeaders");
http2TestServer.addHandler(new InformationalTrailersHandler(), "/InfoRespTrailingHeaders");
http2TestServer.addHandler(new PushPromiseTrailersHandler(), "/PushPromiseTrailingHeaders");
http2TestServer.addHandler(new WarmupHandler(), "/WarmupHandler");
http2TestServer.start();
trailingURI = URI.create("http://" + http2TestServer.serverAuthority() + "/ResponseTrailingHeaders");
trailng1xxURI = URI.create("http://" + http2TestServer.serverAuthority() + "/InfoRespTrailingHeaders");
trailingPushPromiseURI = URI.create("http://" + http2TestServer.serverAuthority() + "/PushPromiseTrailingHeaders");
// Used to ensure HTTP/2 upgrade takes place
warmupURI = URI.create("http://" + http2TestServer.serverAuthority() + "/WarmupHandler");
}
@AfterAll
public static void teardown() {
http2TestServer.stop();
}
@ParameterizedTest
@MethodSource("uris")
public void testTrailingHeaders(String description, HttpRequest hRequest, HttpResponse.PushPromiseHandler<String> pph) {
testLog.println("testTrailingHeaders(): " + description);
HttpClient httpClient = HttpClient.newBuilder().build();
performWarmupRequest(httpClient);
CompletableFuture<HttpResponse<String>> cf = httpClient.sendAsync(hRequest, BodyHandlers.ofString(UTF_8), pph);
testLog.println("testTrailingHeaders(): Performing request: " + hRequest);
HttpResponse<String> resp = cf.join();
assertEquals(200, resp.statusCode(), "Status code of response should be 200");
// Verify Push Promise was successful if necessary
if (pph != null)
verifyPushPromise();
testLog.println("testTrailingHeaders(): Request successfully completed");
}
private void verifyPushPromise() {
assertEquals(1, pushPromiseMap.size(), "Push Promise should not be greater than 1");
// This will only iterate once
for (HttpRequest r : pushPromiseMap.keySet()) {
CompletableFuture<HttpResponse<String>> serverPushResp = pushPromiseMap.get(r);
// Get the push promise HttpResponse result if present
HttpResponse<String> resp = serverPushResp.join();
assertEquals("Sample_Push_Data", resp.body(), "Unexpected Push Promise response body");
assertEquals(200, resp.statusCode(), "Status code of Push Promise response should be 200");
}
}
private void performWarmupRequest(HttpClient httpClient) {
HttpRequest warmupReq = HttpRequest.newBuilder(warmupURI).version(HTTP_2)
.GET()
.build();
httpClient.sendAsync(warmupReq, BodyHandlers.discarding()).join();
}
public static Object[][] uris() {
HttpResponse.PushPromiseHandler<String> pph = (initial, pushRequest, acceptor) -> {
HttpResponse.BodyHandler<String> s = HttpResponse.BodyHandlers.ofString(UTF_8);
TrailingHeadersTest.pushPromiseMap.put(pushRequest, acceptor.apply(s));
};
HttpRequest httpGetTrailing = HttpRequest.newBuilder(trailingURI).version(HTTP_2)
.GET()
.build();
HttpRequest httpPost1xxTrailing = HttpRequest.newBuilder(trailng1xxURI).version(HTTP_2)
.POST(HttpRequest.BodyPublishers.ofString("Test Post"))
.expectContinue(true)
.build();
HttpRequest httpGetPushPromiseTrailing = HttpRequest.newBuilder(trailingPushPromiseURI).version(HTTP_2)
.GET()
.build();
return new Object[][] {
{ "Test GET with Trailing Headers", httpGetTrailing, null },
{ "Test POST with 1xx response & Trailing Headers", httpPost1xxTrailing, null },
{ "Test Push Promise with Trailing Headers", httpGetPushPromiseTrailing, pph }
};
}
static class TrailingHeadersExchange extends Http2TestExchangeImpl {
byte[] resp = "Sample_Data".getBytes(StandardCharsets.UTF_8);
TrailingHeadersExchange(int streamid, String method, HttpHeaders reqheaders, HttpHeadersBuilder rspheadersBuilder,
URI uri, InputStream is, SSLSession sslSession, BodyOutputStream os,
Http2TestServerConnection conn, boolean pushAllowed) {
super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession, os, conn, pushAllowed);
}
public void sendResponseThenTrailers() throws IOException {
/*
HttpHeadersBuilder hb = this.conn.createNewHeadersBuilder();
hb.setHeader("x-sample", "val");
HeaderFrame headerFrame = new HeadersFrame(this.streamid, 0, this.conn.encodeHeaders(hb.build()));
*/
// TODO: see if there is a safe way to encode headers without interrupting connection thread
HeaderFrame headerFrame = new HeadersFrame(this.streamid, 0, List.of());
headerFrame.setFlag(HeaderFrame.END_HEADERS);
headerFrame.setFlag(HeaderFrame.END_STREAM);
this.sendResponseHeaders(200, resp.length);
DataFrame dataFrame = new DataFrame(this.streamid, 0, ByteBuffer.wrap(resp));
this.conn.addToOutputQ(dataFrame);
this.conn.addToOutputQ(headerFrame);
}
@Override
public void serverPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream content) {
HttpHeadersBuilder headersBuilder = new HttpHeadersBuilder();
headersBuilder.setHeader(":method", "GET");
headersBuilder.setHeader(":scheme", uri.getScheme());
headersBuilder.setHeader(":authority", uri.getAuthority());
headersBuilder.setHeader(":path", uri.getPath());
for (Map.Entry<String,List<String>> entry : reqHeaders.map().entrySet()) {
for (String value : entry.getValue())
headersBuilder.addHeader(entry.getKey(), value);
}
HttpHeaders combinedHeaders = headersBuilder.build();
OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, combinedHeaders, rspHeaders, content);
pp.setFlag(HeaderFrame.END_HEADERS);
try {
this.conn.addToOutputQ(pp);
} catch (IOException ex) {
testLog.println("serverPush(): pushPromise exception: " + ex);
}
}
}
static class WarmupHandler implements Http2Handler {
@Override
public void handle(Http2TestExchange exchange) throws IOException {
exchange.sendResponseHeaders(200, 0);
}
}
static class ResponseTrailersHandler implements Http2Handler {
@Override
public void handle(Http2TestExchange exchange) throws IOException {
if (exchange.getProtocol().equals("HTTP/2")) {
if (exchange instanceof TrailingHeadersExchange trailingHeadersExchange) {
trailingHeadersExchange.sendResponseThenTrailers();
}
} else {
testLog.println("ResponseTrailersHandler: Incorrect protocol version");
exchange.sendResponseHeaders(400, 0);
}
}
}
static class InformationalTrailersHandler implements Http2Handler {
@Override
public void handle(Http2TestExchange exchange) throws IOException {
if (exchange.getProtocol().equals("HTTP/2")) {
if (exchange instanceof TrailingHeadersExchange trailingHeadersExchange) {
testLog.println(this.getClass().getCanonicalName() + ": Sending status 100");
trailingHeadersExchange.sendResponseHeaders(100, 0);
try (InputStream is = exchange.getRequestBody()) {
is.readAllBytes();
trailingHeadersExchange.sendResponseThenTrailers();
}
}
} else {
testLog.println("InformationalTrailersHandler: Incorrect protocol version");
exchange.sendResponseHeaders(400, 0);
}
}
}
static class PushPromiseTrailersHandler implements Http2Handler {
@Override
public void handle(Http2TestExchange exchange) throws IOException {
if (exchange.getProtocol().equals("HTTP/2")) {
if (exchange instanceof TrailingHeadersExchange trailingHeadersExchange) {
try (InputStream is = exchange.getRequestBody()) {
is.readAllBytes();
}
if (exchange.serverPushAllowed()) {
pushPromise(trailingHeadersExchange);
}
try (OutputStream os = trailingHeadersExchange.getResponseBody()) {
byte[] bytes = "Sample_Data".getBytes(UTF_8);
trailingHeadersExchange.sendResponseHeaders(200, bytes.length);
os.write(bytes);
}
}
}
}
static final BiPredicate<String,String> ACCEPT_ALL = (x, y) -> true;
private void pushPromise(Http2TestExchange exchange) throws IOException {
URI requestURI = exchange.getRequestURI();
URI uri = requestURI.resolve("/promise");
InputStream is = new ByteArrayInputStream("Sample_Push_Data".getBytes(UTF_8));
Map<String, List<String>> map = new HashMap<>();
map.put("x-promise", List.of("promise-header"));
HttpHeaders headers = HttpHeaders.of(map, ACCEPT_ALL);
exchange.serverPush(uri, headers, is);
testLog.println("PushPromiseTrailersHandler: Push Promise complete");
}
}
}

View file

@ -0,0 +1,126 @@
/*
* Copyright (c) 2022, 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 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 org.junit.jupiter.api.TestInstance;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Version;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import jdk.httpclient.test.lib.http2.Http2TestServer;
import jdk.httpclient.test.lib.http2.Http2TestExchange;
import jdk.httpclient.test.lib.http2.Http2Handler;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* @test
* @bug 8292876
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.httpclient.test.lib.http2.Http2TestServer
* jdk.test.lib.net.SimpleSSLContext
* jdk.httpclient.test.lib.http2.Http2TestExchange
* @compile ../ReferenceTracker.java
* @run junit UserInfoTest
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class UserInfoTest {
static final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE;
Http2TestServer server;
int port;
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
@BeforeAll
void before() throws Exception {
server = createServer(sslContext);
port = server.getAddress().getPort();
server.start();
}
@AfterAll
void after() throws Exception {
server.close();
}
static class Http2TestHandler implements Http2Handler {
@Override
public void handle(Http2TestExchange e) throws IOException {
String authorityHeader = e.getRequestHeaders().firstValue(":authority").orElse(null);
if (authorityHeader == null || authorityHeader.contains("user@")) {
e.sendResponseHeaders(500, -1);
} else {
e.sendResponseHeaders(200, -1);
}
}
}
private static Http2TestServer createServer(SSLContext sslContext) throws Exception {
Http2TestServer http2TestServer = new Http2TestServer("localhost", true, sslContext);
Http2TestHandler handler = new Http2TestHandler();
http2TestServer.addHandler(handler, "/");
return http2TestServer;
}
@Test
public void testAuthorityHeader() throws Exception {
HttpClient client = HttpClient
.newBuilder()
.proxy(HttpClient.Builder.NO_PROXY)
.sslContext(sslContext)
.build();
TRACKER.track(client);
URI uri = URIBuilder.newBuilder()
.scheme("https")
.userInfo("user")
.loopback()
.port(port)
.build();
HttpRequest request = HttpRequest
.newBuilder(uri)
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
assertEquals(200, response.statusCode(), "Test Failed : " + response.uri().getAuthority());
client = null;
System.gc();
var error = TRACKER.check(500);
if (error != null) throw error;
}
}

View file

@ -0,0 +1,369 @@
/*
* Copyright (c) 2014, 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.
*/
package jdk.internal.net.http.hpack;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import static jdk.internal.net.http.hpack.BuffersTestingKit.*;
import static jdk.internal.net.http.hpack.TestHelper.newRandom;
import org.junit.jupiter.api.Test;
//
// Some of the tests below overlap in what they test. This allows to diagnose
// bugs quicker and with less pain by simply ruling out common working bits.
//
public final class BinaryPrimitivesTest {
private final Random random = newRandom();
@Test
public void integerRead1() {
verifyRead(bytes(0b00011111, 0b10011010, 0b00001010), 1337, 5);
}
@Test
public void integerRead2() {
verifyRead(bytes(0b00001010), 10, 5);
}
@Test
public void integerRead3() {
verifyRead(bytes(0b00101010), 42, 8);
}
@Test
public void integerWrite1() {
verifyWrite(bytes(0b00011111, 0b10011010, 0b00001010), 1337, 5);
}
@Test
public void integerWrite2() {
verifyWrite(bytes(0b00001010), 10, 5);
}
@Test
public void integerWrite3() {
verifyWrite(bytes(0b00101010), 42, 8);
}
//
// Since readInteger(x) is the inverse of writeInteger(x), thus:
//
// for all x: readInteger(writeInteger(x)) == x
//
@Test
public void integerIdentity() throws IOException {
final int MAX_VALUE = 1 << 22;
int totalCases = 0;
int maxFilling = 0;
IntegerReader r = new IntegerReader();
IntegerWriter w = new IntegerWriter();
ByteBuffer buf = ByteBuffer.allocate(8);
for (int N = 1; N < 9; N++) {
for (int expected = 0; expected <= MAX_VALUE; expected++) {
w.reset().configure(expected, N, 1).write(buf);
buf.flip();
totalCases++;
maxFilling = Math.max(maxFilling, buf.remaining());
r.reset().configure(N).read(buf);
assertEquals(expected, r.get());
buf.clear();
}
}
// System.out.printf("totalCases: %,d, maxFilling: %,d, maxValue: %,d%n",
// totalCases, maxFilling, MAX_VALUE);
}
@Test
public void integerReadChunked() {
final int NUM_TESTS = 1024;
IntegerReader r = new IntegerReader();
ByteBuffer bb = ByteBuffer.allocate(8);
IntegerWriter w = new IntegerWriter();
for (int i = 0; i < NUM_TESTS; i++) {
final int N = 1 + random.nextInt(8);
final int expected = random.nextInt(Integer.MAX_VALUE) + 1;
w.reset().configure(expected, N, random.nextInt()).write(bb);
bb.flip();
forEachSplit(bb,
(buffers) -> {
Iterable<? extends ByteBuffer> buf = relocateBuffers(injectEmptyBuffers(buffers));
r.configure(N);
for (ByteBuffer b : buf) {
try {
r.read(b);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
assertEquals(expected, r.get());
r.reset();
});
bb.clear();
}
}
// FIXME: use maxValue in the test
@Test
// FIXME: tune values for better coverage
public void integerWriteChunked() {
ByteBuffer bb = ByteBuffer.allocate(6);
IntegerWriter w = new IntegerWriter();
IntegerReader r = new IntegerReader();
for (int i = 0; i < 1024; i++) { // number of tests
final int N = 1 + random.nextInt(8);
final int payload = random.nextInt(255);
final int expected = random.nextInt(Integer.MAX_VALUE) + 1;
forEachSplit(bb,
(buffers) -> {
List<ByteBuffer> buf = new ArrayList<>();
relocateBuffers(injectEmptyBuffers(buffers)).forEach(buf::add);
boolean written = false;
w.configure(expected, N, payload); // TODO: test for payload it can be read after written
for (ByteBuffer b : buf) {
int pos = b.position();
written = w.write(b);
b.position(pos);
}
if (!written) {
fail("please increase bb size");
}
try {
r.configure(N).read(concat(buf));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
// TODO: check payload here
assertEquals(expected, r.get());
w.reset();
r.reset();
bb.clear();
});
}
}
//
// Since readString(x) is the inverse of writeString(x), thus:
//
// for all x: readString(writeString(x)) == x
//
@Test
public void stringIdentity() throws IOException {
final int MAX_STRING_LENGTH = 4096;
ByteBuffer bytes = ByteBuffer.allocate(MAX_STRING_LENGTH + 6); // it takes 6 bytes to encode string length of Integer.MAX_VALUE
CharBuffer chars = CharBuffer.allocate(MAX_STRING_LENGTH);
StringReader reader = new StringReader();
StringWriter writer = new StringWriter();
for (int len = 0; len <= MAX_STRING_LENGTH; len++) {
for (int i = 0; i < 64; i++) {
// not so much "test in isolation", I know... we're testing .reset() as well
bytes.clear();
chars.clear();
byte[] b = new byte[len];
random.nextBytes(b);
String expected = new String(b, StandardCharsets.ISO_8859_1); // reference string
boolean written = writer
.configure(CharBuffer.wrap(expected), 0, expected.length(), false)
.write(bytes);
if (!written) {
fail("please increase 'bytes' size");
}
bytes.flip();
reader.read(bytes, chars);
chars.flip();
assertEquals(expected, chars.toString());
reader.reset();
writer.reset();
}
}
}
// @Test
// public void huffmanStringWriteChunked() {
// fail();
// }
//
// @Test
// public void huffmanStringReadChunked() {
// fail();
// }
@Test
public void stringWriteChunked() {
final int MAX_STRING_LENGTH = 8;
final ByteBuffer bytes = ByteBuffer.allocate(MAX_STRING_LENGTH + 6);
final CharBuffer chars = CharBuffer.allocate(MAX_STRING_LENGTH);
final StringReader reader = new StringReader();
final StringWriter writer = new StringWriter();
for (int len = 0; len <= MAX_STRING_LENGTH; len++) {
byte[] b = new byte[len];
random.nextBytes(b);
String expected = new String(b, StandardCharsets.ISO_8859_1); // reference string
forEachSplit(bytes, (buffers) -> {
writer.configure(expected, 0, expected.length(), false);
boolean written = false;
for (ByteBuffer buf : buffers) {
int p0 = buf.position();
written = writer.write(buf);
buf.position(p0);
}
if (!written) {
fail("please increase 'bytes' size");
}
try {
reader.read(concat(buffers), chars);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
chars.flip();
assertEquals(expected, chars.toString());
reader.reset();
writer.reset();
chars.clear();
bytes.clear();
});
}
}
@Test
public void stringReadChunked() {
final int MAX_STRING_LENGTH = 16;
final ByteBuffer bytes = ByteBuffer.allocate(MAX_STRING_LENGTH + 6);
final CharBuffer chars = CharBuffer.allocate(MAX_STRING_LENGTH);
final StringReader reader = new StringReader();
final StringWriter writer = new StringWriter();
for (int len = 0; len <= MAX_STRING_LENGTH; len++) {
byte[] b = new byte[len];
random.nextBytes(b);
String expected = new String(b, StandardCharsets.ISO_8859_1); // reference string
boolean written = writer
.configure(CharBuffer.wrap(expected), 0, expected.length(), false)
.write(bytes);
writer.reset();
if (!written) {
fail("please increase 'bytes' size");
}
bytes.flip();
forEachSplit(bytes, (buffers) -> {
for (ByteBuffer buf : buffers) {
int p0 = buf.position();
try {
reader.read(buf, chars);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
buf.position(p0);
}
chars.flip();
assertEquals(expected, chars.toString());
reader.reset();
chars.clear();
});
bytes.clear();
}
}
// @Test
// public void test_Huffman_String_Identity() {
// StringWriter writer = new StringWriter();
// StringReader reader = new StringReader();
// // 256 * 8 gives 2048 bits in case of plain 8 bit coding
// // 256 * 30 gives you 7680 bits or 960 bytes in case of almost
// // improbable event of 256 30 bits symbols in a row
// ByteBuffer binary = ByteBuffer.allocate(960);
// CharBuffer text = CharBuffer.allocate(960 / 5); // 5 = minimum code length
// for (int len = 0; len < 128; len++) {
// for (int i = 0; i < 256; i++) {
// // not so much "test in isolation", I know...
// binary.clear();
//
// byte[] bytes = new byte[len];
// random.nextBytes(bytes);
//
// String s = new String(bytes, StandardCharsets.ISO_8859_1);
//
// writer.write(CharBuffer.wrap(s), binary, true);
// binary.flip();
// reader.read(binary, text);
// text.flip();
// assertEquals(text.toString(), s);
// }
// }
// }
// TODO: atomic failures: e.g. readonly/overflow
private static byte[] bytes(int... data) {
byte[] bytes = new byte[data.length];
for (int i = 0; i < data.length; i++) {
bytes[i] = (byte) data[i];
}
return bytes;
}
private static void verifyRead(byte[] data, int expected, int N) {
ByteBuffer buf = ByteBuffer.wrap(data, 0, data.length);
IntegerReader reader = new IntegerReader();
try {
reader.configure(N).read(buf);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
assertEquals(expected, reader.get());
}
private void verifyWrite(byte[] expected, int data, int N) {
IntegerWriter w = new IntegerWriter();
ByteBuffer buf = ByteBuffer.allocate(2 * expected.length);
w.configure(data, N, 1).write(buf);
buf.flip();
assertEquals(ByteBuffer.wrap(expected), buf);
}
}

View file

@ -0,0 +1,210 @@
/*
* Copyright (c) 2015, 2018, 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.
*/
package jdk.internal.net.http.hpack;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import static java.nio.ByteBuffer.allocate;
public final class BuffersTestingKit {
/**
* Relocates a {@code [position, limit)} region of the given buffer to
* corresponding region in a new buffer starting with provided {@code
* newPosition}.
*
* <p> Might be useful to make sure ByteBuffer's users do not rely on any
* absolute positions, but solely on what's reported by position(), limit().
*
* <p> The contents between the given buffer and the returned one are not
* shared.
*/
public static ByteBuffer relocate(ByteBuffer buffer, int newPosition,
int newCapacity) {
int oldPosition = buffer.position();
int oldLimit = buffer.limit();
if (newPosition + oldLimit - oldPosition > newCapacity) {
throw new IllegalArgumentException();
}
ByteBuffer result;
if (buffer.isDirect()) {
result = ByteBuffer.allocateDirect(newCapacity);
} else {
result = allocate(newCapacity);
}
result.position(newPosition);
result.put(buffer).limit(result.position()).position(newPosition);
buffer.position(oldPosition);
if (buffer.isReadOnly()) {
return result.asReadOnlyBuffer();
}
return result;
}
public static Iterable<? extends ByteBuffer> relocateBuffers(
Iterable<? extends ByteBuffer> source) {
return () ->
new Iterator<ByteBuffer>() {
private final Iterator<? extends ByteBuffer> it = source.iterator();
@Override
public boolean hasNext() {
return it.hasNext();
}
@Override
public ByteBuffer next() {
ByteBuffer buf = it.next();
int remaining = buf.remaining();
int newCapacity = remaining + random.nextInt(17);
int newPosition = random.nextInt(newCapacity - remaining + 1);
return relocate(buf, newPosition, newCapacity);
}
};
}
// TODO: not always of size 0 (it's fine for buffer to report !b.hasRemaining())
public static Iterable<? extends ByteBuffer> injectEmptyBuffers(
Iterable<? extends ByteBuffer> source) {
return injectEmptyBuffers(source, () -> allocate(0));
}
public static Iterable<? extends ByteBuffer> injectEmptyBuffers(
Iterable<? extends ByteBuffer> source,
Supplier<? extends ByteBuffer> emptyBufferFactory) {
return () ->
new Iterator<ByteBuffer>() {
private final Iterator<? extends ByteBuffer> it = source.iterator();
private ByteBuffer next = calculateNext();
private ByteBuffer calculateNext() {
if (random.nextBoolean()) {
return emptyBufferFactory.get();
} else if (it.hasNext()) {
return it.next();
} else {
return null;
}
}
@Override
public boolean hasNext() {
return next != null;
}
@Override
public ByteBuffer next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
ByteBuffer next = this.next;
this.next = calculateNext();
return next;
}
};
}
public static ByteBuffer concat(Iterable<? extends ByteBuffer> split) {
return concat(split, ByteBuffer::allocate);
}
public static ByteBuffer concat(Iterable<? extends ByteBuffer> split,
Function<? super Integer, ? extends ByteBuffer> concatBufferFactory) {
int size = 0;
for (ByteBuffer bb : split) {
size += bb.remaining();
}
ByteBuffer result = concatBufferFactory.apply(size);
for (ByteBuffer bb : split) {
result.put(bb);
}
result.flip();
return result;
}
public static void forEachSplit(ByteBuffer bb,
Consumer<? super Iterable<? extends ByteBuffer>> action) {
forEachSplit(bb.remaining(),
(lengths) -> {
int end = bb.position();
List<ByteBuffer> buffers = new LinkedList<>();
for (int len : lengths) {
ByteBuffer d = bb.duplicate();
d.position(end);
d.limit(end + len);
end += len;
buffers.add(d);
}
action.accept(buffers);
});
}
private static void forEachSplit(int n, Consumer<? super Iterable<? extends Integer>> action) {
forEachSplit(n, new Stack<>(), action);
}
private static void forEachSplit(int n, Stack<Integer> path,
Consumer<? super Iterable<? extends Integer>> action) {
if (n == 0) {
action.accept(path);
} else {
for (int i = 1; i <= n; i++) {
path.push(i);
forEachSplit(n - i, path, action);
path.pop();
}
}
}
private static final Random random = new Random();
private BuffersTestingKit() {
throw new InternalError();
}
// public static void main(String[] args) {
//
// List<ByteBuffer> buffers = Arrays.asList(
// (ByteBuffer) allocate(3).position(1).limit(2),
// allocate(0),
// allocate(7));
//
// Iterable<? extends ByteBuffer> buf = relocateBuffers(injectEmptyBuffers(buffers));
// List<ByteBuffer> result = new ArrayList<>();
// buf.forEach(result::add);
// System.out.println(result);
// }
}

View file

@ -0,0 +1,150 @@
/*
* Copyright (c) 2016, 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.
*/
package jdk.internal.net.http.hpack;
import jdk.internal.net.http.hpack.SimpleHeaderTable.CircularBuffer;
import java.util.Arrays;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import static jdk.internal.net.http.common.Utils.pow2Size;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static jdk.internal.net.http.hpack.TestHelper.assertVoidThrows;
import static jdk.internal.net.http.hpack.TestHelper.newRandom;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
public final class CircularBufferTest {
private final Random random = newRandom();
@Test
public void queue() {
for (int capacity = 1; capacity <= 2048; capacity++) {
queueOnce(capacity, 32);
}
}
@Test
public void resize() {
for (int capacity = 1; capacity <= 4096; capacity++) {
resizeOnce(capacity);
}
}
@Test
public void downSizeEmptyBuffer() {
CircularBuffer<Integer> buffer = new CircularBuffer<>(16);
buffer.resize(15);
}
@Test
public void newCapacityLessThanCurrentSize1() {
CircularBuffer<Integer> buffer = new CircularBuffer<>(0);
buffer.resize(5);
buffer.add(1);
buffer.add(1);
buffer.add(1);
assertVoidThrows(IllegalStateException.class, () -> buffer.resize(2));
assertVoidThrows(IllegalStateException.class, () -> buffer.resize(1));
}
@Test
public void newCapacityLessThanCurrentSize2() {
CircularBuffer<Integer> buffer = new CircularBuffer<>(5);
buffer.add(1);
buffer.add(1);
buffer.add(1);
assertVoidThrows(IllegalStateException.class, () -> buffer.resize(2));
assertVoidThrows(IllegalStateException.class, () -> buffer.resize(1));
}
private void resizeOnce(int capacity) {
capacity = pow2Size(capacity);
int nextNumberToPut = 0;
Queue<Integer> referenceQueue = new ArrayBlockingQueue<>(capacity);
CircularBuffer<Integer> buffer = new CircularBuffer<>(capacity);
// Fill full, so the next add will wrap
for (int i = 0; i < capacity; i++, nextNumberToPut++) {
buffer.add(nextNumberToPut);
referenceQueue.add(nextNumberToPut);
}
int gets = random.nextInt(capacity); // [0, capacity)
for (int i = 0; i < gets; i++) {
referenceQueue.poll();
buffer.remove();
}
int puts = random.nextInt(gets + 1); // [0, gets]
for (int i = 0; i < puts; i++, nextNumberToPut++) {
buffer.add(nextNumberToPut);
referenceQueue.add(nextNumberToPut);
}
Integer[] expected = referenceQueue.toArray(new Integer[0]);
buffer.resize(expected.length);
boolean equals = Arrays.equals(buffer.elements, 0, buffer.size,
expected, 0, expected.length);
assertTrue(equals);
}
private void queueOnce(int capacity, int numWraps) {
capacity = pow2Size(capacity);
Queue<Integer> referenceQueue = new ArrayBlockingQueue<>(capacity);
CircularBuffer<Integer> buffer = new CircularBuffer<>(capacity);
int nextNumberToPut = 0;
int totalPuts = 0;
int putsLimit = capacity * numWraps;
int remainingCapacity = capacity;
int size = 0;
while (totalPuts < putsLimit) {
assert remainingCapacity + size == capacity;
int puts = random.nextInt(remainingCapacity + 1); // [0, remainingCapacity]
remainingCapacity -= puts;
size += puts;
for (int i = 0; i < puts; i++, nextNumberToPut++) {
referenceQueue.add(nextNumberToPut);
buffer.add(nextNumberToPut);
}
totalPuts += puts;
int gets = random.nextInt(size + 1); // [0, size]
size -= gets;
remainingCapacity += gets;
for (int i = 0; i < gets; i++) {
Integer expected = referenceQueue.poll();
Integer actual = buffer.remove();
assertEquals(expected, actual);
}
}
}
}

View file

@ -0,0 +1,726 @@
/*
* 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
* 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.
*/
package jdk.internal.net.http.hpack;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static jdk.internal.net.http.hpack.TestHelper.*;
import org.junit.jupiter.api.Test;
//
// Tests whose names start with "testX" are the ones captured from real HPACK
// use cases
//
public final class DecoderTest {
//
// https://tools.ietf.org/html/rfc7541#appendix-C.2.1
//
@Test
public void example1() {
// @formatter:off
test("400a 6375 7374 6f6d 2d6b 6579 0d63 7573\n" +
"746f 6d2d 6865 6164 6572",
"[ 1] (s = 55) custom-key: custom-header\n" +
" Table size: 55",
"custom-key: custom-header");
// @formatter:on
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.2.2
//
@Test
public void example2() {
// @formatter:off
test("040c 2f73 616d 706c 652f 7061 7468",
"empty.",
":path: /sample/path");
// @formatter:on
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.2.3
//
@Test
public void example3() {
// @formatter:off
test("1008 7061 7373 776f 7264 0673 6563 7265\n" +
"74",
"empty.",
"password: secret");
// @formatter:on
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.2.4
//
@Test
public void example4() {
// @formatter:off
test("82",
"empty.",
":method: GET");
// @formatter:on
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.3
//
@Test
public void example5() {
// @formatter:off
Decoder d = new Decoder(256);
test(d, "8286 8441 0f77 7777 2e65 7861 6d70 6c65\n" +
"2e63 6f6d",
"[ 1] (s = 57) :authority: www.example.com\n" +
" Table size: 57",
":method: GET\n" +
":scheme: http\n" +
":path: /\n" +
":authority: www.example.com");
test(d, "8286 84be 5808 6e6f 2d63 6163 6865",
"[ 1] (s = 53) cache-control: no-cache\n" +
"[ 2] (s = 57) :authority: www.example.com\n" +
" Table size: 110",
":method: GET\n" +
":scheme: http\n" +
":path: /\n" +
":authority: www.example.com\n" +
"cache-control: no-cache");
test(d, "8287 85bf 400a 6375 7374 6f6d 2d6b 6579\n" +
"0c63 7573 746f 6d2d 7661 6c75 65",
"[ 1] (s = 54) custom-key: custom-value\n" +
"[ 2] (s = 53) cache-control: no-cache\n" +
"[ 3] (s = 57) :authority: www.example.com\n" +
" Table size: 164",
":method: GET\n" +
":scheme: https\n" +
":path: /index.html\n" +
":authority: www.example.com\n" +
"custom-key: custom-value");
// @formatter:on
}
@Test
public void example5AllSplits() {
// @formatter:off
testAllSplits(
"8286 8441 0f77 7777 2e65 7861 6d70 6c65\n" +
"2e63 6f6d",
"[ 1] (s = 57) :authority: www.example.com\n" +
" Table size: 57",
":method: GET\n" +
":scheme: http\n" +
":path: /\n" +
":authority: www.example.com");
// @formatter:on
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.4
//
@Test
public void example6() {
// @formatter:off
Decoder d = new Decoder(256);
test(d, "8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4\n" +
"ff",
"[ 1] (s = 57) :authority: www.example.com\n" +
" Table size: 57",
":method: GET\n" +
":scheme: http\n" +
":path: /\n" +
":authority: www.example.com");
test(d, "8286 84be 5886 a8eb 1064 9cbf",
"[ 1] (s = 53) cache-control: no-cache\n" +
"[ 2] (s = 57) :authority: www.example.com\n" +
" Table size: 110",
":method: GET\n" +
":scheme: http\n" +
":path: /\n" +
":authority: www.example.com\n" +
"cache-control: no-cache");
test(d, "8287 85bf 4088 25a8 49e9 5ba9 7d7f 8925\n" +
"a849 e95b b8e8 b4bf",
"[ 1] (s = 54) custom-key: custom-value\n" +
"[ 2] (s = 53) cache-control: no-cache\n" +
"[ 3] (s = 57) :authority: www.example.com\n" +
" Table size: 164",
":method: GET\n" +
":scheme: https\n" +
":path: /index.html\n" +
":authority: www.example.com\n" +
"custom-key: custom-value");
// @formatter:on
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.5
//
@Test
public void example7() {
// @formatter:off
Decoder d = new Decoder(256);
test(d, "4803 3330 3258 0770 7269 7661 7465 611d\n" +
"4d6f 6e2c 2032 3120 4f63 7420 3230 3133\n" +
"2032 303a 3133 3a32 3120 474d 546e 1768\n" +
"7474 7073 3a2f 2f77 7777 2e65 7861 6d70\n" +
"6c65 2e63 6f6d",
"[ 1] (s = 63) location: https://www.example.com\n" +
"[ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" +
"[ 3] (s = 52) cache-control: private\n" +
"[ 4] (s = 42) :status: 302\n" +
" Table size: 222",
":status: 302\n" +
"cache-control: private\n" +
"date: Mon, 21 Oct 2013 20:13:21 GMT\n" +
"location: https://www.example.com");
test(d, "4803 3330 37c1 c0bf",
"[ 1] (s = 42) :status: 307\n" +
"[ 2] (s = 63) location: https://www.example.com\n" +
"[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" +
"[ 4] (s = 52) cache-control: private\n" +
" Table size: 222",
":status: 307\n" +
"cache-control: private\n" +
"date: Mon, 21 Oct 2013 20:13:21 GMT\n" +
"location: https://www.example.com");
test(d, "88c1 611d 4d6f 6e2c 2032 3120 4f63 7420\n" +
"3230 3133 2032 303a 3133 3a32 3220 474d\n" +
"54c0 5a04 677a 6970 7738 666f 6f3d 4153\n" +
"444a 4b48 514b 425a 584f 5157 454f 5049\n" +
"5541 5851 5745 4f49 553b 206d 6178 2d61\n" +
"6765 3d33 3630 303b 2076 6572 7369 6f6e\n" +
"3d31",
"[ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1\n" +
"[ 2] (s = 52) content-encoding: gzip\n" +
"[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT\n" +
" Table size: 215",
":status: 200\n" +
"cache-control: private\n" +
"date: Mon, 21 Oct 2013 20:13:22 GMT\n" +
"location: https://www.example.com\n" +
"content-encoding: gzip\n" +
"set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1");
// @formatter:on
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.6
//
@Test
public void example8() {
// @formatter:off
Decoder d = new Decoder(256);
test(d, "4882 6402 5885 aec3 771a 4b61 96d0 7abe\n" +
"9410 54d4 44a8 2005 9504 0b81 66e0 82a6\n" +
"2d1b ff6e 919d 29ad 1718 63c7 8f0b 97c8\n" +
"e9ae 82ae 43d3",
"[ 1] (s = 63) location: https://www.example.com\n" +
"[ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" +
"[ 3] (s = 52) cache-control: private\n" +
"[ 4] (s = 42) :status: 302\n" +
" Table size: 222",
":status: 302\n" +
"cache-control: private\n" +
"date: Mon, 21 Oct 2013 20:13:21 GMT\n" +
"location: https://www.example.com");
test(d, "4883 640e ffc1 c0bf",
"[ 1] (s = 42) :status: 307\n" +
"[ 2] (s = 63) location: https://www.example.com\n" +
"[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" +
"[ 4] (s = 52) cache-control: private\n" +
" Table size: 222",
":status: 307\n" +
"cache-control: private\n" +
"date: Mon, 21 Oct 2013 20:13:21 GMT\n" +
"location: https://www.example.com");
test(d, "88c1 6196 d07a be94 1054 d444 a820 0595\n" +
"040b 8166 e084 a62d 1bff c05a 839b d9ab\n" +
"77ad 94e7 821d d7f2 e6c7 b335 dfdf cd5b\n" +
"3960 d5af 2708 7f36 72c1 ab27 0fb5 291f\n" +
"9587 3160 65c0 03ed 4ee5 b106 3d50 07",
"[ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1\n" +
"[ 2] (s = 52) content-encoding: gzip\n" +
"[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT\n" +
" Table size: 215",
":status: 200\n" +
"cache-control: private\n" +
"date: Mon, 21 Oct 2013 20:13:22 GMT\n" +
"location: https://www.example.com\n" +
"content-encoding: gzip\n" +
"set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1");
// @formatter:on
}
@Test
// One of responses from Apache Server that helped to catch a bug
public void testX() {
Decoder d = new Decoder(4096);
// @formatter:off
test(d, "3fe1 1f88 6196 d07a be94 03ea 693f 7504\n" +
"00b6 a05c b827 2e32 fa98 b46f 769e 86b1\n" +
"9272 b025 da5c 2ea9 fd70 a8de 7fb5 3556\n" +
"5ab7 6ece c057 02e2 2ad2 17bf 6c96 d07a\n" +
"be94 0854 cb6d 4a08 0075 40bd 71b6 6e05\n" +
"a531 68df 0f13 8efe 4522 cd32 21b6 5686\n" +
"eb23 781f cf52 848f d24a 8f0f 0d02 3435\n" +
"5f87 497c a589 d34d 1f",
"[ 1] (s = 53) content-type: text/html\n" +
"[ 2] (s = 50) accept-ranges: bytes\n" +
"[ 3] (s = 74) last-modified: Mon, 11 Jun 2007 18:53:14 GMT\n" +
"[ 4] (s = 77) server: Apache/2.4.17 (Unix) OpenSSL/1.0.2e-dev\n" +
"[ 5] (s = 65) date: Mon, 09 Nov 2015 16:26:39 GMT\n" +
" Table size: 319",
":status: 200\n" +
"date: Mon, 09 Nov 2015 16:26:39 GMT\n" +
"server: Apache/2.4.17 (Unix) OpenSSL/1.0.2e-dev\n" +
"last-modified: Mon, 11 Jun 2007 18:53:14 GMT\n" +
"etag: \"2d-432a5e4a73a80\"\n" +
"accept-ranges: bytes\n" +
"content-length: 45\n" +
"content-type: text/html");
// @formatter:on
}
@Test
public void testX1() {
// Supplier of a decoder with a particular state
Supplier<Decoder> s = () -> {
Decoder d = new Decoder(4096);
// @formatter:off
test(d, "88 76 92 ca 54 a7 d7 f4 fa ec af ed 6d da 61 d7 bb 1e ad ff" +
"df 61 97 c3 61 be 94 13 4a 65 b6 a5 04 00 b8 a0 5a b8 db 77" +
"1b 71 4c 5a 37 ff 0f 0d 84 08 00 00 03",
"[ 1] (s = 65) date: Fri, 24 Jun 2016 14:55:56 GMT\n" +
"[ 2] (s = 59) server: Jetty(9.3.z-SNAPSHOT)\n" +
" Table size: 124",
":status: 200\n" +
"server: Jetty(9.3.z-SNAPSHOT)\n" +
"date: Fri, 24 Jun 2016 14:55:56 GMT\n" +
"content-length: 100000"
);
// @formatter:on
return d;
};
// For all splits of the following data fed to the supplied decoder we
// must get what's expected
// @formatter:off
testAllSplits(s,
"88 bf be 0f 0d 84 08 00 00 03",
"[ 1] (s = 65) date: Fri, 24 Jun 2016 14:55:56 GMT\n" +
"[ 2] (s = 59) server: Jetty(9.3.z-SNAPSHOT)\n" +
" Table size: 124",
":status: 200\n" +
"server: Jetty(9.3.z-SNAPSHOT)\n" +
"date: Fri, 24 Jun 2016 14:55:56 GMT\n" +
"content-length: 100000");
// @formatter:on
}
//
// This test is missing in the spec
//
@Test
public void sizeUpdate() throws IOException {
Decoder d = new Decoder(4096);
assertEquals(4096, d.getTable().maxSize());
d.decode(ByteBuffer.wrap(new byte[]{0b00111110}), true, nopCallback()); // newSize = 30
assertEquals(30, d.getTable().maxSize());
}
@Test
public void incorrectSizeUpdate() {
ByteBuffer b = ByteBuffer.allocate(8);
Encoder e = new Encoder(8192) {
@Override
protected int calculateCapacity(int maxCapacity) {
return maxCapacity;
}
};
e.header("a", "b");
e.encode(b);
b.flip();
{
Decoder d = new Decoder(4096);
assertVoidThrows(IOException.class,
() -> d.decode(b, true, (name, value) -> { }));
}
b.flip();
{
Decoder d = new Decoder(4096);
assertVoidThrows(IOException.class,
() -> d.decode(b, false, (name, value) -> { }));
}
}
@Test
public void corruptedHeaderBlockInteger() {
Decoder d = new Decoder(4096);
ByteBuffer data = ByteBuffer.wrap(new byte[]{
(byte) 0b11111111, // indexed
(byte) 0b10011010 // 25 + ...
});
IOException e = assertVoidThrows(IOException.class,
() -> d.decode(data, true, nopCallback()));
assertExceptionMessageContains(e, "Unexpected end of header block");
}
// 5.1. Integer Representation
// ...
// Integer encodings that exceed implementation limits -- in value or octet
// length -- MUST be treated as decoding errors. Different limits can
// be set for each of the different uses of integers, based on
// implementation constraints.
@Test
public void headerBlockIntegerNoOverflow() {
Decoder d = new Decoder(4096);
ByteBuffer data = ByteBuffer.wrap(new byte[]{
(byte) 0b11111111, // indexed + 127
// Integer.MAX_VALUE - 127 (base 128, little-endian):
(byte) 0b10000000,
(byte) 0b11111111,
(byte) 0b11111111,
(byte) 0b11111111,
(byte) 0b00000111
});
IOException e = assertVoidThrows(IOException.class,
() -> d.decode(data, true, nopCallback()));
assertExceptionMessageContains(e.getCause(), "index=2147483647");
}
@Test
public void headerBlockIntegerOverflow() {
Decoder d = new Decoder(4096);
ByteBuffer data = ByteBuffer.wrap(new byte[]{
(byte) 0b11111111, // indexed + 127
// Integer.MAX_VALUE - 127 + 1 (base 128, little endian):
(byte) 0b10000001,
(byte) 0b11111111,
(byte) 0b11111111,
(byte) 0b11111111,
(byte) 0b00000111
});
IOException e = assertVoidThrows(IOException.class,
() -> d.decode(data, true, nopCallback()));
assertExceptionMessageContains(e, "Integer overflow");
}
@Test
public void corruptedHeaderBlockString1() {
Decoder d = new Decoder(4096);
ByteBuffer data = ByteBuffer.wrap(new byte[]{
0b00001111, // literal, index=15
0b00000000,
0b00001000, // huffman=false, length=8
0b00000000, // \
0b00000000, // but only 3 octets available...
0b00000000 // /
});
IOException e = assertVoidThrows(IOException.class,
() -> d.decode(data, true, nopCallback()));
assertExceptionMessageContains(e, "Unexpected end of header block");
}
@Test
public void corruptedHeaderBlockString2() {
Decoder d = new Decoder(4096);
ByteBuffer data = ByteBuffer.wrap(new byte[]{
0b00001111, // literal, index=15
0b00000000,
(byte) 0b10001000, // huffman=true, length=8
0b00000000, // \
0b00000000, // \
0b00000000, // but only 5 octets available...
0b00000000, // /
0b00000000 // /
});
IOException e = assertVoidThrows(IOException.class,
() -> d.decode(data, true, nopCallback()));
assertExceptionMessageContains(e, "Unexpected end of header block");
}
// 5.2. String Literal Representation
// ...A Huffman-encoded string literal containing the EOS symbol MUST be
// treated as a decoding error...
@Test
public void corruptedHeaderBlockHuffmanStringEOS() {
Decoder d = new Decoder(4096);
ByteBuffer data = ByteBuffer.wrap(new byte[]{
0b00001111, // literal, index=15
0b00000000,
(byte) 0b10000110, // huffman=true, length=6
0b00011001, 0b01001101, (byte) 0b11111111,
(byte) 0b11111111, (byte) 0b11111111, (byte) 0b11111100
});
IOException e = assertVoidThrows(IOException.class,
() -> d.decode(data, true, nopCallback()));
assertExceptionMessageContains(e, "Encountered EOS");
}
// 5.2. String Literal Representation
// ...A padding strictly longer than 7 bits MUST be treated as a decoding
// error...
@Test
public void corruptedHeaderBlockHuffmanStringLongPadding1() {
Decoder d = new Decoder(4096);
ByteBuffer data = ByteBuffer.wrap(new byte[]{
0b00001111, // literal, index=15
0b00000000,
(byte) 0b10000011, // huffman=true, length=3
0b00011001, 0b01001101, (byte) 0b11111111
// len("aei") + len(padding) = (5 + 5 + 5) + (9)
});
IOException e = assertVoidThrows(IOException.class,
() -> d.decode(data, true, nopCallback()));
assertExceptionMessageContains(e, "Padding is too long", "len=9");
}
@Test
public void corruptedHeaderBlockHuffmanStringLongPadding2() {
Decoder d = new Decoder(4096);
ByteBuffer data = ByteBuffer.wrap(new byte[]{
0b00001111, // literal, index=15
0b00000000,
(byte) 0b10000011, // huffman=true, length=3
0b00011001, 0b01111010, (byte) 0b11111111
// len("aek") + len(padding) = (5 + 5 + 7) + (7)
});
assertVoidDoesNotThrow(() -> d.decode(data, true, nopCallback()));
}
// 5.2. String Literal Representation
// ...A padding not corresponding to the most significant bits of the code
// for the EOS symbol MUST be treated as a decoding error...
@Test
public void corruptedHeaderBlockHuffmanStringNotEOSPadding() {
Decoder d = new Decoder(4096);
ByteBuffer data = ByteBuffer.wrap(new byte[]{
0b00001111, // literal, index=15
0b00000000,
(byte) 0b10000011, // huffman=true, length=3
0b00011001, 0b01111010, (byte) 0b11111110
});
IOException e = assertVoidThrows(IOException.class,
() -> d.decode(data, true, nopCallback()));
assertExceptionMessageContains(e, "Not a EOS prefix");
}
@Test
public void argsTestBiConsumerIsNull() {
Decoder decoder = new Decoder(4096);
assertVoidThrows(NullPointerException.class,
() -> decoder.decode(ByteBuffer.allocate(16), true, null));
}
@Test
public void argsTestByteBufferIsNull() {
Decoder decoder = new Decoder(4096);
assertVoidThrows(NullPointerException.class,
() -> decoder.decode(null, true, nopCallback()));
}
@Test
public void argsTestBothAreNull() {
Decoder decoder = new Decoder(4096);
assertVoidThrows(NullPointerException.class,
() -> decoder.decode(null, true, null));
}
private static void test(String hexdump,
String headerTable, String headerList) {
test(new Decoder(4096), hexdump, headerTable, headerList);
}
private static void testAllSplits(String hexdump,
String expectedHeaderTable,
String expectedHeaderList) {
testAllSplits(() -> new Decoder(256), hexdump, expectedHeaderTable, expectedHeaderList);
}
private static void testAllSplits(Supplier<Decoder> supplier,
String hexdump,
String expectedHeaderTable,
String expectedHeaderList) {
ByteBuffer source = SpecHelper.toBytes(hexdump);
BuffersTestingKit.forEachSplit(source, iterable -> {
List<String> actual = new LinkedList<>();
Iterator<? extends ByteBuffer> i = iterable.iterator();
if (!i.hasNext()) {
return;
}
Decoder d = supplier.get();
do {
ByteBuffer n = i.next();
try {
d.decode(n, !i.hasNext(), (name, value) -> {
if (value == null) {
actual.add(name.toString());
} else {
actual.add(name + ": " + value);
}
});
} catch (IOException e) {
throw new UncheckedIOException(e);
}
} while (i.hasNext());
assertEquals(expectedHeaderTable, d.getTable().getStateString());
assertEquals(expectedHeaderList, actual.stream().collect(Collectors.joining("\n")));
});
// Now introduce last ByteBuffer which is empty and EOF (mimics idiom
// I've found in HttpClient code)
BuffersTestingKit.forEachSplit(source, iterable -> {
List<String> actual = new LinkedList<>();
Iterator<? extends ByteBuffer> i = iterable.iterator();
if (!i.hasNext()) {
return;
}
Decoder d = supplier.get();
do {
ByteBuffer n = i.next();
try {
d.decode(n, false, (name, value) -> {
if (value == null) {
actual.add(name.toString());
} else {
actual.add(name + ": " + value);
}
});
} catch (IOException e) {
throw new UncheckedIOException(e);
}
} while (i.hasNext());
try {
d.decode(ByteBuffer.allocate(0), false, (name, value) -> {
if (value == null) {
actual.add(name.toString());
} else {
actual.add(name + ": " + value);
}
});
} catch (IOException e) {
throw new UncheckedIOException(e);
}
assertEquals(expectedHeaderTable, d.getTable().getStateString());
assertEquals(expectedHeaderList, actual.stream().collect(Collectors.joining("\n")));
});
}
//
// Sometimes we need to keep the same decoder along several runs,
// as it models the same connection
//
private static void test(Decoder d, String hexdump,
String expectedHeaderTable, String expectedHeaderList) {
ByteBuffer source = SpecHelper.toBytes(hexdump);
List<String> actual = new LinkedList<>();
try {
d.decode(source, true, (name, value) -> {
if (value == null) {
actual.add(name.toString());
} else {
actual.add(name + ": " + value);
}
});
} catch (IOException e) {
throw new UncheckedIOException(e);
}
assertEquals(expectedHeaderTable, d.getTable().getStateString());
assertEquals(expectedHeaderList, actual.stream().collect(Collectors.joining("\n")));
}
private static DecodingCallback nopCallback() {
return (t, u) -> { };
}
}

View file

@ -0,0 +1,693 @@
/*
* Copyright (c) 2014, 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.
*/
package jdk.internal.net.http.hpack;
import java.io.IOException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import static jdk.internal.net.http.hpack.BuffersTestingKit.concat;
import static jdk.internal.net.http.hpack.BuffersTestingKit.forEachSplit;
import static jdk.internal.net.http.hpack.SpecHelper.toHexdump;
import static jdk.internal.net.http.hpack.TestHelper.assertVoidThrows;
import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
// TODO: map textual representation of commands from the spec to actual
// calls to encoder (actually, this is a good idea for decoder as well)
public final class EncoderTest {
//
// https://tools.ietf.org/html/rfc7541#appendix-C.2.1
//
@Test
public void example1() {
Encoder e = newCustomEncoder(256);
drainInitialUpdate(e);
e.literalWithIndexing("custom-key", false, "custom-header", false);
// @formatter:off
test(e,
"400a 6375 7374 6f6d 2d6b 6579 0d63 7573\n" +
"746f 6d2d 6865 6164 6572",
"[ 1] (s = 55) custom-key: custom-header\n" +
" Table size: 55");
// @formatter:on
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.2.2
//
@Test
public void example2() {
Encoder e = newCustomEncoder(256);
drainInitialUpdate(e);
e.literal(4, "/sample/path", false);
// @formatter:off
test(e,
"040c 2f73 616d 706c 652f 7061 7468",
"empty.");
// @formatter:on
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.2.3
//
@Test
public void example3() {
Encoder e = newCustomEncoder(256);
drainInitialUpdate(e);
e.literalNeverIndexed("password", false, "secret", false);
// @formatter:off
test(e,
"1008 7061 7373 776f 7264 0673 6563 7265\n" +
"74",
"empty.");
// @formatter:on
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.2.4
//
@Test
public void example4() {
Encoder e = newCustomEncoder(256);
drainInitialUpdate(e);
e.indexed(2);
// @formatter:off
test(e,
"82",
"empty.");
// @formatter:on
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.3
//
@Test
public void example5() {
Encoder e = newCustomEncoder(256);
drainInitialUpdate(e);
ByteBuffer output = ByteBuffer.allocate(64);
e.indexed(2);
e.encode(output);
e.indexed(6);
e.encode(output);
e.indexed(4);
e.encode(output);
e.literalWithIndexing(1, "www.example.com", false);
e.encode(output);
output.flip();
// @formatter:off
test(e, output,
"8286 8441 0f77 7777 2e65 7861 6d70 6c65\n" +
"2e63 6f6d",
"[ 1] (s = 57) :authority: www.example.com\n" +
" Table size: 57");
output.clear();
e.indexed( 2);
e.encode(output);
e.indexed( 6);
e.encode(output);
e.indexed( 4);
e.encode(output);
e.indexed(62);
e.encode(output);
e.literalWithIndexing(24, "no-cache", false);
e.encode(output);
output.flip();
test(e, output,
"8286 84be 5808 6e6f 2d63 6163 6865",
"[ 1] (s = 53) cache-control: no-cache\n" +
"[ 2] (s = 57) :authority: www.example.com\n" +
" Table size: 110");
output.clear();
e.indexed( 2);
e.encode(output);
e.indexed( 7);
e.encode(output);
e.indexed( 5);
e.encode(output);
e.indexed(63);
e.encode(output);
e.literalWithIndexing("custom-key", false, "custom-value", false);
e.encode(output);
output.flip();
test(e, output,
"8287 85bf 400a 6375 7374 6f6d 2d6b 6579\n" +
"0c63 7573 746f 6d2d 7661 6c75 65",
"[ 1] (s = 54) custom-key: custom-value\n" +
"[ 2] (s = 53) cache-control: no-cache\n" +
"[ 3] (s = 57) :authority: www.example.com\n" +
" Table size: 164");
// @formatter:on
}
@Test
public void example5AllSplits() {
List<Consumer<Encoder>> actions = new LinkedList<>();
actions.add(e -> e.indexed(2));
actions.add(e -> e.indexed(6));
actions.add(e -> e.indexed(4));
actions.add(e -> e.literalWithIndexing(1, "www.example.com", false));
encodeAllSplits(
actions,
"8286 8441 0f77 7777 2e65 7861 6d70 6c65\n" +
"2e63 6f6d",
"[ 1] (s = 57) :authority: www.example.com\n" +
" Table size: 57");
}
private static void encodeAllSplits(Iterable<Consumer<Encoder>> consumers,
String expectedHexdump,
String expectedTableState) {
ByteBuffer buffer = SpecHelper.toBytes(expectedHexdump);
erase(buffer); // Zeroed buffer of size needed to hold the encoding
forEachSplit(buffer, iterable -> {
List<ByteBuffer> copy = new LinkedList<>();
iterable.forEach(b -> copy.add(ByteBuffer.allocate(b.remaining())));
Iterator<ByteBuffer> output = copy.iterator();
if (!output.hasNext()) {
throw new IllegalStateException("No buffers to encode to");
}
Encoder e = newCustomEncoder(256); // FIXME: pull up (as a parameter)
drainInitialUpdate(e);
boolean encoded;
ByteBuffer b = output.next();
for (Consumer<Encoder> c : consumers) {
c.accept(e);
do {
encoded = e.encode(b);
if (!encoded) {
if (output.hasNext()) {
b = output.next();
} else {
throw new IllegalStateException("No room for encoding");
}
}
}
while (!encoded);
}
copy.forEach(Buffer::flip);
ByteBuffer data = concat(copy);
test(e, data, expectedHexdump, expectedTableState);
});
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.4
//
@Test
public void example6() {
Encoder e = newCustomEncoder(256);
drainInitialUpdate(e);
ByteBuffer output = ByteBuffer.allocate(64);
e.indexed(2);
e.encode(output);
e.indexed(6);
e.encode(output);
e.indexed(4);
e.encode(output);
e.literalWithIndexing(1, "www.example.com", true);
e.encode(output);
output.flip();
// @formatter:off
test(e, output,
"8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4\n" +
"ff",
"[ 1] (s = 57) :authority: www.example.com\n" +
" Table size: 57");
output.clear();
e.indexed( 2);
e.encode(output);
e.indexed( 6);
e.encode(output);
e.indexed( 4);
e.encode(output);
e.indexed(62);
e.encode(output);
e.literalWithIndexing(24, "no-cache", true);
e.encode(output);
output.flip();
test(e, output,
"8286 84be 5886 a8eb 1064 9cbf",
"[ 1] (s = 53) cache-control: no-cache\n" +
"[ 2] (s = 57) :authority: www.example.com\n" +
" Table size: 110");
output.clear();
e.indexed( 2);
e.encode(output);
e.indexed( 7);
e.encode(output);
e.indexed( 5);
e.encode(output);
e.indexed(63);
e.encode(output);
e.literalWithIndexing("custom-key", true, "custom-value", true);
e.encode(output);
output.flip();
test(e, output,
"8287 85bf 4088 25a8 49e9 5ba9 7d7f 8925\n" +
"a849 e95b b8e8 b4bf",
"[ 1] (s = 54) custom-key: custom-value\n" +
"[ 2] (s = 53) cache-control: no-cache\n" +
"[ 3] (s = 57) :authority: www.example.com\n" +
" Table size: 164");
// @formatter:on
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.5
//
@Test
public void example7() {
Encoder e = newCustomEncoder(256);
drainInitialUpdate(e);
ByteBuffer output = ByteBuffer.allocate(128);
// @formatter:off
e.literalWithIndexing( 8, "302", false);
e.encode(output);
e.literalWithIndexing(24, "private", false);
e.encode(output);
e.literalWithIndexing(33, "Mon, 21 Oct 2013 20:13:21 GMT", false);
e.encode(output);
e.literalWithIndexing(46, "https://www.example.com", false);
e.encode(output);
output.flip();
test(e, output,
"4803 3330 3258 0770 7269 7661 7465 611d\n" +
"4d6f 6e2c 2032 3120 4f63 7420 3230 3133\n" +
"2032 303a 3133 3a32 3120 474d 546e 1768\n" +
"7474 7073 3a2f 2f77 7777 2e65 7861 6d70\n" +
"6c65 2e63 6f6d",
"[ 1] (s = 63) location: https://www.example.com\n" +
"[ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" +
"[ 3] (s = 52) cache-control: private\n" +
"[ 4] (s = 42) :status: 302\n" +
" Table size: 222");
output.clear();
e.literalWithIndexing( 8, "307", false);
e.encode(output);
e.indexed(65);
e.encode(output);
e.indexed(64);
e.encode(output);
e.indexed(63);
e.encode(output);
output.flip();
test(e, output,
"4803 3330 37c1 c0bf",
"[ 1] (s = 42) :status: 307\n" +
"[ 2] (s = 63) location: https://www.example.com\n" +
"[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" +
"[ 4] (s = 52) cache-control: private\n" +
" Table size: 222");
output.clear();
e.indexed( 8);
e.encode(output);
e.indexed(65);
e.encode(output);
e.literalWithIndexing(33, "Mon, 21 Oct 2013 20:13:22 GMT", false);
e.encode(output);
e.indexed(64);
e.encode(output);
e.literalWithIndexing(26, "gzip", false);
e.encode(output);
e.literalWithIndexing(55, "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1", false);
e.encode(output);
output.flip();
test(e, output,
"88c1 611d 4d6f 6e2c 2032 3120 4f63 7420\n" +
"3230 3133 2032 303a 3133 3a32 3220 474d\n" +
"54c0 5a04 677a 6970 7738 666f 6f3d 4153\n" +
"444a 4b48 514b 425a 584f 5157 454f 5049\n" +
"5541 5851 5745 4f49 553b 206d 6178 2d61\n" +
"6765 3d33 3630 303b 2076 6572 7369 6f6e\n" +
"3d31",
"[ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1\n" +
"[ 2] (s = 52) content-encoding: gzip\n" +
"[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT\n" +
" Table size: 215");
// @formatter:on
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.6
//
@Test
public void example8() {
Encoder e = newCustomEncoder(256);
drainInitialUpdate(e);
ByteBuffer output = ByteBuffer.allocate(128);
// @formatter:off
e.literalWithIndexing( 8, "302", true);
e.encode(output);
e.literalWithIndexing(24, "private", true);
e.encode(output);
e.literalWithIndexing(33, "Mon, 21 Oct 2013 20:13:21 GMT", true);
e.encode(output);
e.literalWithIndexing(46, "https://www.example.com", true);
e.encode(output);
output.flip();
test(e, output,
"4882 6402 5885 aec3 771a 4b61 96d0 7abe\n" +
"9410 54d4 44a8 2005 9504 0b81 66e0 82a6\n" +
"2d1b ff6e 919d 29ad 1718 63c7 8f0b 97c8\n" +
"e9ae 82ae 43d3",
"[ 1] (s = 63) location: https://www.example.com\n" +
"[ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" +
"[ 3] (s = 52) cache-control: private\n" +
"[ 4] (s = 42) :status: 302\n" +
" Table size: 222");
output.clear();
e.literalWithIndexing( 8, "307", true);
e.encode(output);
e.indexed(65);
e.encode(output);
e.indexed(64);
e.encode(output);
e.indexed(63);
e.encode(output);
output.flip();
test(e, output,
"4883 640e ffc1 c0bf",
"[ 1] (s = 42) :status: 307\n" +
"[ 2] (s = 63) location: https://www.example.com\n" +
"[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" +
"[ 4] (s = 52) cache-control: private\n" +
" Table size: 222");
output.clear();
e.indexed( 8);
e.encode(output);
e.indexed(65);
e.encode(output);
e.literalWithIndexing(33, "Mon, 21 Oct 2013 20:13:22 GMT", true);
e.encode(output);
e.indexed(64);
e.encode(output);
e.literalWithIndexing(26, "gzip", true);
e.encode(output);
e.literalWithIndexing(55, "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1", true);
e.encode(output);
output.flip();
test(e, output,
"88c1 6196 d07a be94 1054 d444 a820 0595\n" +
"040b 8166 e084 a62d 1bff c05a 839b d9ab\n" +
"77ad 94e7 821d d7f2 e6c7 b335 dfdf cd5b\n" +
"3960 d5af 2708 7f36 72c1 ab27 0fb5 291f\n" +
"9587 3160 65c0 03ed 4ee5 b106 3d50 07",
"[ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1\n" +
"[ 2] (s = 52) content-encoding: gzip\n" +
"[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT\n" +
" Table size: 215");
// @formatter:on
}
@Test
public void initialSizeUpdateDefaultEncoder() throws IOException {
Function<Integer, Encoder> e = Encoder::new;
testSizeUpdate(e, 1024, asList(), asList(0));
testSizeUpdate(e, 1024, asList(1024), asList(0));
testSizeUpdate(e, 1024, asList(1024, 1024), asList(0));
testSizeUpdate(e, 1024, asList(1024, 512), asList(0));
testSizeUpdate(e, 1024, asList(512, 1024), asList(0));
testSizeUpdate(e, 1024, asList(512, 2048), asList(0));
}
@Test
public void initialSizeUpdateCustomEncoder() throws IOException {
Function<Integer, Encoder> e = EncoderTest::newCustomEncoder;
testSizeUpdate(e, 1024, asList(), asList(1024));
testSizeUpdate(e, 1024, asList(1024), asList(1024));
testSizeUpdate(e, 1024, asList(1024, 1024), asList(1024));
testSizeUpdate(e, 1024, asList(1024, 512), asList(512));
testSizeUpdate(e, 1024, asList(512, 1024), asList(1024));
testSizeUpdate(e, 1024, asList(512, 2048), asList(2048));
}
@Test
public void seriesOfSizeUpdatesDefaultEncoder() throws IOException {
Function<Integer, Encoder> e = c -> {
Encoder encoder = new Encoder(c);
drainInitialUpdate(encoder);
return encoder;
};
testSizeUpdate(e, 0, asList(0), asList());
testSizeUpdate(e, 1024, asList(1024), asList());
testSizeUpdate(e, 1024, asList(2048), asList());
testSizeUpdate(e, 1024, asList(512), asList());
testSizeUpdate(e, 1024, asList(1024, 1024), asList());
testSizeUpdate(e, 1024, asList(1024, 2048), asList());
testSizeUpdate(e, 1024, asList(2048, 1024), asList());
testSizeUpdate(e, 1024, asList(1024, 512), asList());
testSizeUpdate(e, 1024, asList(512, 1024), asList());
}
//
// https://tools.ietf.org/html/rfc7541#section-4.2
//
@Test
public void seriesOfSizeUpdatesCustomEncoder() throws IOException {
Function<Integer, Encoder> e = c -> {
Encoder encoder = newCustomEncoder(c);
drainInitialUpdate(encoder);
return encoder;
};
testSizeUpdate(e, 0, asList(0), asList());
testSizeUpdate(e, 1024, asList(1024), asList());
testSizeUpdate(e, 1024, asList(2048), asList(2048));
testSizeUpdate(e, 1024, asList(512), asList(512));
testSizeUpdate(e, 1024, asList(1024, 1024), asList());
testSizeUpdate(e, 1024, asList(1024, 2048), asList(2048));
testSizeUpdate(e, 1024, asList(2048, 1024), asList());
testSizeUpdate(e, 1024, asList(1024, 512), asList(512));
testSizeUpdate(e, 1024, asList(512, 1024), asList(512, 1024));
}
@Test
public void callSequenceViolations() {
{ // Hasn't set up a header
Encoder e = new Encoder(0);
assertVoidThrows(IllegalStateException.class, () -> e.encode(ByteBuffer.allocate(16)));
}
{ // Can't set up header while there's an unfinished encoding
Encoder e = new Encoder(0);
e.indexed(32);
assertVoidThrows(IllegalStateException.class, () -> e.indexed(32));
}
{ // Can't setMaxCapacity while there's an unfinished encoding
Encoder e = new Encoder(0);
e.indexed(32);
assertVoidThrows(IllegalStateException.class, () -> e.setMaxCapacity(512));
}
{ // Hasn't set up a header
Encoder e = new Encoder(0);
e.setMaxCapacity(256);
assertVoidThrows(IllegalStateException.class, () -> e.encode(ByteBuffer.allocate(16)));
}
{ // Hasn't set up a header after the previous encoding
Encoder e = new Encoder(0);
e.indexed(0);
boolean encoded = e.encode(ByteBuffer.allocate(16));
assertTrue(encoded); // assumption
assertVoidThrows(IllegalStateException.class, () -> e.encode(ByteBuffer.allocate(16)));
}
}
private static void test(Encoder encoder,
String expectedTableState,
String expectedHexdump) {
ByteBuffer b = ByteBuffer.allocate(128);
encoder.encode(b);
b.flip();
test(encoder, b, expectedTableState, expectedHexdump);
}
private static void test(Encoder encoder,
ByteBuffer output,
String expectedHexdump,
String expectedTableState) {
String actualTableState = encoder.getHeaderTable().getStateString();
assertEquals(expectedTableState, actualTableState);
String actualHexdump = toHexdump(output);
assertEquals(expectedHexdump.replaceAll("\\n", " "), actualHexdump);
}
// initial size - the size encoder is constructed with
// updates - a sequence of values for consecutive calls to encoder.setMaxCapacity
// expected - a sequence of values expected to be decoded by a decoder
private void testSizeUpdate(Function<Integer, Encoder> encoder,
int initialSize,
List<Integer> updates,
List<Integer> expected) throws IOException {
Encoder e = encoder.apply(initialSize);
updates.forEach(e::setMaxCapacity);
ByteBuffer b = ByteBuffer.allocate(64);
e.header("a", "b");
e.encode(b);
b.flip();
Decoder d = new Decoder(updates.isEmpty() ? initialSize : Collections.max(updates));
List<Integer> actual = new ArrayList<>();
d.decode(b, true, new DecodingCallback() {
@Override
public void onDecoded(CharSequence name, CharSequence value) { }
@Override
public void onSizeUpdate(int capacity) {
actual.add(capacity);
}
});
assertEquals(expected, actual);
}
//
// Default encoder does not need any table, therefore a subclass that
// behaves differently is needed
//
private static Encoder newCustomEncoder(int maxCapacity) {
return new Encoder(maxCapacity) {
@Override
protected int calculateCapacity(int maxCapacity) {
return maxCapacity;
}
};
}
private static void drainInitialUpdate(Encoder e) {
ByteBuffer b = ByteBuffer.allocate(4);
e.header("a", "b");
boolean done;
do {
done = e.encode(b);
b.flip();
} while (!done);
}
private static void erase(ByteBuffer buffer) {
buffer.clear();
while (buffer.hasRemaining()) {
buffer.put((byte) 0);
}
buffer.clear();
}
}

View file

@ -0,0 +1,152 @@
/*
* Copyright (c) 2014, 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.
*/
package jdk.internal.net.http.hpack;
import jdk.internal.net.http.hpack.SimpleHeaderTable.HeaderField;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class HeaderTableTest extends SimpleHeaderTableTest {
@Override
protected HeaderTable createHeaderTable(int maxSize) {
return new HeaderTable(maxSize, HPACK.getLogger());
}
@Test
public void staticData() {
HeaderTable table = createHeaderTable(0);
Map<Integer, HeaderField> staticHeaderFields = createStaticEntries();
Map<String, Set<Integer>> indexes = new HashMap<>();
for (Map.Entry<Integer, HeaderField> e : staticHeaderFields.entrySet()) {
Integer idx = e.getKey();
String name = e.getValue().name;
indexes.merge(name, Set.of(idx), (v1, v2) -> {
HashSet<Integer> s = new HashSet<>();
s.addAll(v1);
s.addAll(v2);
return s;
});
}
staticHeaderFields.forEach((key, expectedHeaderField) -> {
// lookup
HeaderField actualHeaderField = table.get(key);
assertEquals(expectedHeaderField.name, actualHeaderField.name);
assertEquals(expectedHeaderField.value, actualHeaderField.value);
// reverse lookup (name, value)
String hName = expectedHeaderField.name;
String hValue = expectedHeaderField.value;
int expectedIndex = key;
int actualIndex = table.indexOf(hName, hValue);
assertEquals(expectedIndex, actualIndex);
// reverse lookup (name)
Set<Integer> expectedIndexes = indexes.get(hName);
int actualMinimalIndex = table.indexOf(hName, "blah-blah");
assertTrue(expectedIndexes.contains(-actualMinimalIndex));
});
}
@Test
public void lowerIndexPriority() {
HeaderTable table = createHeaderTable(256);
int oldLength = table.length();
table.put("bender", "rodriguez");
table.put("bender", "rodriguez");
table.put("bender", "rodriguez");
assertEquals(oldLength + 3, table.length()); // more like an assumption
int i = table.indexOf("bender", "rodriguez");
assertEquals(oldLength + 1, i);
}
@Test
public void indexesAreNotLost2() {
HeaderTable table = createHeaderTable(256);
int oldLength = table.length();
table.put("bender", "rodriguez");
assertEquals(oldLength + 1, table.indexOf("bender", "rodriguez"));
table.put("bender", "rodriguez");
assertEquals(oldLength + 1, table.indexOf("bender", "rodriguez"));
table.evictEntry();
assertEquals(oldLength + 1, table.indexOf("bender", "rodriguez"));
table.evictEntry();
assertEquals(0, table.indexOf("bender", "rodriguez"));
}
@Test
public void lowerIndexPriority2() {
HeaderTable table = createHeaderTable(256);
int oldLength = table.length();
int idx = rnd.nextInt(oldLength) + 1;
HeaderField f = table.get(idx);
table.put(f.name, f.value);
assertEquals(oldLength + 1, table.length());
int i = table.indexOf(f.name, f.value);
assertEquals(idx, i);
}
@Test
public void indexOf() {
// Let's put a series of header fields
int NUM_HEADERS = 32;
HeaderTable table =
createHeaderTable((32 + 4) * NUM_HEADERS);
// ^ ^
// entry overhead symbols per entry (max 2x2 digits)
for (int i = 1; i <= NUM_HEADERS; i++) {
String s = String.valueOf(i);
table.put(s, s);
}
// and verify indexOf (reverse lookup) returns correct indexes for
// full lookup
for (int j = 1; j <= NUM_HEADERS; j++) {
String s = String.valueOf(j);
int actualIndex = table.indexOf(s, s);
int expectedIndex = STATIC_TABLE_LENGTH + NUM_HEADERS - j + 1;
assertEquals(expectedIndex, actualIndex);
}
// as well as for just a name lookup
for (int j = 1; j <= NUM_HEADERS; j++) {
String s = String.valueOf(j);
int actualIndex = table.indexOf(s, "blah");
int expectedIndex = -(STATIC_TABLE_LENGTH + NUM_HEADERS - j + 1);
assertEquals(expectedIndex, actualIndex);
}
// lookup for non-existent name returns 0
assertEquals(0, table.indexOf("chupacabra", "1"));
}
}

View file

@ -0,0 +1,839 @@
/*
* 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
* 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.
*/
package jdk.internal.net.http.hpack;
import jdk.internal.net.http.hpack.Huffman.Reader;
import jdk.internal.net.http.hpack.Huffman.Writer;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static jdk.internal.net.http.hpack.HPACK.bytesForBits;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
public final class HuffmanTest {
/*
* Implementations of Huffman.Reader and Huffman.Writer under test.
* Change them here.
*/
private static final Supplier<Reader> READER = QuickHuffman.Reader::new;
private static final Supplier<Writer> WRITER = QuickHuffman.Writer::new;
//
// https://tools.ietf.org/html/rfc7541#appendix-B
//
private static final String SPECIFICATION =
// @formatter:off
" code as bits as hex len\n" +
" sym aligned to MSB aligned in\n" +
" to LSB bits\n" +
" ( 0) |11111111|11000 1ff8 [13]\n" +
" ( 1) |11111111|11111111|1011000 7fffd8 [23]\n" +
" ( 2) |11111111|11111111|11111110|0010 fffffe2 [28]\n" +
" ( 3) |11111111|11111111|11111110|0011 fffffe3 [28]\n" +
" ( 4) |11111111|11111111|11111110|0100 fffffe4 [28]\n" +
" ( 5) |11111111|11111111|11111110|0101 fffffe5 [28]\n" +
" ( 6) |11111111|11111111|11111110|0110 fffffe6 [28]\n" +
" ( 7) |11111111|11111111|11111110|0111 fffffe7 [28]\n" +
" ( 8) |11111111|11111111|11111110|1000 fffffe8 [28]\n" +
" ( 9) |11111111|11111111|11101010 ffffea [24]\n" +
" ( 10) |11111111|11111111|11111111|111100 3ffffffc [30]\n" +
" ( 11) |11111111|11111111|11111110|1001 fffffe9 [28]\n" +
" ( 12) |11111111|11111111|11111110|1010 fffffea [28]\n" +
" ( 13) |11111111|11111111|11111111|111101 3ffffffd [30]\n" +
" ( 14) |11111111|11111111|11111110|1011 fffffeb [28]\n" +
" ( 15) |11111111|11111111|11111110|1100 fffffec [28]\n" +
" ( 16) |11111111|11111111|11111110|1101 fffffed [28]\n" +
" ( 17) |11111111|11111111|11111110|1110 fffffee [28]\n" +
" ( 18) |11111111|11111111|11111110|1111 fffffef [28]\n" +
" ( 19) |11111111|11111111|11111111|0000 ffffff0 [28]\n" +
" ( 20) |11111111|11111111|11111111|0001 ffffff1 [28]\n" +
" ( 21) |11111111|11111111|11111111|0010 ffffff2 [28]\n" +
" ( 22) |11111111|11111111|11111111|111110 3ffffffe [30]\n" +
" ( 23) |11111111|11111111|11111111|0011 ffffff3 [28]\n" +
" ( 24) |11111111|11111111|11111111|0100 ffffff4 [28]\n" +
" ( 25) |11111111|11111111|11111111|0101 ffffff5 [28]\n" +
" ( 26) |11111111|11111111|11111111|0110 ffffff6 [28]\n" +
" ( 27) |11111111|11111111|11111111|0111 ffffff7 [28]\n" +
" ( 28) |11111111|11111111|11111111|1000 ffffff8 [28]\n" +
" ( 29) |11111111|11111111|11111111|1001 ffffff9 [28]\n" +
" ( 30) |11111111|11111111|11111111|1010 ffffffa [28]\n" +
" ( 31) |11111111|11111111|11111111|1011 ffffffb [28]\n" +
" ' ' ( 32) |010100 14 [ 6]\n" +
" '!' ( 33) |11111110|00 3f8 [10]\n" +
" '\"' ( 34) |11111110|01 3f9 [10]\n" +
" '#' ( 35) |11111111|1010 ffa [12]\n" +
" '$' ( 36) |11111111|11001 1ff9 [13]\n" +
" '%' ( 37) |010101 15 [ 6]\n" +
" '&' ( 38) |11111000 f8 [ 8]\n" +
" ''' ( 39) |11111111|010 7fa [11]\n" +
" '(' ( 40) |11111110|10 3fa [10]\n" +
" ')' ( 41) |11111110|11 3fb [10]\n" +
" '*' ( 42) |11111001 f9 [ 8]\n" +
" '+' ( 43) |11111111|011 7fb [11]\n" +
" ',' ( 44) |11111010 fa [ 8]\n" +
" '-' ( 45) |010110 16 [ 6]\n" +
" '.' ( 46) |010111 17 [ 6]\n" +
" '/' ( 47) |011000 18 [ 6]\n" +
" '0' ( 48) |00000 0 [ 5]\n" +
" '1' ( 49) |00001 1 [ 5]\n" +
" '2' ( 50) |00010 2 [ 5]\n" +
" '3' ( 51) |011001 19 [ 6]\n" +
" '4' ( 52) |011010 1a [ 6]\n" +
" '5' ( 53) |011011 1b [ 6]\n" +
" '6' ( 54) |011100 1c [ 6]\n" +
" '7' ( 55) |011101 1d [ 6]\n" +
" '8' ( 56) |011110 1e [ 6]\n" +
" '9' ( 57) |011111 1f [ 6]\n" +
" ':' ( 58) |1011100 5c [ 7]\n" +
" ';' ( 59) |11111011 fb [ 8]\n" +
" '<' ( 60) |11111111|1111100 7ffc [15]\n" +
" '=' ( 61) |100000 20 [ 6]\n" +
" '>' ( 62) |11111111|1011 ffb [12]\n" +
" '?' ( 63) |11111111|00 3fc [10]\n" +
" '@' ( 64) |11111111|11010 1ffa [13]\n" +
" 'A' ( 65) |100001 21 [ 6]\n" +
" 'B' ( 66) |1011101 5d [ 7]\n" +
" 'C' ( 67) |1011110 5e [ 7]\n" +
" 'D' ( 68) |1011111 5f [ 7]\n" +
" 'E' ( 69) |1100000 60 [ 7]\n" +
" 'F' ( 70) |1100001 61 [ 7]\n" +
" 'G' ( 71) |1100010 62 [ 7]\n" +
" 'H' ( 72) |1100011 63 [ 7]\n" +
" 'I' ( 73) |1100100 64 [ 7]\n" +
" 'J' ( 74) |1100101 65 [ 7]\n" +
" 'K' ( 75) |1100110 66 [ 7]\n" +
" 'L' ( 76) |1100111 67 [ 7]\n" +
" 'M' ( 77) |1101000 68 [ 7]\n" +
" 'N' ( 78) |1101001 69 [ 7]\n" +
" 'O' ( 79) |1101010 6a [ 7]\n" +
" 'P' ( 80) |1101011 6b [ 7]\n" +
" 'Q' ( 81) |1101100 6c [ 7]\n" +
" 'R' ( 82) |1101101 6d [ 7]\n" +
" 'S' ( 83) |1101110 6e [ 7]\n" +
" 'T' ( 84) |1101111 6f [ 7]\n" +
" 'U' ( 85) |1110000 70 [ 7]\n" +
" 'V' ( 86) |1110001 71 [ 7]\n" +
" 'W' ( 87) |1110010 72 [ 7]\n" +
" 'X' ( 88) |11111100 fc [ 8]\n" +
" 'Y' ( 89) |1110011 73 [ 7]\n" +
" 'Z' ( 90) |11111101 fd [ 8]\n" +
" '[' ( 91) |11111111|11011 1ffb [13]\n" +
" '\\' ( 92) |11111111|11111110|000 7fff0 [19]\n" +
" ']' ( 93) |11111111|11100 1ffc [13]\n" +
" '^' ( 94) |11111111|111100 3ffc [14]\n" +
" '_' ( 95) |100010 22 [ 6]\n" +
" '`' ( 96) |11111111|1111101 7ffd [15]\n" +
" 'a' ( 97) |00011 3 [ 5]\n" +
" 'b' ( 98) |100011 23 [ 6]\n" +
" 'c' ( 99) |00100 4 [ 5]\n" +
" 'd' (100) |100100 24 [ 6]\n" +
" 'e' (101) |00101 5 [ 5]\n" +
" 'f' (102) |100101 25 [ 6]\n" +
" 'g' (103) |100110 26 [ 6]\n" +
" 'h' (104) |100111 27 [ 6]\n" +
" 'i' (105) |00110 6 [ 5]\n" +
" 'j' (106) |1110100 74 [ 7]\n" +
" 'k' (107) |1110101 75 [ 7]\n" +
" 'l' (108) |101000 28 [ 6]\n" +
" 'm' (109) |101001 29 [ 6]\n" +
" 'n' (110) |101010 2a [ 6]\n" +
" 'o' (111) |00111 7 [ 5]\n" +
" 'p' (112) |101011 2b [ 6]\n" +
" 'q' (113) |1110110 76 [ 7]\n" +
" 'r' (114) |101100 2c [ 6]\n" +
" 's' (115) |01000 8 [ 5]\n" +
" 't' (116) |01001 9 [ 5]\n" +
" 'u' (117) |101101 2d [ 6]\n" +
" 'v' (118) |1110111 77 [ 7]\n" +
" 'w' (119) |1111000 78 [ 7]\n" +
" 'x' (120) |1111001 79 [ 7]\n" +
" 'y' (121) |1111010 7a [ 7]\n" +
" 'z' (122) |1111011 7b [ 7]\n" +
" '{' (123) |11111111|1111110 7ffe [15]\n" +
" '|' (124) |11111111|100 7fc [11]\n" +
" '}' (125) |11111111|111101 3ffd [14]\n" +
" '~' (126) |11111111|11101 1ffd [13]\n" +
" (127) |11111111|11111111|11111111|1100 ffffffc [28]\n" +
" (128) |11111111|11111110|0110 fffe6 [20]\n" +
" (129) |11111111|11111111|010010 3fffd2 [22]\n" +
" (130) |11111111|11111110|0111 fffe7 [20]\n" +
" (131) |11111111|11111110|1000 fffe8 [20]\n" +
" (132) |11111111|11111111|010011 3fffd3 [22]\n" +
" (133) |11111111|11111111|010100 3fffd4 [22]\n" +
" (134) |11111111|11111111|010101 3fffd5 [22]\n" +
" (135) |11111111|11111111|1011001 7fffd9 [23]\n" +
" (136) |11111111|11111111|010110 3fffd6 [22]\n" +
" (137) |11111111|11111111|1011010 7fffda [23]\n" +
" (138) |11111111|11111111|1011011 7fffdb [23]\n" +
" (139) |11111111|11111111|1011100 7fffdc [23]\n" +
" (140) |11111111|11111111|1011101 7fffdd [23]\n" +
" (141) |11111111|11111111|1011110 7fffde [23]\n" +
" (142) |11111111|11111111|11101011 ffffeb [24]\n" +
" (143) |11111111|11111111|1011111 7fffdf [23]\n" +
" (144) |11111111|11111111|11101100 ffffec [24]\n" +
" (145) |11111111|11111111|11101101 ffffed [24]\n" +
" (146) |11111111|11111111|010111 3fffd7 [22]\n" +
" (147) |11111111|11111111|1100000 7fffe0 [23]\n" +
" (148) |11111111|11111111|11101110 ffffee [24]\n" +
" (149) |11111111|11111111|1100001 7fffe1 [23]\n" +
" (150) |11111111|11111111|1100010 7fffe2 [23]\n" +
" (151) |11111111|11111111|1100011 7fffe3 [23]\n" +
" (152) |11111111|11111111|1100100 7fffe4 [23]\n" +
" (153) |11111111|11111110|11100 1fffdc [21]\n" +
" (154) |11111111|11111111|011000 3fffd8 [22]\n" +
" (155) |11111111|11111111|1100101 7fffe5 [23]\n" +
" (156) |11111111|11111111|011001 3fffd9 [22]\n" +
" (157) |11111111|11111111|1100110 7fffe6 [23]\n" +
" (158) |11111111|11111111|1100111 7fffe7 [23]\n" +
" (159) |11111111|11111111|11101111 ffffef [24]\n" +
" (160) |11111111|11111111|011010 3fffda [22]\n" +
" (161) |11111111|11111110|11101 1fffdd [21]\n" +
" (162) |11111111|11111110|1001 fffe9 [20]\n" +
" (163) |11111111|11111111|011011 3fffdb [22]\n" +
" (164) |11111111|11111111|011100 3fffdc [22]\n" +
" (165) |11111111|11111111|1101000 7fffe8 [23]\n" +
" (166) |11111111|11111111|1101001 7fffe9 [23]\n" +
" (167) |11111111|11111110|11110 1fffde [21]\n" +
" (168) |11111111|11111111|1101010 7fffea [23]\n" +
" (169) |11111111|11111111|011101 3fffdd [22]\n" +
" (170) |11111111|11111111|011110 3fffde [22]\n" +
" (171) |11111111|11111111|11110000 fffff0 [24]\n" +
" (172) |11111111|11111110|11111 1fffdf [21]\n" +
" (173) |11111111|11111111|011111 3fffdf [22]\n" +
" (174) |11111111|11111111|1101011 7fffeb [23]\n" +
" (175) |11111111|11111111|1101100 7fffec [23]\n" +
" (176) |11111111|11111111|00000 1fffe0 [21]\n" +
" (177) |11111111|11111111|00001 1fffe1 [21]\n" +
" (178) |11111111|11111111|100000 3fffe0 [22]\n" +
" (179) |11111111|11111111|00010 1fffe2 [21]\n" +
" (180) |11111111|11111111|1101101 7fffed [23]\n" +
" (181) |11111111|11111111|100001 3fffe1 [22]\n" +
" (182) |11111111|11111111|1101110 7fffee [23]\n" +
" (183) |11111111|11111111|1101111 7fffef [23]\n" +
" (184) |11111111|11111110|1010 fffea [20]\n" +
" (185) |11111111|11111111|100010 3fffe2 [22]\n" +
" (186) |11111111|11111111|100011 3fffe3 [22]\n" +
" (187) |11111111|11111111|100100 3fffe4 [22]\n" +
" (188) |11111111|11111111|1110000 7ffff0 [23]\n" +
" (189) |11111111|11111111|100101 3fffe5 [22]\n" +
" (190) |11111111|11111111|100110 3fffe6 [22]\n" +
" (191) |11111111|11111111|1110001 7ffff1 [23]\n" +
" (192) |11111111|11111111|11111000|00 3ffffe0 [26]\n" +
" (193) |11111111|11111111|11111000|01 3ffffe1 [26]\n" +
" (194) |11111111|11111110|1011 fffeb [20]\n" +
" (195) |11111111|11111110|001 7fff1 [19]\n" +
" (196) |11111111|11111111|100111 3fffe7 [22]\n" +
" (197) |11111111|11111111|1110010 7ffff2 [23]\n" +
" (198) |11111111|11111111|101000 3fffe8 [22]\n" +
" (199) |11111111|11111111|11110110|0 1ffffec [25]\n" +
" (200) |11111111|11111111|11111000|10 3ffffe2 [26]\n" +
" (201) |11111111|11111111|11111000|11 3ffffe3 [26]\n" +
" (202) |11111111|11111111|11111001|00 3ffffe4 [26]\n" +
" (203) |11111111|11111111|11111011|110 7ffffde [27]\n" +
" (204) |11111111|11111111|11111011|111 7ffffdf [27]\n" +
" (205) |11111111|11111111|11111001|01 3ffffe5 [26]\n" +
" (206) |11111111|11111111|11110001 fffff1 [24]\n" +
" (207) |11111111|11111111|11110110|1 1ffffed [25]\n" +
" (208) |11111111|11111110|010 7fff2 [19]\n" +
" (209) |11111111|11111111|00011 1fffe3 [21]\n" +
" (210) |11111111|11111111|11111001|10 3ffffe6 [26]\n" +
" (211) |11111111|11111111|11111100|000 7ffffe0 [27]\n" +
" (212) |11111111|11111111|11111100|001 7ffffe1 [27]\n" +
" (213) |11111111|11111111|11111001|11 3ffffe7 [26]\n" +
" (214) |11111111|11111111|11111100|010 7ffffe2 [27]\n" +
" (215) |11111111|11111111|11110010 fffff2 [24]\n" +
" (216) |11111111|11111111|00100 1fffe4 [21]\n" +
" (217) |11111111|11111111|00101 1fffe5 [21]\n" +
" (218) |11111111|11111111|11111010|00 3ffffe8 [26]\n" +
" (219) |11111111|11111111|11111010|01 3ffffe9 [26]\n" +
" (220) |11111111|11111111|11111111|1101 ffffffd [28]\n" +
" (221) |11111111|11111111|11111100|011 7ffffe3 [27]\n" +
" (222) |11111111|11111111|11111100|100 7ffffe4 [27]\n" +
" (223) |11111111|11111111|11111100|101 7ffffe5 [27]\n" +
" (224) |11111111|11111110|1100 fffec [20]\n" +
" (225) |11111111|11111111|11110011 fffff3 [24]\n" +
" (226) |11111111|11111110|1101 fffed [20]\n" +
" (227) |11111111|11111111|00110 1fffe6 [21]\n" +
" (228) |11111111|11111111|101001 3fffe9 [22]\n" +
" (229) |11111111|11111111|00111 1fffe7 [21]\n" +
" (230) |11111111|11111111|01000 1fffe8 [21]\n" +
" (231) |11111111|11111111|1110011 7ffff3 [23]\n" +
" (232) |11111111|11111111|101010 3fffea [22]\n" +
" (233) |11111111|11111111|101011 3fffeb [22]\n" +
" (234) |11111111|11111111|11110111|0 1ffffee [25]\n" +
" (235) |11111111|11111111|11110111|1 1ffffef [25]\n" +
" (236) |11111111|11111111|11110100 fffff4 [24]\n" +
" (237) |11111111|11111111|11110101 fffff5 [24]\n" +
" (238) |11111111|11111111|11111010|10 3ffffea [26]\n" +
" (239) |11111111|11111111|1110100 7ffff4 [23]\n" +
" (240) |11111111|11111111|11111010|11 3ffffeb [26]\n" +
" (241) |11111111|11111111|11111100|110 7ffffe6 [27]\n" +
" (242) |11111111|11111111|11111011|00 3ffffec [26]\n" +
" (243) |11111111|11111111|11111011|01 3ffffed [26]\n" +
" (244) |11111111|11111111|11111100|111 7ffffe7 [27]\n" +
" (245) |11111111|11111111|11111101|000 7ffffe8 [27]\n" +
" (246) |11111111|11111111|11111101|001 7ffffe9 [27]\n" +
" (247) |11111111|11111111|11111101|010 7ffffea [27]\n" +
" (248) |11111111|11111111|11111101|011 7ffffeb [27]\n" +
" (249) |11111111|11111111|11111111|1110 ffffffe [28]\n" +
" (250) |11111111|11111111|11111101|100 7ffffec [27]\n" +
" (251) |11111111|11111111|11111101|101 7ffffed [27]\n" +
" (252) |11111111|11111111|11111101|110 7ffffee [27]\n" +
" (253) |11111111|11111111|11111101|111 7ffffef [27]\n" +
" (254) |11111111|11111111|11111110|000 7fffff0 [27]\n" +
" (255) |11111111|11111111|11111011|10 3ffffee [26]\n" +
" EOS (256) |11111111|11111111|11111111|111111 3fffffff [30]";
// @formatter:on
private static final Code EOS = new Code((char) 256, 0x3fffffff, 30);
private final SortedMap<Character, Code> CODES = readSpecification();
private static final class Code {
final char sym;
final int hex;
final int len;
public Code(char sym, int hex, int len) {
this.sym = sym;
this.hex = hex;
this.len = len;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Code code = (Code) o;
return sym == code.sym &&
hex == code.hex &&
len == code.len;
}
@Override
public int hashCode() {
return Objects.hash(sym, hex, len);
}
}
private SortedMap<Character, Code> readSpecification() {
Pattern line = Pattern.compile(
"\\(\\s*(?<sym>\\d+)\\s*\\)\\s*(?<bits>(\\|([01])+)+)\\s*" +
"(?<hex>[0-9a-zA-Z]+)\\s*\\[\\s*(?<len>\\d+)\\s*\\]");
Matcher m = line.matcher(SPECIFICATION);
SortedMap<Character, Code> map = new TreeMap<>();
while (m.find()) {
String symString = m.group("sym");
String binaryString = m.group("bits").replaceAll("\\|", "");
String hexString = m.group("hex");
String lenString = m.group("len");
// several sanity checks for the data read from the table, just to
// make sure what we read makes sense:
int sym = Integer.parseInt(symString);
if (sym < 0 || sym > 65535) {
throw new IllegalArgumentException();
}
int binary = Integer.parseInt(binaryString, 2);
int len = Integer.parseInt(lenString);
if (binaryString.length() != len) {
throw new IllegalArgumentException();
}
int hex = Integer.parseInt(hexString, 16);
if (hex != binary) {
throw new IllegalArgumentException();
}
if (map.put((char) sym, new Code((char) sym, hex, len)) != null) {
// a mapping for sym already exists
throw new IllegalStateException();
}
}
if (map.size() != 257) {
throw new IllegalArgumentException();
}
return map;
}
/*
* Encodes manually each symbol (character) from the specification and
* checks that Huffman.Reader decodes the result back to the initial
* character. This verifies that Huffman.Reader is implemented according to
* RFC 7541.
*/
@Test
public void decodingConsistentWithSpecification() throws IOException {
Reader reader = READER.get();
for (Code code : CODES.values()) {
if (code.equals(EOS)) {
continue; // skip EOS
}
ByteBuffer input = encode(code);
StringBuilder output = new StringBuilder(1);
reader.read(input, output, true);
reader.reset();
// compare chars using their decimal representation (as some chars
// might not be printable/visible)
int expected = code.sym;
int actual = (int) output.charAt(0);
assertEquals(1, output.length()); // exactly 1 character
assertEquals(expected, actual);
}
}
@Test
public void decodeEOS1() {
Reader reader = READER.get();
TestHelper.assertVoidThrows(
IOException.class,
() -> reader.read(encode(EOS), new StringBuilder(), true));
}
@Test
public void decodeEOS2() {
Reader reader = READER.get();
TestHelper.assertVoidThrows(
IOException.class,
() -> reader.read(encode(EOS), new StringBuilder(), false));
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.4.1
//
@Test
public void read01() {
readExhaustively("f1e3 c2e5 f23a 6ba0 ab90 f4ff", "www.example.com");
}
@Test
public void write01() {
writeExhaustively("www.example.com", "f1e3 c2e5 f23a 6ba0 ab90 f4ff");
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.4.2
//
@Test
public void read02() {
readExhaustively("a8eb 1064 9cbf", "no-cache");
}
@Test
public void write02() {
writeExhaustively("no-cache", "a8eb 1064 9cbf");
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.4.3
//
@Test
public void read03() {
readExhaustively("25a8 49e9 5ba9 7d7f", "custom-key");
}
@Test
public void write03() {
writeExhaustively("custom-key", "25a8 49e9 5ba9 7d7f");
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.4.3
//
@Test
public void read04() {
readExhaustively("25a8 49e9 5bb8 e8b4 bf", "custom-value");
}
@Test
public void write04() {
writeExhaustively("custom-value", "25a8 49e9 5bb8 e8b4 bf");
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.6.1
//
@Test
public void read05() {
readExhaustively("6402", "302");
}
@Test
public void write05() {
writeExhaustively("302", "6402");
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.6.1
//
@Test
public void read06() {
readExhaustively("aec3 771a 4b", "private");
}
@Test
public void write06() {
writeExhaustively("private", "aec3 771a 4b");
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.6.1
//
@Test
public void read07() {
readExhaustively(
"d07a be94 1054 d444 a820 0595 040b 8166 e082 a62d 1bff",
"Mon, 21 Oct 2013 20:13:21 GMT");
}
@Test
public void write07() {
writeExhaustively(
"Mon, 21 Oct 2013 20:13:21 GMT",
"d07a be94 1054 d444 a820 0595 040b 8166 e082 a62d 1bff");
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.6.1
//
@Test
public void read08() {
readExhaustively("9d29 ad17 1863 c78f 0b97 c8e9 ae82 ae43 d3",
"https://www.example.com");
}
@Test
public void write08() {
writeExhaustively("https://www.example.com",
"9d29 ad17 1863 c78f 0b97 c8e9 ae82 ae43 d3");
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.6.2
//
@Test
public void read09() {
readExhaustively("640e ff", "307");
}
@Test
public void write09() {
writeExhaustively("307", "640e ff");
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.6.3
//
@Test
public void read10() {
readExhaustively(
"d07a be94 1054 d444 a820 0595 040b 8166 e084 a62d 1bff",
"Mon, 21 Oct 2013 20:13:22 GMT");
}
@Test
public void write10() {
writeExhaustively(
"Mon, 21 Oct 2013 20:13:22 GMT",
"d07a be94 1054 d444 a820 0595 040b 8166 e084 a62d 1bff");
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.6.3
//
@Test
public void read11() {
readExhaustively("9bd9 ab", "gzip");
}
@Test
public void write11() {
writeExhaustively("gzip", "9bd9 ab");
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.6.3
//
@Test
public void read12() {
// The number of possibilities here grow as 2^(n-1). There are 45 bytes
// in this input. So it would require 2^44 decoding operations. If we
// spend 1 microsecond per operation, it would take approximately
//
// ((10^15 * 10^(-6)) / 86400) / 365, or about 32 years
//
// Conclusion: too big to be read exhaustively
read("94e7 821d d7f2 e6c7 b335 dfdf cd5b 3960 "
+ "d5af 2708 7f36 72c1 ab27 0fb5 291f 9587 "
+ "3160 65c0 03ed 4ee5 b106 3d50 07",
"foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1");
}
@Test
public void write12() {
write("foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1",
"94e7 821d d7f2 e6c7 b335 dfdf cd5b 3960 "
+ "d5af 2708 7f36 72c1 ab27 0fb5 291f 9587 "
+ "3160 65c0 03ed 4ee5 b106 3d50 07");
}
@Test
public void read13() {
readExhaustively("6274 a6b4 0989 4de4 b27f 80",
"/https2/fixed?0");
}
@Test
public void roundTrip() throws IOException {
class Helper {
// Maps code's length to a character that is encoded with a code of
// that length. Which of the characters with the same code's length
// is picked is undefined.
private Map<Integer, Character> chars = new HashMap<>();
{
for (Map.Entry<Character, Code> e : CODES.entrySet()) {
chars.putIfAbsent(e.getValue().len, e.getKey());
}
}
private CharSequence charsOfLength(int... lengths) {
StringBuilder b = new StringBuilder(lengths.length);
for (int length : lengths) {
Character c = chars.get(length);
if (c == null) {
throw new IllegalArgumentException(
"No code has length " + length);
}
b.append(c);
}
return b.toString();
}
private void identity(CharSequence str) throws IOException {
Writer w = WRITER.get();
StringBuilder b = new StringBuilder(str.length());
int size = w.lengthOf(str);
ByteBuffer buffer = ByteBuffer.allocate(size);
w.from(str, 0, str.length()).write(buffer);
Reader r = READER.get();
r.read(buffer.flip(), b, true);
assertEquals(str, b.toString());
}
private void roundTrip(int... lengths) throws IOException {
identity(charsOfLength(lengths));
}
}
// The idea is to build a number of input strings that are encoded
// without the need for padding. The sizes of the encoded forms,
// therefore, must be 8, 16, 24, 32, 48, 56, 64 and 72 bits. Then check
// that they are encoded and then decoded into the same strings.
Helper h = new Helper();
// -- 8 bit code --
h.roundTrip( 8);
// -- 16 bit code --
h.roundTrip( 5, 11);
h.roundTrip( 5, 5, 6);
// -- 24 bit code --
h.roundTrip(24);
h.roundTrip( 5, 19);
h.roundTrip( 5, 5, 14);
h.roundTrip( 5, 5, 6, 8);
// -- 32 bit code --
h.roundTrip( 5, 27);
h.roundTrip( 5, 5, 22);
h.roundTrip( 5, 5, 7, 15);
h.roundTrip( 5, 5, 5, 5, 12);
h.roundTrip( 5, 5, 5, 5, 5, 7);
// -- 48 bit code --
h.roundTrip(20, 28);
h.roundTrip( 5, 13, 30);
h.roundTrip( 5, 5, 8, 30);
h.roundTrip( 5, 5, 5, 5, 28);
h.roundTrip( 5, 5, 5, 5, 5, 23);
h.roundTrip( 5, 5, 5, 5, 5, 8, 15);
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 13);
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 8);
// -- 56 bit code --
h.roundTrip(26, 30);
h.roundTrip( 5, 21, 30);
h.roundTrip( 5, 5, 19, 27);
h.roundTrip( 5, 5, 5, 11, 30);
h.roundTrip( 5, 5, 5, 5, 6, 30);
h.roundTrip( 5, 5, 5, 5, 5, 5, 26);
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 21);
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 6, 15);
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 11);
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6);
// -- 64 bit code --
h.roundTrip( 6, 28, 30);
h.roundTrip( 5, 5, 24, 30);
h.roundTrip( 5, 5, 5, 19, 30);
h.roundTrip( 5, 5, 5, 5, 14, 30);
h.roundTrip( 5, 5, 5, 5, 5, 11, 28);
h.roundTrip( 5, 5, 5, 5, 5, 5, 6, 28);
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 24);
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 19);
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 14);
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 8);
// -- 72 bit code --
h.roundTrip(12, 30, 30);
h.roundTrip( 5, 7, 30, 30);
h.roundTrip( 5, 5, 5, 27, 30);
h.roundTrip( 5, 5, 5, 5, 22, 30);
h.roundTrip( 5, 5, 5, 5, 5, 19, 28);
h.roundTrip( 5, 5, 5, 5, 5, 5, 12, 30);
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 7, 30);
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 27);
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 22);
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 7, 15);
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 12);
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 7);
}
@Test
public void cannotEncodeOutsideByte() {
TestHelper.Block<Object> coding =
() -> WRITER.get()
.from(String.valueOf((char) 256), 0, 1)
.write(ByteBuffer.allocate(1));
RuntimeException e =
TestHelper.assertVoidThrows(RuntimeException.class, coding);
TestHelper.assertExceptionMessageContains(e, "char");
}
private static void read(String hexdump, String decoded) {
ByteBuffer source = SpecHelper.toBytes(hexdump);
Appendable actual = new StringBuilder();
Reader reader = READER.get();
try {
reader.read(source, actual, true);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
assertEquals(decoded, actual.toString());
}
private static void readExhaustively(String hexdump, String decoded) {
ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0);
Reader reader = READER.get();
ByteBuffer source = SpecHelper.toBytes(hexdump);
StringBuilder actual = new StringBuilder();
BuffersTestingKit.forEachSplit(source, buffers -> {
try {
for (ByteBuffer b : buffers) {
reader.read(b, actual, false);
}
reader.read(EMPTY_BUFFER, actual, true);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
assertEquals(decoded, actual.toString());
reader.reset();
actual.setLength(0);
});
}
private static void write(String decoded, String hexdump) {
Writer writer = WRITER.get();
int n = writer.lengthOf(decoded);
ByteBuffer destination = ByteBuffer.allocateDirect(n);
writer.from(decoded, 0, decoded.length());
boolean written = writer.write(destination);
assertTrue(written);
String actual = SpecHelper.toHexdump(destination.flip());
assertEquals(hexdump, actual);
writer.reset();
}
private static void writeExhaustively(String decoded, String hexdump) {
Writer writer = WRITER.get();
int n = writer.lengthOf(decoded);
ByteBuffer destination = ByteBuffer.allocate(n);
BuffersTestingKit.forEachSplit(destination, byteBuffers -> {
writer.from(decoded, 0, decoded.length());
boolean written = false;
for (ByteBuffer b : byteBuffers) {
int pos = b.position();
written = writer.write(b);
b.position(pos); // "flip" to the saved position, for reading
}
assertTrue(written);
ByteBuffer concated = BuffersTestingKit.concat(byteBuffers);
String actual = SpecHelper.toHexdump(concated);
assertEquals(hexdump, actual);
writer.reset();
});
}
/*
* Encodes a single character. This representation is padded, thus ready to
* be decoded.
*/
private static ByteBuffer encode(Code code) {
int EOS_MSB = EOS.hex << (32 - EOS.len);
int padding = EOS_MSB >>> code.len;
int hexMSB = code.hex << (32 - code.len);
int c = hexMSB | padding;
int n = bytesForBits(code.len);
byte[] result = new byte[n];
for (int i = 0; i < n; i++) {
result[i] = (byte) (c >> (32 - 8 * (i + 1)));
}
return ByteBuffer.wrap(result);
}
}

View file

@ -0,0 +1,332 @@
/*
* Copyright (c) 2014, 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.
*/
package jdk.internal.net.http.hpack;
import jdk.internal.net.http.hpack.SimpleHeaderTable.HeaderField;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static jdk.internal.net.http.hpack.TestHelper.assertExceptionMessageContains;
import static jdk.internal.net.http.hpack.TestHelper.assertThrows;
import static jdk.internal.net.http.hpack.TestHelper.assertVoidThrows;
import static jdk.internal.net.http.hpack.TestHelper.newRandom;
import static java.lang.String.format;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
public class SimpleHeaderTableTest {
//
// https://tools.ietf.org/html/rfc7541#appendix-A
//
// @formatter:off
private static final String SPECIFICATION =
" | 1 | :authority | |\n" +
" | 2 | :method | GET |\n" +
" | 3 | :method | POST |\n" +
" | 4 | :path | / |\n" +
" | 5 | :path | /index.html |\n" +
" | 6 | :scheme | http |\n" +
" | 7 | :scheme | https |\n" +
" | 8 | :status | 200 |\n" +
" | 9 | :status | 204 |\n" +
" | 10 | :status | 206 |\n" +
" | 11 | :status | 304 |\n" +
" | 12 | :status | 400 |\n" +
" | 13 | :status | 404 |\n" +
" | 14 | :status | 500 |\n" +
" | 15 | accept-charset | |\n" +
" | 16 | accept-encoding | gzip, deflate |\n" +
" | 17 | accept-language | |\n" +
" | 18 | accept-ranges | |\n" +
" | 19 | accept | |\n" +
" | 20 | access-control-allow-origin | |\n" +
" | 21 | age | |\n" +
" | 22 | allow | |\n" +
" | 23 | authorization | |\n" +
" | 24 | cache-control | |\n" +
" | 25 | content-disposition | |\n" +
" | 26 | content-encoding | |\n" +
" | 27 | content-language | |\n" +
" | 28 | content-length | |\n" +
" | 29 | content-location | |\n" +
" | 30 | content-range | |\n" +
" | 31 | content-type | |\n" +
" | 32 | cookie | |\n" +
" | 33 | date | |\n" +
" | 34 | etag | |\n" +
" | 35 | expect | |\n" +
" | 36 | expires | |\n" +
" | 37 | from | |\n" +
" | 38 | host | |\n" +
" | 39 | if-match | |\n" +
" | 40 | if-modified-since | |\n" +
" | 41 | if-none-match | |\n" +
" | 42 | if-range | |\n" +
" | 43 | if-unmodified-since | |\n" +
" | 44 | last-modified | |\n" +
" | 45 | link | |\n" +
" | 46 | location | |\n" +
" | 47 | max-forwards | |\n" +
" | 48 | proxy-authenticate | |\n" +
" | 49 | proxy-authorization | |\n" +
" | 50 | range | |\n" +
" | 51 | referer | |\n" +
" | 52 | refresh | |\n" +
" | 53 | retry-after | |\n" +
" | 54 | server | |\n" +
" | 55 | set-cookie | |\n" +
" | 56 | strict-transport-security | |\n" +
" | 57 | transfer-encoding | |\n" +
" | 58 | user-agent | |\n" +
" | 59 | vary | |\n" +
" | 60 | via | |\n" +
" | 61 | www-authenticate | |\n";
// @formatter:on
static final int STATIC_TABLE_LENGTH = createStaticEntries().size();
final Random rnd = newRandom();
/** Creates a header table under test. Override in subclass. */
protected SimpleHeaderTable createHeaderTable(int maxSize) {
return new SimpleHeaderTable(maxSize, HPACK.getLogger());
}
@Test
public void staticData0() {
SimpleHeaderTable table = createHeaderTable(0);
Map<Integer, HeaderField> staticHeaderFields = createStaticEntries();
staticHeaderFields.forEach((index, expectedHeaderField) -> {
SimpleHeaderTable.HeaderField actualHeaderField = table.get(index);
assertEquals(expectedHeaderField.name, actualHeaderField.name);
assertEquals(expectedHeaderField.value, actualHeaderField.value);
});
}
@Test
public void constructorSetsMaxSize() {
int size = rnd.nextInt(64);
SimpleHeaderTable table = createHeaderTable(size);
assertEquals(0, table.size());
assertEquals(size, table.maxSize());
}
@Test
public void negativeMaximumSize() {
int maxSize = -(rnd.nextInt(100) + 1); // [-100, -1]
SimpleHeaderTable table = createHeaderTable(0);
IllegalArgumentException e =
assertVoidThrows(IllegalArgumentException.class,
() -> table.setMaxSize(maxSize));
assertExceptionMessageContains(e, "maxSize");
}
@Test
public void zeroMaximumSize() {
SimpleHeaderTable table = createHeaderTable(0);
table.setMaxSize(0);
assertEquals(0, table.maxSize());
}
@Test
public void negativeIndex() {
int idx = -(rnd.nextInt(256) + 1); // [-256, -1]
SimpleHeaderTable table = createHeaderTable(0);
IndexOutOfBoundsException e =
assertVoidThrows(IndexOutOfBoundsException.class,
() -> table.get(idx));
assertExceptionMessageContains(e, "index");
}
@Test
public void zeroIndex() {
SimpleHeaderTable table = createHeaderTable(0);
IndexOutOfBoundsException e =
assertThrows(IndexOutOfBoundsException.class,
() -> table.get(0));
assertExceptionMessageContains(e, "index");
}
@Test
public void length() {
SimpleHeaderTable table = createHeaderTable(0);
assertEquals(STATIC_TABLE_LENGTH, table.length());
}
@Test
public void indexOutsideStaticRange() {
SimpleHeaderTable table = createHeaderTable(0);
int idx = table.length() + (rnd.nextInt(256) + 1);
IndexOutOfBoundsException e =
assertThrows(IndexOutOfBoundsException.class,
() -> table.get(idx));
assertExceptionMessageContains(e, "index");
}
@Test
public void entryPutAfterStaticArea() {
SimpleHeaderTable table = createHeaderTable(256);
int idx = table.length() + 1;
assertThrows(IndexOutOfBoundsException.class, () -> table.get(idx));
byte[] bytes = new byte[32];
rnd.nextBytes(bytes);
String name = new String(bytes, StandardCharsets.ISO_8859_1);
String value = "custom-value";
table.put(name, value);
SimpleHeaderTable.HeaderField f = table.get(idx);
assertEquals(name, f.name);
assertEquals(value, f.value);
}
@Test
public void staticTableHasZeroSize() {
SimpleHeaderTable table = createHeaderTable(0);
assertEquals(0, table.size());
}
// TODO: negative indexes check
// TODO: ensure full table clearance when adding huge header field
// TODO: ensure eviction deletes minimum needed entries, not more
@Test
public void fifo() {
// Let's add a series of header fields
int NUM_HEADERS = 32;
SimpleHeaderTable table =
createHeaderTable((32 + 4) * NUM_HEADERS);
// ^ ^
// entry overhead symbols per entry (max 2x2 digits)
for (int i = 1; i <= NUM_HEADERS; i++) {
String s = String.valueOf(i);
table.put(s, s);
}
// They MUST appear in a FIFO order:
// newer entries are at lower indexes
// older entries are at higher indexes
for (int j = 1; j <= NUM_HEADERS; j++) {
SimpleHeaderTable.HeaderField f = table.get(STATIC_TABLE_LENGTH + j);
int actualName = Integer.parseInt(f.name);
int expectedName = NUM_HEADERS - j + 1;
assertEquals(expectedName, actualName);
}
// Entries MUST be evicted in the order they were added:
// the newer the entry the later it is evicted
for (int k = 1; k <= NUM_HEADERS; k++) {
SimpleHeaderTable.HeaderField f = table.evictEntry();
assertEquals(String.valueOf(k), f.name);
}
}
@Test
public void testToString() {
testToString0();
}
@Test
public void testToStringDifferentLocale() {
Locale locale = Locale.getDefault();
Locale.setDefault(Locale.FRENCH);
try {
String s = format("%.1f", 3.1);
assertEquals("3,1", s); // assumption of the test, otherwise the test is useless
testToString0();
} finally {
Locale.setDefault(locale);
}
}
private void testToString0() {
SimpleHeaderTable table = createHeaderTable(0);
{
int maxSize = 2048;
table.setMaxSize(maxSize);
String expected = format(
"dynamic length: %s, full length: %s, used space: %s/%s (%.1f%%)",
0, STATIC_TABLE_LENGTH, 0, maxSize, 0.0);
assertEquals(expected, table.toString());
}
{
String name = "custom-name";
String value = "custom-value";
int size = 512;
table.setMaxSize(size);
table.put(name, value);
String s = table.toString();
int used = name.length() + value.length() + 32;
double ratio = used * 100.0 / size;
String expected = format(
"dynamic length: %s, full length: %s, used space: %s/%s (%.1f%%)",
1, STATIC_TABLE_LENGTH + 1, used, size, ratio);
assertEquals(expected, s);
}
{
table.setMaxSize(78);
table.put(":method", "");
table.put(":status", "");
String s = table.toString();
String expected =
format("dynamic length: %s, full length: %s, used space: %s/%s (%.1f%%)",
2, STATIC_TABLE_LENGTH + 2, 78, 78, 100.0);
assertEquals(expected, s);
}
}
@Test
public void stateString() {
SimpleHeaderTable table = createHeaderTable(256);
table.put("custom-key", "custom-header");
// @formatter:off
assertEquals("[ 1] (s = 55) custom-key: custom-header\n" +
" Table size: 55", table.getStateString());
// @formatter:on
}
static Map<Integer, HeaderField> createStaticEntries() {
Pattern line = Pattern.compile(
"\\|\\s*(?<index>\\d+?)\\s*\\|\\s*(?<name>.+?)\\s*\\|\\s*(?<value>.*?)\\s*\\|");
Matcher m = line.matcher(SPECIFICATION);
Map<Integer, HeaderField> result = new HashMap<>();
while (m.find()) {
int index = Integer.parseInt(m.group("index"));
String name = m.group("name");
String value = m.group("value");
HeaderField f = new HeaderField(name, value);
result.put(index, f);
}
return Collections.unmodifiableMap(result); // lol
}
}

View file

@ -0,0 +1,70 @@
/*
* Copyright (c) 2014, 2018, 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.
*/
package jdk.internal.net.http.hpack;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
//
// THIS IS NOT A TEST
//
public final class SpecHelper {
private SpecHelper() {
throw new AssertionError();
}
public static ByteBuffer toBytes(String hexdump) {
Pattern hexByte = Pattern.compile("[0-9a-fA-F]{2}");
List<String> bytes = new ArrayList<>();
Matcher matcher = hexByte.matcher(hexdump);
while (matcher.find()) {
bytes.add(matcher.group(0));
}
ByteBuffer result = ByteBuffer.allocate(bytes.size());
for (String f : bytes) {
result.put((byte) Integer.parseInt(f, 16));
}
result.flip();
return result;
}
public static String toHexdump(ByteBuffer bb) {
List<String> words = new ArrayList<>();
int i = 0;
while (bb.hasRemaining()) {
if (i % 2 == 0) {
words.add("");
}
byte b = bb.get();
String hex = Integer.toHexString(256 + Byte.toUnsignedInt(b)).substring(1);
words.set(i / 2, words.get(i / 2) + hex);
i++;
}
return words.stream().collect(Collectors.joining(" "));
}
}

View file

@ -0,0 +1,164 @@
/*
* Copyright (c) 2014, 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.
*/
package jdk.internal.net.http.hpack;
import java.util.Objects;
import java.util.Random;
import org.junit.jupiter.api.Test;
public final class TestHelper {
public static Random newRandom() {
long seed = Long.getLong("jdk.test.lib.random.seed", System.currentTimeMillis());
System.out.println("new java.util.Random(" + seed + ")");
return new Random(seed);
}
public static <T extends Throwable> T assertVoidThrows(Class<T> clazz, Block<?> code) {
return assertThrows(clazz, () -> {
code.run();
return null;
});
}
public static <T extends Throwable> T assertThrows(Class<T> clazz, ReturningBlock<?> code) {
Objects.requireNonNull(clazz, "clazz == null");
Objects.requireNonNull(code, "code == null");
try {
code.run();
} catch (Throwable t) {
if (clazz.isInstance(t)) {
return clazz.cast(t);
}
throw new AssertionError("Expected to catch exception of type "
+ clazz.getCanonicalName() + ", instead caught "
+ t.getClass().getCanonicalName(), t);
}
throw new AssertionError(
"Expected to catch exception of type " + clazz.getCanonicalName()
+ ", but caught nothing");
}
public static <T> T assertDoesNotThrow(ReturningBlock<T> code) {
Objects.requireNonNull(code, "code == null");
try {
return code.run();
} catch (Throwable t) {
throw new AssertionError(
"Expected code block to exit normally, instead " +
"caught " + t.getClass().getCanonicalName(), t);
}
}
public static void assertVoidDoesNotThrow(Block<?> code) {
Objects.requireNonNull(code, "code == null");
try {
code.run();
} catch (Throwable t) {
throw new AssertionError(
"Expected code block to exit normally, instead " +
"caught " + t.getClass().getCanonicalName(), t);
}
}
public static void assertExceptionMessageContains(Throwable t,
CharSequence firstSubsequence,
CharSequence... others) {
assertCharSequenceContains(t.getMessage(), firstSubsequence, others);
}
public static void assertCharSequenceContains(CharSequence s,
CharSequence firstSubsequence,
CharSequence... others) {
if (s == null) {
throw new NullPointerException("Exception message is null");
}
String str = s.toString();
String missing = null;
if (!str.contains(firstSubsequence.toString())) {
missing = firstSubsequence.toString();
} else {
for (CharSequence o : others) {
if (!str.contains(o.toString())) {
missing = o.toString();
break;
}
}
}
if (missing != null) {
throw new AssertionError("CharSequence '" + s + "'" + " does not "
+ "contain subsequence '" + missing + "'");
}
}
public interface ReturningBlock<T> {
T run() throws Throwable;
}
public interface Block<T> {
void run() throws Throwable;
}
// tests
@Test
public void assertThrows() {
assertThrows(NullPointerException.class, () -> ((Object) null).toString());
}
@Test
public void assertThrowsWrongType() {
try {
assertThrows(IllegalArgumentException.class, () -> ((Object) null).toString());
} catch (AssertionError e) {
Throwable cause = e.getCause();
String message = e.getMessage();
if (cause != null
&& cause instanceof NullPointerException
&& message != null
&& message.contains("instead caught")) {
return;
}
}
throw new AssertionError();
}
@Test
public void assertThrowsNoneCaught() {
try {
assertThrows(IllegalArgumentException.class, () -> null);
} catch (AssertionError e) {
Throwable cause = e.getCause();
String message = e.getMessage();
if (cause == null
&& message != null
&& message.contains("but caught nothing")) {
return;
}
}
throw new AssertionError();
}
}

Binary file not shown.