HTTP/3 Support in JDK 26

One new JDK 26 feature is that HttpClient, which has been part of Java SE since JDK 11, now supports HTTP/3.

Before discussing some API details, let’s briefly review what HTTP/3 is. From an HTTP protocol perspective, it is not vastly different from HTTP/2 in terms of features. However, the major difference lies in the underlying transport protocol: whereas HTTP/2 operates over TCP, HTTP/3 uses UDP. HTTP/3 is built on top of the QUIC protocol. For more details, please refer to JEP 517.


Using the HttpClient API

Let’s now see how to use HTTP/3 support with the java.net.http.HttpClient API. If you have never used this API before, its Javadoc is a good place to start.

In summary, an application creates a java.net.http.HttpClient instance and typically maintains it for the application’s lifetime. When issuing HTTP requests, the application code then builds a java.net.http.HttpRequest instance and uses the HttpClient.send(...) method to send the request and obtain a java.net.http.HttpResponse. In more advanced use cases where the application does not want to wait for the response, the HttpClient.sendAsync(...) method can be used to send the request asynchronously. This method returns a java.util.concurrent.CompletableFuture, which the application can use later to obtain the related HttpResponse.

HttpResponse provides methods to retrieve the response body, the HTTP response code, the protocol version used, etc. A typical usage example is shown below:

HttpClient client = HttpClient.newBuilder().build(); // create a HttpClient instance
...
URI reqURI = new URI("https://www.google.com/");
HttpRequest req = HttpRequest.newBuilder().uri(reqURI).build(); // create a request instance

final HttpResponse.BodyHandler<String> bodyHandler = BodyHandlers.ofString(StandardCharsets.UTF_8);
HttpResponse<String> resp = client.send(req, bodyHandler); // send the request and obtain the response as a String content
System.out.println("status code: " + resp.statusCode() + " HTTP protocol version: " + resp.version()); // print the response status code and the HTTP protocol version used

None of this is new as this API has been around since JDK 11. So, let’s now see what’s new in JDK 26 and how to enable HTTP/3 support in HttpClient.

By default, the HttpClient (even in JDK 26) uses HTTP/2 as the preferred HTTP version when issuing requests. You can override the default per HttpClient-instance behavior by setting a preferred version. For example:

HttpClient client = HttpClient.newBuilder()
        .version(HttpClient.Version.HTTP_1_1)
        .build();

will construct a client that uses HTTP/1.1 as the preferred version for all requests it issues. The HTTP protocol version set at the client level can also be overridden at the HttpRequest level as the following example.

HttpRequest req = HttpRequest.newBuilder()
        .uri(reqURI)
        .version(HttpClient.Version.HTTP_2)
        .build();

In this example, the HttpClient will use the preferred version specified by the HttpRequest, i.e., HTTP/2 in this case, when issuing the request. If the server doesn’t support HTTP/2, the internal HttpClient implementation will automatically downgrade the request to the HTTP/1.1 protocol, establish an HTTP/1.1 request/response exchange with the server, and provide the application with the related HTTP/1.1 response.

This behavior has been present in previous HttpClient implementations. What is new in JDK 26 is the introduction of a new protocol version value: HttpClient.Version.HTTP_3. Applications can opt in to use the HTTP/3 protocol version by either setting it as a preferred version at the HttpClient instance level:

HttpClient client = HttpClient.newBuilder()
        .version(HttpClient.Version.HTTP_3)
        .build();

or on specific HttpRequest instance:

HttpRequest req = HttpRequest.newBuilder()
        .uri(reqURI)
        .version(HttpClient.Version.HTTP_3)
        .build();

In either case, when HTTP_3 is set as the preferred version, the HttpClient implementation will attempt to establish a UDP-based connection (since HTTP/3 operates over UDP) to the target server. If that UDP-based QUIC connection attempt fails, either because the server does not support HTTP/3 or if the connection does not complete in a timely manner, the HttpClient implementation will automatically downgrade the protocol version to HTTP/2 (over TCP) and attempt to complete the request using HTTP/2. If the server does not support HTTP/2, the request will be further downgraded to HTTP/1.1, as before.

