HTTP Client Updates in Java 26

Java 26, which will be released on March 17th, will debut a number of changes to the java.net.http.HttpClient, which was originally added in JDK 11. In this article we will review these changes and how developers can take advantage of them, and what they might need to consider when upgrading to Java 26 and beyond.

HTTP/3 Support

JEP 517 adds support for HTTP/3 to the HttpClient. It’s important to note that support is only be added, but the default will remain HTTP/2, which has been the case since the HttpClient was initially added in JDK 11.

The preferred protocol version can be set at the HttpClient level:

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

And at the HttpRequest level:

var request = HttpRequest.newBuilder(URI.create("https://openjdk.org/"))
                         .version(HttpClient.Version.HTTP_3)
                         .GET().build();

The HttpClient offers options for navigating the transition between HTTP/1.1 and HTTP/2 to HTTP/3 covered below.

Connecting to HTTP/3 services

Part of the reason for not setting HTTP/3 as the default for HttpClient, is because HTTP/3 uses a new transport protocol, QUIC, an update from the TCP protocol used by HTTP/1.1 and HTTP/2, and HTTP/3 is not as widely deployed as HTTP/1.1 or HTTP/2. However while QUIC offers many advantages, the addition of a new transport protocol does bring some complications. Specifically there’s no way to know which transport protocol is being used ahead of time. To that end, below are four strategies that can be used with the HttpClient that can handle working with HTTP/1.1, HTTP/2, and HTTP/3 services.

Send a HTTP/3 Request First

HTTP/3 should be used when available, and with services migrating towards HTTP/3 an optimistic approach can be taken to attempt to connect using HTTP/3 first, and only fall back to HTTP/2 or HTTP/1.1 if a connection cannot be established. This can be done by not setting the protocol version at the client level, but setting it at the request level:

var client = HttpClient.newBuilder().build();

var request = HttpRequest.newBuilder(URI.create("https://openjdk.org/"))
                         .version(HttpClient.Version.HTTP_3)
                         .GET().build();

Send HTTP/3 and HTTP/2 or HTTP/1.1 Requests in Parallel

Sending requests in serial, like in the above example, can increase latency, espeically when a fallback is likely. Alternatively HTTP/3 and HTTP/2 or HTTP/1.1 requests can be sent in parallel, and the first one that succeeds will be used. This can be accomplished by setting the preferred version to HTTP/3 at the client level, but not setting a preferred version at the request level:

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

var request = HttpRequest.newBuilder(URI.create("https://openjdk.org/"))
                         .GET().build();

Send a HTTP/2 or HTTP/1.1 Request First and Redirect to HTTP/3 if Available

If it’s likely the service you are connecting to does not support HTTP/3, then a pessimistic approach could be taken, and send a HTTP/1.1 or HTTP/2 request first, and only switching to HTTP/3 if the serivce says its available. This can be done by setting the Http3DiscoveryMode.ALT_SVC option:

var client = HttpClient.newBuilder()
                       .build();

var request = HttpRequest.newBuilder(URI.create("https://openjdk.org/"))
						 .setOption(HttpOption.H3_DISCOVERY, Http3DiscoveryMode.ALT_SVC)
						 .GET().build();

Only Use HTTP/3

The last strategy would be to only use HTTP/3 and not fall back if the server does not reply with HTTP/3. This can be done by setting the Http3DiscoveryMode.HTTP_3_URI_ONLY option:

var client = HttpClient.newBuilder()
                       .build();

var request = HttpRequest.newBuilder(URI.create("https://openjdk.org/"))
						 .setOption(HttpOption.H3_DISCOVERY, Http3DiscoveryMode.HTTP_3_URI_ONLY)
						 .GET().build();

Uploading File Regions

A new static method, java.net.HttpRequest.BodyPublishers.ofFileChannel(FileChannel chan, long position, long size) has been added. ofFileChannel(FileChannel chan, long position, long size) allows for a specific region of a file to be uploaded. For large files, this can avoid having to read the entire file into memory, and can be leveraged to slice a file it chunks and uploaded in parallel.

https://bugs.openjdk.org/browse/JDK-8329829

Signature Schemes and Named Groups Usage Fix

During the setup of new connections, HttpClient now uses the signature schemes and named groups configured on SSLParameters, like in the example below, when negotiating the TLS handshake. Previously these configured values were ignored.

final SSLParameters sslParameters = new SSLParameters();
sslParameters.setNamedGroups(new String[]{namedGroup});

HttpClient.newBuilder()
	.sslContext(sslBundle.createSslContext())
	.sslParameters(sslParameters)
	.build();

https://bugs.openjdk.org/browse/JDK-8367112

Timeout Update

The HttpClient request timeout set using java.net.http.HttpRequest.Builder::timeout previously applied only until the response headers were received. Its scope has now been extended to also cover the consumption of the response body, if present. In most cases this should align with expected behavior of canceling a long running connection, but if your application frequently deals with large response bodies and depends upon the previous behavior, you will need to update your timeout values accordingly.

https://bugs.openjdk.org/browse/JDK-8208693

Content-Length Removed for non-POST/PUT Requests

The HttpClient will no longer send a Content-Length header on HTTP/1.1 request for non-POST/PUT methods which contain no body. This follows RFC9110 semantics. If your application depends on the Content-Length header, it can be added using HttpRequest.Builder, and by updating the jdk.httpclient.allowRestrictedHeaders property to include Content-Length.

https://bugs.openjdk.org/browse/JDK-8358942

Missing File Exception Update

Previously the java.net.http.HttpRequest.BodyPublishers::ofFile(Path) method could throw a java.io.NoSuchFileException if the provided Path was not associated with the default file system. This inconsistency has been removed by mapping these to java.io.FileNotFoundException, in line with the ofFile(Path) API specification.

https://bugs.openjdk.org/browse/JDK-8358688

Max-Age Updates

java.net.HttpCookie has been updated to correctly return the value of Max-Age when when calling getMaxAge() for cookies that have both Expires and Max-Age attributes. This follows RFC 6265 specifies that the Max-Age attribute should take precedence over the Expires attribute.

https://bugs.openjdk.org/browse/JDK-8351983

Java 26 ended up being a pretty important release for HttpClient users. With HTTP/3 support being added as well as several other updates. If you’d like to learn more about the HttpClient, and all the other features coming in Java 26 and beyond, consider attending JavaOne March 17-19th in Redwood Shores, California. Tickets are on sale now as well as the full session list at: https://javaone.com.