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.
397 lines
17 KiB
Java
397 lines
17 KiB
Java
/*
|
|
* Copyright (c) 2023, 2026, Oracle and/or its affiliates. All rights reserved.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* This code is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License version 2 only, as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* version 2 for more details (a copy is included in the LICENSE file that
|
|
* accompanied this code).
|
|
*
|
|
* You should have received a copy of the GNU General Public License version
|
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*
|
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
* or visit www.oracle.com if you need additional information or have any
|
|
* questions.
|
|
*/
|
|
|
|
/*
|
|
* @test
|
|
* @bug 8087112 8159814
|
|
* @library /test/lib /test/jdk/java/net/httpclient/lib
|
|
* @build jdk.httpclient.test.lib.http2.Http2TestServer
|
|
* jdk.httpclient.test.lib.http2.PushHandler
|
|
* jdk.test.lib.Utils
|
|
* jdk.test.lib.net.SimpleSSLContext
|
|
* @run junit/othervm/timeout=960
|
|
* -Djdk.httpclient.HttpClient.log=errors,requests,headers
|
|
* -Djdk.internal.httpclient.debug=false
|
|
* H3ServerPush
|
|
* @summary This is a clone of http2/ServerPush but for HTTP/3
|
|
*/
|
|
|
|
import java.io.IOException;
|
|
import java.io.UncheckedIOException;
|
|
import java.net.URI;
|
|
import java.net.http.HttpClient;
|
|
import java.net.http.HttpClient.Builder;
|
|
import java.net.http.HttpClient.Version;
|
|
import java.net.http.HttpRequest;
|
|
import java.net.http.HttpResponse;
|
|
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.nio.ByteBuffer;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.nio.file.Paths;
|
|
import java.util.ArrayList;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
import java.util.concurrent.CompletableFuture;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import java.util.concurrent.ConcurrentMap;
|
|
import java.util.function.Consumer;
|
|
|
|
import jdk.httpclient.test.lib.common.HttpServerAdapters;
|
|
import jdk.httpclient.test.lib.http2.Http2TestServer;
|
|
import jdk.httpclient.test.lib.http2.PushHandler;
|
|
import jdk.test.lib.net.SimpleSSLContext;
|
|
|
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
|
import static jdk.test.lib.Utils.createTempFileOfSize;
|
|
|
|
import org.junit.jupiter.api.AfterAll;
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
import org.junit.jupiter.api.BeforeAll;
|
|
import org.junit.jupiter.api.Test;
|
|
|
|
public class H3ServerPush implements HttpServerAdapters {
|
|
|
|
private static final String CLASS_NAME = H3ServerPush.class.getSimpleName();
|
|
|
|
static final int LOOPS = 13;
|
|
static final int FILE_SIZE = 512 * 1024 + 343;
|
|
|
|
private static Path tempFile;
|
|
|
|
private static HttpTestServer server;
|
|
private static URI uri;
|
|
private static URI headURI;
|
|
|
|
@BeforeAll
|
|
public static void setup() throws Exception {
|
|
tempFile = createTempFileOfSize(CLASS_NAME, ".dat", FILE_SIZE);
|
|
var sslContext = SimpleSSLContext.findSSLContext();
|
|
var h2Server = new Http2TestServer(true, sslContext);
|
|
h2Server.enableH3AltServiceOnSamePort();
|
|
h2Server.addHandler(new PushHandler(tempFile, LOOPS), "/foo/");
|
|
System.out.println("Using temp file:" + tempFile);
|
|
server = HttpTestServer.of(h2Server);
|
|
server.addHandler(new HttpHeadOrGetHandler(), "/head/");
|
|
headURI = new URI("https://" + server.serverAuthority() + "/head/x");
|
|
uri = new URI("https://" + server.serverAuthority() + "/foo/a/b/c");
|
|
|
|
System.err.println("Server listening at " + server.serverAuthority());
|
|
server.start();
|
|
|
|
}
|
|
|
|
private void sendHeadRequest(HttpClient client) throws IOException, InterruptedException {
|
|
HttpRequest headRequest = HttpRequest.newBuilder(headURI)
|
|
.HEAD().version(Version.HTTP_2).build();
|
|
var headResponse = client.send(headRequest, BodyHandlers.ofString());
|
|
assertEquals(200, headResponse.statusCode());
|
|
assertEquals(Version.HTTP_2, headResponse.version());
|
|
}
|
|
|
|
@AfterAll
|
|
public static void teardown() {
|
|
server.stop();
|
|
}
|
|
|
|
// Test 1 - custom written push promise handler, everything as a String
|
|
@Test
|
|
public void testTypeString() throws Exception {
|
|
System.out.println("\n**** testTypeString\n");
|
|
String tempFileAsString = Files.readString(tempFile);
|
|
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);
|
|
};
|
|
|
|
try (HttpClient client = newClientBuilderForH3().proxy(Builder.NO_PROXY)
|
|
.sslContext(SimpleSSLContext.findSSLContext())
|
|
.version(Version.HTTP_3).build()) {
|
|
sendHeadRequest(client);
|
|
|
|
HttpRequest request = HttpRequest.newBuilder(uri).GET()
|
|
.build();
|
|
CompletableFuture<HttpResponse<String>> cf =
|
|
client.sendAsync(request, BodyHandlers.ofString(UTF_8), pph);
|
|
resultMap.put(request, cf);
|
|
System.out.println("waiting for response");
|
|
var resp = cf.join();
|
|
assertEquals(Version.HTTP_3, resp.version());
|
|
var seen = new HashSet<>();
|
|
resultMap.forEach((k, v) -> {
|
|
if (seen.add(k)) {
|
|
System.out.println("Got " + v.join());
|
|
}
|
|
});
|
|
|
|
// waiting for all promises to reach us
|
|
System.out.println("waiting for promises");
|
|
System.out.println("results.size: " + resultMap.size());
|
|
for (HttpRequest r : resultMap.keySet()) {
|
|
System.out.println("Checking " + r);
|
|
HttpResponse<String> response = resultMap.get(r).join();
|
|
assertEquals(200, response.statusCode());
|
|
assertEquals(Version.HTTP_3, response.version());
|
|
assertEquals(tempFileAsString, response.body());
|
|
}
|
|
resultMap.forEach((k, v) -> {
|
|
if (seen.add(k)) {
|
|
System.out.println("Got " + v.join());
|
|
}
|
|
});
|
|
assertEquals(LOOPS + 1, resultMap.size());
|
|
}
|
|
}
|
|
|
|
// Test 2 - of(...) populating the given Map, everything as a String
|
|
@Test
|
|
public void testTypeStringOfMap() throws Exception {
|
|
System.out.println("\n**** testTypeStringOfMap\n");
|
|
String tempFileAsString = Files.readString(tempFile);
|
|
ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<String>>>
|
|
resultMap = new ConcurrentHashMap<>();
|
|
|
|
PushPromiseHandler<String> pph =
|
|
PushPromiseHandler.of(pushPromise -> BodyHandlers.ofString(UTF_8), resultMap);
|
|
|
|
try (HttpClient client = newClientBuilderForH3().proxy(Builder.NO_PROXY)
|
|
.sslContext(SimpleSSLContext.findSSLContext())
|
|
.version(Version.HTTP_3).build()) {
|
|
sendHeadRequest(client);
|
|
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(Version.HTTP_3, response.version());
|
|
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 {
|
|
System.out.println("\n**** testTypePath\n");
|
|
String tempFileAsString = Files.readString(tempFile);
|
|
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);
|
|
};
|
|
|
|
try (HttpClient client = newClientBuilderForH3().proxy(Builder.NO_PROXY)
|
|
.sslContext(SimpleSSLContext.findSSLContext())
|
|
.version(Version.HTTP_3).build()) {
|
|
sendHeadRequest(client);
|
|
|
|
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());
|
|
assertEquals(Version.HTTP_3, response.version());
|
|
String fileAsString = Files.readString(response.body());
|
|
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 {
|
|
System.out.println("\n**** testTypePathOfMap\n");
|
|
String tempFileAsString = Files.readString(tempFile);
|
|
ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<Path>>> resultsMap
|
|
= new ConcurrentHashMap<>();
|
|
|
|
PushPromiseHandler<Path> pushPromiseHandler =
|
|
PushPromiseHandler.of(H3ServerPush::requestToPath, resultsMap);
|
|
|
|
try (HttpClient client = newClientBuilderForH3().proxy(Builder.NO_PROXY)
|
|
.sslContext(SimpleSSLContext.findSSLContext())
|
|
.version(Version.HTTP_3).build()) {
|
|
sendHeadRequest(client);
|
|
|
|
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());
|
|
assertEquals(Version.HTTP_3, response.version());
|
|
String fileAsString = Files.readString(response.body());
|
|
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.isEmpty()) {
|
|
int size = listByteArrays.stream().mapToInt(ba -> ba.length).sum();
|
|
ByteBuffer bb = ByteBuffer.allocate(size);
|
|
listByteArrays.forEach(bb::put);
|
|
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 {
|
|
System.out.println("\n**** testTypeByteArrayConsumer\n");
|
|
String tempFileAsString = Files.readString(tempFile);
|
|
ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<Void>>> resultsMap
|
|
= new ConcurrentHashMap<>();
|
|
Map<HttpRequest,ByteArrayConsumer> byteArrayConsumerMap
|
|
= new ConcurrentHashMap<>();
|
|
|
|
try (HttpClient client = newClientBuilderForH3().proxy(Builder.NO_PROXY)
|
|
.sslContext(SimpleSSLContext.findSSLContext())
|
|
.version(Version.HTTP_3).build()) {
|
|
sendHeadRequest(client);
|
|
|
|
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());
|
|
assertEquals(Version.HTTP_3, response.version());
|
|
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 {
|
|
System.out.println("\n**** testTypeByteArrayConsumerOfMap\n");
|
|
String tempFileAsString = Files.readString(tempFile);
|
|
ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<Void>>> resultsMap
|
|
= new ConcurrentHashMap<>();
|
|
Map<HttpRequest, ByteArrayConsumer> byteArrayConsumerMap
|
|
= new ConcurrentHashMap<>();
|
|
|
|
try (HttpClient client = newClientBuilderForH3().proxy(Builder.NO_PROXY)
|
|
.sslContext(SimpleSSLContext.findSSLContext())
|
|
.version(Version.HTTP_3).build()) {
|
|
sendHeadRequest(client);
|
|
|
|
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());
|
|
assertEquals(Version.HTTP_3, response.version());
|
|
byte[] ba = byteArrayConsumerMap.get(r).getAccumulatedBytes();
|
|
String result = new String(ba, UTF_8);
|
|
assertEquals(tempFileAsString, result);
|
|
}
|
|
assertEquals(LOOPS + 1, resultsMap.size());
|
|
}
|
|
}
|
|
}
|