So, the application code would look similar to what we saw previously with just one change, where the preferred version is set as HTTP/3 either when building the HttpClient instance or when building the HttpRequest instance:

HttpClient client = HttpClient.newBuilder()
        .version(HttpClient.Version.HTTP_3)
        .build(); // create a HttpClient instance with HTTP/3 as the preferred version
...
URI reqURI = new URI("https://www.google.com/");
HttpRequest req = HttpRequest.newBuilder()
        .uri(reqURI)
        .build(); // create a request instance

final HttpResponse.BodyHandler<String> bodyHandler = BodyHandlers.ofString(StandardCharsets.UTF_8);
HttpResponse<String> resp = client.send(req, bodyHandler); // send the request and obtain the response as a String content
System.out.println("status code: " + resp.statusCode() + 
        " HTTP protocol version: " + resp.version()); // print the response status code and the HTTP protocol version used


HTTP Version Discovery

Please note that setting HTTP/3 as the preferred version does not guarantee that the request will use HTTP/3 as the protocol version. This is why it is referred to as the “preferred” version. The HttpClient cannot determine in advance whether the server to which the request is sent actually supports HTTP/3.

Therefore for the first request, the HttpClient instance will use an internal implementation specific algorithm which involves an attempt to establish a TCP based communication (HTTP/2) or a UDP based communication (HTTP/3) against that server. Whichever succeeds first will be used as the communication mode and thus decides which HTTP protocol version is used for that request.

For more information on HTTP/3 version discovery, please refer to the Http3DiscoveryMode Javadoc.

With that background, let’s now look into a few specific cases and some code examples demonstrating the usage.

Let’s consider the case where the application wants to force the use of HTTP/3 - i.e. try and communicate with the server only through QUIC (i.e. over UDP) and then issue the HTTP/3 request. And if that fails, the connection should not be downgraded to HTTP/2. Applications would typically do this only when they are absolutely certain that the target server (represented by the host and port used in the request URI) supports HTTP/3 over that host/port combination.

For example, consider google.com as the server against which we will issue that request. Based on prior experiments, we know that google.com supports HTTP/3 at the same host/port where it supports HTTP/2 (or HTTP/1.1). Here’s what the code would look like in this case:

import java.net.http.HttpClient;
import java.net.http.HttpOption;
import java.net.http.HttpOption.Http3DiscoveryMode;
import java.net.http.HttpClient.Version;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse.BodyHandlers;
import java.net.http.HttpResponse;
import java.net.URI;
import java.nio.charset.StandardCharsets;

...

HttpClient client = HttpClient.newBuilder()
                     .version(Version.HTTP_3) // configure HTTP/3 as the preferred version of the client
                     .build();
URI reqURI = new URI("https://www.google.com/");
HttpRequest req = HttpRequest.newBuilder()
                   .uri(reqURI)
                   .setOption(HttpOption.H3_DISCOVERY, Http3DiscoveryMode.HTTP_3_URI_ONLY) // enforce that only HTTP/3 is used
                   .build();

final HttpResponse.BodyHandler<String> bodyHandler = BodyHandlers.ofString(StandardCharsets.UTF_8);
HttpResponse<String> resp = client.send(req, bodyHandler); // send the request and obtain the response as a String content
System.out.println("status code: " + resp.statusCode() + " HTTP protocol version: " + resp.version()); // print the response status code and the HTTP protocol version used

Apart from configuring the HttpClient instance with version(Version.HTTP_3) as the preferred version, the other important detail in this code is the line where we configure the HttpRequest with:

setOption(HttpOption.H3_DISCOVERY, Http3DiscoveryMode.HTTP_3_URI_ONLY) // enforce that only HTTP/3 is used

The H3_DISCOVERY option with a value of Http3DiscoveryMode.HTTP_3_URI_ONLY instructs the HttpClient instance that this request must only use HTTP/3, and if it’s not possible, then the request should fail (with an exception).

