Add http3 module: HTTP/3 in the Java HTTP Client (JEP 517), opt-in, discovery modes, fallback timings, proxy downgrade and a packet-loss comparison

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KqJyCidz3ZgRyHABv2GVJh
This commit is contained in:
2026-09-30 20:27:46 +00:00
co-authored by Claude Sonnet 5.5
parent c8ff3acf09
commit fadfd05670
25 changed files with 617 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
/** Plain Java 11 HttpClient code. No version is set. The same source file runs on JDK 11 to 27. */
public class Basics {
public static void main(String[] args) throws Exception {
var client = Tls.builder().build(); // the only non-11 line is the self-signed trust
var request = HttpRequest.newBuilder(URI.create(args[0])).GET().build();
for (int i = 1; i <= 3; i++) {
HttpResponse<String> r = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("request " + i + ": " + r.version() + " " + r.statusCode() + " alt-svc=" + r.headers().firstValue("alt-svc").orElse("-"));
}
}
}
+49
View File
@@ -0,0 +1,49 @@
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpOption;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.CompletableFuture;
/**
* Bench PROTO WORKLOAD TRIALS LOSS_LABEL
* PROTO h1 | h2 | h3 (h3 uses HTTP_3_URI_ONLY so a silent fallback cannot flatter the result)
* WORKLOAD one = fresh client, one 1 MiB download (handshake + one stream)
* many = fresh client, 10 parallel 100 KiB downloads (handshake + ten streams)
* Every response is checked: same HTTP version as asked for, exact body length. A trial that throws or
* exceeds 15 s is counted as failed.
*/
public class Bench {
public static void main(String[] a) throws Exception {
String proto = a[0], workload = a[1], loss = a[3];
int trials = Integer.parseInt(a[2]);
var version = switch (proto) { case "h1" -> HttpClient.Version.HTTP_1_1; case "h2" -> HttpClient.Version.HTTP_2; default -> HttpClient.Version.HTTP_3; };
int n = workload.equals("one") ? 1 : 10;
int size = workload.equals("one") ? 1_048_576 : 102_400;
List<Long> times = new ArrayList<>();
int failed = 0;
String firstFailure = null;
for (int t = -1; t < trials; t++) { // trial -1 is a warm-up and is discarded
long t0 = System.nanoTime();
try (var client = Tls.builder().version(version).build()) {
var rb = HttpRequest.newBuilder(URI.create("https://localhost:4433/bytes/" + size)).timeout(Duration.ofSeconds(15));
if (version == HttpClient.Version.HTTP_3) rb.setOption(HttpOption.H3_DISCOVERY, HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY);
var req = rb.build();
List<CompletableFuture<HttpResponse<byte[]>>> fs = new ArrayList<>();
for (int i = 0; i < n; i++) fs.add(client.sendAsync(req, HttpResponse.BodyHandlers.ofByteArray()));
for (var f : fs) {
var r = f.get();
if (r.version() != version || r.body().length != size) throw new IllegalStateException(r.version() + " " + r.body().length);
}
if (t >= 0) times.add((System.nanoTime() - t0) / 1_000_000);
} catch (Exception e) {
if (t >= 0) { failed++; if (firstFailure == null) firstFailure = e.toString(); }
}
}
Collections.sort(times);
String stats = times.isEmpty() ? "n/a" : "median=%d p90=%d max=%d".formatted(times.get(times.size() / 2), times.get((int) Math.min(times.size() - 1, Math.ceil(times.size() * 0.9) - 1)), times.get(times.size() - 1));
System.out.printf("%s %-4s loss=%-3s ok=%d failed=%d %s (ms)%s%n", proto, workload, loss, times.size(), failed, stats, firstFailure == null ? "" : " first failure: " + firstFailure);
}
}
+28
View File
@@ -0,0 +1,28 @@
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpOption.Http3DiscoveryMode;
import java.net.http.HttpOption;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
/** One fresh client, the three H3_DISCOVERY modes, four requests each. */
public class Discovery {
public static void main(String[] args) throws Exception {
URI uri = URI.create(args[0]);
for (Http3DiscoveryMode mode : Http3DiscoveryMode.values()) {
var client = Tls.builder().version(HttpClient.Version.HTTP_3).build();
var req = HttpRequest.newBuilder(uri).setOption(HttpOption.H3_DISCOVERY, mode).build();
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 4; i++) {
try {
var r = client.send(req, HttpResponse.BodyHandlers.ofString());
sb.append(' ').append(r.version());
} catch (Exception e) {
sb.append(' ').append(e.getClass().getSimpleName()).append('(').append(e.getMessage()).append(')');
}
}
System.out.printf("%-16s ->%s%n", mode, sb);
client.close();
}
}
}
+29
View File
@@ -0,0 +1,29 @@
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpOption;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
/** How long does the first request take, and which version answers, for each way of asking for HTTP/3? */
public class Fallback {
public static void main(String[] args) throws Exception {
URI uri = URI.create(args[0]);
long limitSeconds = Long.parseLong(args[1]);
for (String mode : new String[] {"request HTTP_3 (ANY)", "client HTTP_3 (ANY)", "request HTTP_3_URI_ONLY"}) {
var cb = Tls.builder();
if (mode.startsWith("client")) cb.version(HttpClient.Version.HTTP_3);
var rb = HttpRequest.newBuilder(uri).timeout(Duration.ofSeconds(limitSeconds));
if (!mode.startsWith("client")) rb.version(HttpClient.Version.HTTP_3);
if (mode.endsWith("URI_ONLY")) rb.setOption(HttpOption.H3_DISCOVERY, HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY);
long t0 = System.nanoTime();
String result;
try (var client = cb.build()) {
result = client.send(rb.build(), HttpResponse.BodyHandlers.ofString()).version().toString();
} catch (Exception e) {
result = e.getClass().getName() + ": " + e.getMessage();
}
System.out.printf("%-26s -> %-70s after %5d ms%n", mode, result, (System.nanoTime() - t0) / 1_000_000);
}
}
}
+22
View File
@@ -0,0 +1,22 @@
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
/** The JEP 517 opt-in, client-wide or per request. Prints the version every response really used. */
public class OptIn {
public static void main(String[] args) throws Exception {
URI uri = URI.create(args[0]);
String where = args.length > 1 ? args[1] : "client"; // "client" or "request"
var builder = Tls.builder();
if (where.equals("client")) builder.version(HttpClient.Version.HTTP_3);
var client = builder.build();
var req = HttpRequest.newBuilder(uri);
if (where.equals("request")) req.version(HttpClient.Version.HTTP_3);
for (int i = 1; i <= 4; i++) {
var r = client.send(req.build(), HttpResponse.BodyHandlers.ofString());
System.out.println(where + "-level HTTP_3, request " + i + ": " + r.version());
}
client.close();
}
}
+26
View File
@@ -0,0 +1,26 @@
import java.io.FileInputStream;
import java.net.http.HttpClient;
import java.security.KeyStore;
import java.security.cert.CertificateFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
/** Trusts only the self-signed test certificate in certs/cert.pem (made by server/make-cert.sh). */
final class Tls {
static SSLContext context() throws Exception {
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(null);
try (var in = new FileInputStream("certs/cert.pem")) {
ks.setCertificateEntry("test", CertificateFactory.getInstance("X.509").generateCertificate(in));
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
tmf.init(ks);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, tmf.getTrustManagers(), null);
return ctx;
}
static HttpClient.Builder builder() throws Exception {
return HttpClient.newBuilder().sslContext(context());
}
}
+29
View File
@@ -0,0 +1,29 @@
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpOption;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
/** The Javadoc says HTTP/3 through a proxy is not supported: ANY silently downgrades, HTTP_3_URI_ONLY fails. */
public class ViaProxy {
public static void main(String[] args) throws Exception {
URI uri = URI.create(args[0]);
int proxyPort = Integer.parseInt(args[1]);
for (boolean uriOnly : new boolean[] {false, true}) {
var client = Tls.builder().version(HttpClient.Version.HTTP_3)
.proxy(ProxySelector.of(new InetSocketAddress("127.0.0.1", proxyPort))).build();
var rb = HttpRequest.newBuilder(uri).timeout(Duration.ofSeconds(10));
if (uriOnly) rb.setOption(HttpOption.H3_DISCOVERY, HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY);
try {
var r = client.send(rb.build(), HttpResponse.BodyHandlers.ofString());
System.out.println((uriOnly ? "HTTP_3_URI_ONLY" : "ANY ") + " via proxy -> " + r.version());
} catch (Exception e) {
System.out.println((uriOnly ? "HTTP_3_URI_ONLY" : "ANY ") + " via proxy -> " + e.getClass().getName() + ": " + e.getMessage());
}
client.close();
}
}
}