Java 26: HTTP/3 for the HTTP Client API (JEP 517)

Zero-RTT connection setup, no head-of-line blocking, and connection migration across networks — Java’s built-in HttpClient now speaks HTTP/3. JEP 517 finalizes HTTP/3 support in JDK 26, making it the first JDK version with zero-dependency QUIC-based HTTP.

Status: Standard — available in JDK 26 without any flags.

What Changed

java.net.http.HttpClient now supports HTTP/3 (RFC 9114), which runs over QUIC (RFC 9000) instead of TCP. HTTP/3 is already used by ~30% of all web traffic (Google, Cloudflare, Meta). Until JDK 26, Java developers needed third-party libraries like Netty or Jetty for HTTP/3 support.

HTTP/3 Key Improvements

  • No head-of-line blocking — Each QUIC stream is independently delivered; a lost packet on one stream doesn’t block others.
  • Faster connection setup — QUIC combines TLS and transport handshakes, achieving 0-RTT or 1-RTT.
  • Connection migration — Connections survive network changes (Wi-Fi → cellular) without reconnecting.
  • Built-in encryption — QUIC mandates TLS 1.3 for all traffic.

Demo Highlights

1. HTTP Version Constants

JDK 26 adds HttpClient.Version.HTTP_3 alongside the existing HTTP_1_1 and HTTP_2 constants:

for (HttpClient.Version version : HttpClient.Version.values()) {
    System.out.println("Available: " + version);
}
// Available: HTTP_1_1
// Available: HTTP_2
// Available: HTTP_3

2. Basic HTTP/3 GET Request

Configure the client with HTTP_3 and the transport upgrades automatically when the server supports it:

try (var client = HttpClient.newBuilder()
        .version(HttpClient.Version.HTTP_3)
        .connectTimeout(Duration.ofSeconds(5))
        .build()) {

    var request = HttpRequest.newBuilder()
        .uri(URI.create("https://www.google.com"))
        .GET()
        .build();

    var response = client.send(request, HttpResponse.BodyHandlers.ofString());

    System.out.println("Status:   " + response.statusCode());
    System.out.println("Protocol: " + response.version());  // → HTTP_3
}

3. Version Negotiation Comparison

Make the same request with different preferred versions and inspect which protocol was actually negotiated:

HttpClient.Version[] versions = {
    HttpClient.Version.HTTP_1_1,
    HttpClient.Version.HTTP_2,
    HttpClient.Version.HTTP_3
};

for (var preferredVersion : versions) {
    try (var client = HttpClient.newBuilder()
            .version(preferredVersion)
            .connectTimeout(Duration.ofSeconds(5))
            .build()) {

        var response = client.send(request, HttpResponse.BodyHandlers.discarding());
        System.out.println("Preferred: " + preferredVersion
            + " → Actual: " + response.version());
    }
}

4. Async HTTP/3 Requests

HTTP/3 works seamlessly with sendAsync. QUIC’s multiplexing means multiple concurrent requests over a single connection without head-of-line blocking:

CompletableFuture<HttpResponse<String>> future =
    client.sendAsync(request, HttpResponse.BodyHandlers.ofString());

// Non-blocking: do other work while waiting
HttpResponse<String> response = future.join();
System.out.println("Protocol: " + response.version());

5. Concurrent Multiplexed Requests

HTTP/3’s QUIC transport eliminates head-of-line blocking. Multiple concurrent requests over the same connection don’t interfere:

List<String> urls = List.of(
    "https://www.google.com",
    "https://www.google.com/search?q=java",
    "https://www.google.com/search?q=quic",
    "https://www.google.com/search?q=http3"
);

List<CompletableFuture<HttpResponse<String>>> futures = urls.stream()
    .map(url -> HttpRequest.newBuilder().uri(URI.create(url)).GET().build())
    .map(req -> client.sendAsync(req, HttpResponse.BodyHandlers.ofString()))
    .toList();

CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)).join();
// All 4 requests ran over QUIC streams in parallel — no blocking

Real-World Use Cases

The companion Http3RealWorldExamples class demonstrates six production scenarios:

Version-Aware REST API Client

A microservice client that prefers HTTP/3 for reduced latency but logs the actual negotiated protocol for diagnostics:

try (var client = HttpClient.newBuilder()
        .version(HttpClient.Version.HTTP_3)
        .connectTimeout(Duration.ofSeconds(5))
        .build()) {

    var request = HttpRequest.newBuilder()
        .uri(URI.create("https://api.example.com/v1/users"))
        .header("Accept", "application/json")
        .GET()
        .build();

    var response = client.send(request, HttpResponse.BodyHandlers.ofString());
    System.out.println("Protocol: " + response.version());  // Logged for diagnostics
}

Health Check Service with Protocol Detection

Monitor multiple endpoints and report their HTTP version support alongside availability:

List<String> endpoints = List.of(
    "https://www.google.com",
    "https://cloudflare.com",
    "https://www.github.com"
);

for (String endpoint : endpoints) {
    long start = System.nanoTime();
    var response = client.send(headRequest(endpoint), HttpResponse.BodyHandlers.discarding());
    long latency = (System.nanoTime() - start) / 1_000_000;

    String badge = switch (response.version()) {
        case HTTP_3   -> "🟢 HTTP/3";
        case HTTP_2   -> "🟡 HTTP/2";
        case HTTP_1_1 -> "🔴 HTTP/1.1";
    };
    System.out.println(badge + "  " + endpoint + "  " + latency + "ms");
}

Parallel API Aggregator (Fan-Out)

A BFF layer aggregates data from multiple upstream services in parallel. HTTP/3’s QUIC multiplexing avoids head-of-line blocking — a slow response doesn’t stall the others:

try (var client = HttpClient.newBuilder()
        .version(HttpClient.Version.HTTP_3)
        .build()) {

    List<CompletableFuture<String>> futures = List.of(
        "user-profile", "recent-orders", "notifications", "recommendations"
    ).stream()
        .map(service -> client.sendAsync(requestFor(service), BodyHandlers.ofString())
            .thenApply(r -> service + " → " + r.version() + " " + r.statusCode()))
        .toList();

    CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
}

Resilient Downloader with Version Fallback

Download content preferring HTTP/3, but gracefully fall back through HTTP/2 → HTTP/1.1:

HttpClient.Version[] fallbackChain = {
    HttpClient.Version.HTTP_3,
    HttpClient.Version.HTTP_2,
    HttpClient.Version.HTTP_1_1
};

for (var version : fallbackChain) {
    try (var client = HttpClient.newBuilder()
            .version(version)
            .connectTimeout(Duration.ofSeconds(3))
            .build()) {

        var response = client.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() == 200) {
            System.out.println("✅ Downloaded via " + response.version());
            break;
        }
    } catch (Exception e) {
        System.out.println("⚠️ " + version + " failed — trying next...");
    }
}

Other Use Cases in the Demo

  • Protocol version audit — Scan endpoints and report which support HTTP/3 (compliance dashboards)
  • Latency benchmark — Compare HTTP/2 vs HTTP/3 response times

Best For

HTTP/3 benefits are most visible for:

  • High-latency or lossy networks (mobile, satellite, intercontinental)
  • APIs with many small concurrent requests (microservices, fan-out)
  • CDN integration (Cloudflare, Fastly, Akamai all support HTTP/3)
  • Mobile backends where connection migration avoids reconnection overhead

Running the Demo

mvn compile exec:exec \
  -Dexec.mainClass=org.example.standard.Http3Demo

Check out the full source on GitHub.