At this point, we know for certain (and even demonstrated) that requests to www.google.com support HTTP/3, so the HttpRequest can enforce the use of HTTP/3 as shown below.

import java.net.http.HttpClient;
import java.net.http.HttpOption;
import java.net.http.HttpOption.Http3DiscoveryMode;
import java.net.http.HttpClient.Version;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse.BodyHandlers;
import java.net.http.HttpResponse;
import java.net.URI;
import java.nio.charset.StandardCharsets;

public class Http3Usage {

    public static void main(final String[] args) throws Exception {
        final boolean printRespHeaders = args.length == 1 && args[0].equals("--print-response-headers");
        try (final HttpClient client = HttpClient.newBuilder()
                                         .version(Version.HTTP_3)
                                         .build()) {

            final URI reqURI = new URI("https://www.google.com/");
            final HttpRequest req = HttpRequest.newBuilder()
                                       .uri(reqURI)
                                       .setOption(HttpOption.H3_DISCOVERY, Http3DiscoveryMode.HTTP_3_URI_ONLY)
                                       .build();

            System.out.println("issuing first request: " + req);

            final HttpResponse.BodyHandler<String> bodyHandler = BodyHandlers.ofString(StandardCharsets.UTF_8);
            final HttpResponse<String> firstResp = client.send(req, bodyHandler);

            System.out.println("received response, status code: " + firstResp.statusCode() + 
                    " HTTP protocol version used: " + firstResp.version());

            if (printRespHeaders) {
                System.out.println("response headers: ");
                firstResp.headers().map().entrySet().forEach((e) -> System.out.println(e));
            }
        }
    }
}

When you use JDK 26 early access build and run this as:

java Http3Usage.java

you should see the following output:

issuing first request: https://www.google.com/ GET
received response, status code: 200 HTTP protocol version used: HTTP_3

Notice that the response’s protocol version is HTTP_3, so it did indeed use HTTP/3 as the protocol version for that request.

Let’s now check what would have happened if we had not instructed the HttpClient to enforce the HTTP/3 usage. So, let’s remove (or comment out) .setOption(HttpOption.H3_DISCOVERY, Http3DiscoveryMode.HTTP_3_URI_ONLY) from the above code and keep the rest of the code as-is. When you do that and run that program again using java Http3Usage.java you should see the following output:

issuing first request: https://www.google.com/ GET
received response, status code: 200 HTTP protocol version used: HTTP_2

Notice the difference in the response’s protocol version: even though HTTP/3 was set as the preferred version, the request/response exchange used HTTP/2. As mentioned earlier, this is expected behavior because the HttpClient instance cannot guarantee that the server at the given host and port supports the “preferred” HTTP/3 version. Therefore, the HttpClient implementation used used an internal algorithm which ended up establishing a TCP based connection first and thus using it to issue the HTTP/2 request.

Taking this example a step further, some of you might wonder whether, over time, the HttpClient can learn that a server at a particular host and port supports HTTP/3.

The answer is yes, there are several mechanisms for this. In fact, the “HTTP Alternative Services” standard (RFC 7838 ) defines one such approach. HTTP Alternative Services (referred to as “Alt-Services” from now on) is a standard mechanism that allows servers to advertise alternative services they support. The RFC outlines several methods for advertising alternative services. One common approach is for the server to include an HTTP response header named “alt-svc” in response to some HTTP requests. The response header value should contain details about the Alt-Services the server supports and the host/port combination where each is supported.

For example, an “alt-svc” header of the following form:

alt-svc=h3=":443"

indicates that the server on the host, which was used for the HTTP request, at port 443 supports HTTP/3 protocol (h3 represents an ALPN for HTTP/3 support). When such a response header is advertised by the server, the HttpClient recognizes this as a standard header and makes note of this detail. So, the next time a request is issued to the same server and port, HttpClient will check its internal registry to determine if an “h3” Alt-Service was previously advertised for that server. If so, it will attempt to establish the HTTP/3 connection using the alternate host and port.

