Sunday, October 05, 2025

HTTP/3 support now available in Java HttpClient for upcoming Java 26 release

Many of you might have heard about the recent Java 25 release. If not, then here's a very good and extensive article on what Java 25 contains https://blogs.oracle.com/java/post/the-arrival-of-java-25.

Today though I will be writing about what's coming in Java 26, which is proposed to be released in March 2026 https://mail.openjdk.org/pipermail/jdk-dev/2025-October/010505.html. Of course, just like Java 25, Java 26 too will have numerous new features, enhancements and bug fixes. As usual early access builds are available and for Java 26 you can find them at https://jdk.java.net/26/

One new feature in Java 26 is that HttpClient, which is part of Java SE since Java 11, will now support HTTP/3 protocol version. Before getting into the details about what the API looks like, let's see what HTTP/3 is about. From a HTTP protocol point of view, it is not much different than HTTP/2. What's significantly different is that unlike HTTP/2 which works using TCP (https://en.wikipedia.org/wiki/Transmission_Control_Protocol), HTTP/3 instead works using UDP (https://en.wikipedia.org/wiki/User_Datagram_Protocol). HTTP/3 protocol is built on top of the (relatively new standard) QUIC protocol. I won't go into the details of the protocol itself (QUIC is vast), but for those who might be interested in these details, JEP 517, through which the HTTP/3 support in HttpClient was integrated has all the details including links to the relevant RFCs https://openjdk.org/jeps/517.

Let's now see how to utilize the HTTP/3 feature of the java.net.http.HttpClient API. If you haven't previously used the Java SE HttpClient then the API doc here https://docs.oracle.com/en/java/javase/25/docs/api/java.net.http/java/net/http/package-summary.html is a good start. Briefly, the application code builds a java.net.http.HttpClient instance and maintains that instance as long as it wishes to (typically throughout 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. Some advanced usages, where the application doesn't want to wait until the response arrives, may use the HttpClient.sendAsync(...) method to send the request and obtain a java.util.concurrent.Future instance, which the application can use at a later point to obtain the HttpResponse. The HttpResponse itself has methods which allow the application to obtain the HTTP response code, the HTTP protocol version that was used for that request/response exchange and of course the body of the response. Typical code would look something like:


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

HttpResponse<String> resp = client.send(req, BodyHandlers.ofString(StandardCharsets.UTF_8)); // 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 and this API has been around since Java 11. So let's see what's new in Java 26 and how to enable HTTP/3 usage by the HttpClient.

By default, the HttpClient (even in Java 26) is enabled with HTTP/2 as the preferred HTTP version to use when issuing requests. The default can be overridden per HttpClient instance basis by setting a preferred version of choice. For example:


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


will construct a client which will use HTTP/1.1 as the preferred version for all the requests that it issues. Unless the HttpRequest instance is coded to use a different version. For example:


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


In this case the HttpRequest's preferred version (HTTP/2) will be the one that the HttpClient will prefer to use when issuing the request. If the server doesn't support HTTP/2 then the internal implementation of the HttpClient will do the necessary work to downgrade to HTTP/1.1 protocol version for that request and establish a HTTP/1.1 request/response exchange with the server and provide the application with that HTTP/1.1 response.

Again, this too is not new and has been this way in the implementation of HttpClient. What's new in Java 26 is that a new version value has been introduced - HttpClient.Version.HTTP_3 representing the HTTP/3 protocol version. Applications can opt in to use 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 instances:


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


In either case, when HTTP_3 version is set as the preferred version, the HttpClient implementation will attempt to establish a UDP based communication (remember HTTP/3 is a UDP based protocol) with the server to which the request is targeted. If that UDP based QUIC connection attempt fails (because the server may not support HTTP/3) or the connection attempt over UDP doesn't complete in a timely fashion (datagram delivery is unreliable), then just like for HTTP/2, the internal implementation of the HttpClient will downgrade the protocol version to use HTTP/2 (remember, over TCP) and try and complete that request using HTTP/2 version (and if the server doesn't support HTTP/2 then like before the request will be internally downgraded to HTTP/1.1 version).

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

HttpResponse<String> resp = client.send(req, BodyHandlers.ofString(StandardCharsets.UTF_8)); // 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


Do note that setting HTTP/3 as the preferred version doesn't necessary mean that the request will mandatorily use HTTP/3 as the protocol version. That's why it's called the preferred version. The HttpClient will not have the necessary knowledge to be sure that HTTP/3 is indeed supported by the server against which the request is issued. So, especially for the first request against that server, 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.

There are a few other details to the HTTP/3 version discovery. I won't go into those for now, but the API documentation of java.net.http.HttpOption.Http3DiscoveryMode has some of those details https://download.java.net/java/early_access/jdk26/docs/api/java.net.http/java/net/http/HttpOption.Http3DiscoveryMode.html.

With that background, let's now consider 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. If that fails, then don't attempt to use HTTP/2 for that request. 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. Let's for this 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 support 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 be used

                   .build();