Let’s see this behavior in action. As before, we’ll configure the HttpClient instance with HTTP/3 as the preferred version, but this time we will not enforce HTTP/3 on the HttpRequest. We will then send 2 requests to the same Google URI using the same HttpClient instance and observe how it behaves.

import java.net.http.HttpClient;
import java.net.http.HttpOption;
import java.net.http.HttpOption.Http3DiscoveryMode;
import java.net.http.HttpClient.Version;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse.BodyHandlers;
import java.net.http.HttpResponse;
import java.net.URI;
import java.nio.charset.StandardCharsets;

public class Http3Usage {

    public static void main(final String[] args) throws Exception {

        final boolean printRespHeaders = args.length == 1 && args[0].equals("--print-response-headers");

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

            final URI reqURI = new URI("https://www.google.com/");
            final HttpRequest req = HttpRequest.newBuilder()
                                       .uri(reqURI)
                                       .build();

            System.out.println("issuing first request: " + req);

            final HttpResponse.BodyHandler<String> bodyHandler = BodyHandlers.ofString(StandardCharsets.UTF_8);
            final HttpResponse<String> firstResp = client.send(req, bodyHandler);

            System.out.println("received response, status code: " + firstResp.statusCode() + 
                    " HTTP protocol version used: " + firstResp.version());

            if (printRespHeaders) {
                System.out.println("response headers: ");
                firstResp.headers().map().entrySet().forEach((e) -> System.out.println(e));
            }

            System.out.println("issuing second request: " + req);

            final HttpResponse<String> secondResp = client.send(req, bodyHandler);

            System.out.println("received response, status code: " + secondResp.statusCode() 
                    + " HTTP protocol version used: " + secondResp.version());

            if (printRespHeaders) {
                System.out.println("response headers: ");
                secondResp.headers().map().entrySet().forEach((e) -> System.out.println(e));
            }
        }
    }
}

Let’s run it again with a JDK 26 EA build.

java Http3Usage.java

This command should output the following:

issuing first request: https://www.google.com/ GET
received response, status code: 200 HTTP protocol version used: HTTP_2
issuing second request: https://www.google.com/ GET
received response, status code: 200 HTTP protocol version used: HTTP_3

Notice that the first request used HTTP/2, and the second request against the same request URI, using the same HttpClient instance, used the preferred HTTP/3 version. That demonstrates that the HttpClient instance can determine whether a server at a particular host/port supports HTTP/3 and then use that knowledge to issue HTTP/3 requests against that server, if the application prefers that protocol version.

Earlier, we discussed how servers advertise Alt-Services through response headers. Since our code has access to the HttpResponse, let’s check whether www.google.com actually advertised the “h3” Alt-Service in its response headers.

The code above will output response headers when it is launched with the --print-response-headers program argument.

java Http3Usage.java --print-response-headers

You can see that the first request/response exchange ended up using HTTP/2, while the second request/response exchange used HTTP/3. You will see many more lines in the output, as all the response headers from the HttpResponse are printed. Among these lines, if you search for “alt-svc,” you should find:

alt-svc=[h3=":443"; ma=2592000,h3-29=":443"; ma=2592000]

So www.google.com responded to the request and included this additional response header advertising the Alt-Service representing HTTP/3 support.

There are more ways the HttpClient instance can discover HTTP/3 support, but those are beyond the scope of this article.


Conclusion

Support for HTTP/3 in Java’s HttpClient has been integrated into the mainline JDK repository and is available in the JDK 26 early access builds.

Although the API enhancement appears trivial from a usage perspective (by design), supporting QUIC and subsequently HTTP/3 on top of QUIC represents the result of several years of development effort within the JDK.

New tests have been added, and extensive manual testing has been conducted. However, this implementation is still new and has not seen much usage outside of the JDK development team. If the HTTP/3 support in JDK’s HttpClient is of interest to you, it will be very valuable if you run your applications/experiments using an early access build of JDK 26 and provide any feedback/bug reports on the net-dev OpenJDK mailing list.


~

This article was originally published here.