HttpResponse<String> resp = client.send(req, BodyHandlers.ofString(StandardCharsets.UTF_8)); // 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 be 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 as the protocol version and if it's not possible to use that version, then the request should fail (with an exception). Like previously noted, we know that google at "https://www.google.com/" supports HTTP/3. So we know for a certainty that this request should succeed with HTTP/3. The complete code for this demo is here:


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<String> firstResp = client.send(req, BodyHandlers.ofString(StandardCharsets.UTF_8));

            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 Java 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.

At this point we know for certain (and even demonstrated) that the request to "https://www.google.com/" support HTTP/3 and so the HttpRequest can enforce the use of HTTP/3. Let's see what would have happened if we had not instructed the HttpClient to enforce the HTTP/3 usage. So let's remove (or comment out) this line from the above code and keep the rest of the code as-is:


.setOption(HttpOption.H3_DISCOVERY, Http3DiscoveryMode.HTTP_3_URI_ONLY)


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. The request/response exchange used HTTP_2 when HTTP/3 was the preferred version but not enforced for this request. As noted previously, this is understandable because the HttpClient instance that we used to issue this request can't say for sure if the server at that port/host does indeed support the "preferred" HTTP/3 version. So the HttpClient implementation used an internal algorithm which ended up establishing a TCP based connection first and thus using it to issue the HTTP/2 request.

Moving one step further with this example - some of you might ask whether over time, the HttpClient is capable of gaining knowledge that a server residing at host/port combination supports HTTP/3. The answer is yes. There are several ways this can happen. In fact, there's a standard called "HTTP Alternative Services" (RFC-7838 https://datatracker.ietf.org/doc/html/rfc7838). HTTP Alternative Services (we will refer to it as Alt-Services to keep it short) is a standard way for servers to advertize alternative services that are supported by that server. The RFC states the different ways it can be done. One way is for the server to send back a HTTP response header named "alt-svc" in response to some HTTP request. This header's value is then expected to contain details about the Alt-Services that the server supports and the host/port combination where it is supported. For example, an "alt-svc" header of the following form:


alt-svc=h3=":443"


implies that the server at 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 advertized 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 against that same server host/port combination then it will look into its internal registry to see if an "h3" Alt-Service was advertized for that server and if so, then will try to establish the HTTP/3 request against that alternate host/port combination.

Let's see if we can see this in action, in our demo code. Like previously, we will configure the HttpClient instance with HTTP/3 as the preferred version. We won't enforce HTTP/3 on the HttpRequest because that's not what we want now. Then, we will send 2 requests using the same HttpClient instance against the same google URI and see how it behaves. Here's the code:


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<String> firstResp = client.send(req, BodyHandlers.ofString(StandardCharsets.UTF_8));

            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, BodyHandlers.ofString(StandardCharsets.UTF_8));

            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));

            }

        }

    }

}


With Java 26 early access release, let's run it again as:


java Http3Usage.java


This should now print the following output:


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 how 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 is capable of gaining knowledge that a server at a particular host/port supports HTTP/3, and then is capable of using that knowledge to issue HTTP/3 requests against that server, if the application prefers that protocol version.

We talked about Alt-Services being advertized by servers, as response headers. Since we already have the code here which has access to the HttpResponse, let's see if google did indeed advertize the "h3" Alt-Service as a response header. The above code is already equipped to print the response headers if the application is launched with the "--print-response-headers" program argument. So let's do that:


java Http3Usage.java --print-response-headers


Like previously, you will notice that the first request/response exchange ended up using HTTP/2 and the second request/response exchange used HTTP/3. Additionally you will see a lot more lines in the output, these are all the response headers that we printed from the HttpResponse. In those lines, if you search for "alt-svc" you should find:


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


So google responded to the requests and included this additional response header advertizing the Alt-Service representing HTTP/3 support.

There are a few other ways the HttpClient instance gains knowledge of HTTP/3 support but that's not too important for now.

The support for HTTP/3 in Java's HttpClient has been integrated into the mainline JDK repo and is available in the early access builds of Java 26. The early access builds are available for download at https://jdk.java.net/26/. Although the API enhancement from the usage side looks very trivial (and that's intentional), the work to support QUIC and then HTTP/3 on top of QUIC is the result of several years of work in the JDK. New tests have been added and extensive manual testing has also been carried out to address any issues. However, the implementation is new and hasn't seen much usage outside of the people involved in developing this implementation. If the support for HTTP/3 in JDK's HttpClient is of interest to you, then it will be very valuable if you run your applications/experiments using an early access build of Java 26 (https://jdk.java.net/26/) and provide any feedback/bug reports. JEP 517 has all the necessary details about this feature https://openjdk.org/jeps/517 and if you have any feedback about the usage of this feature, then please subscribe to net-dev OpenJDK mailing list https://mail.openjdk.org/mailman/listinfo/net-dev and start a discussion with the relevant details.


No comments: