原文はこちら。
The original article was written by Jaikiran Pai (Principal Member of Technical Staff, Oracle).
https://inside.java/2025/10/22/http3-support/
https://jaitechwriteups.blogspot.com/2025/10/http3-support-now-available-in-java-httpclient.html
JDK 26の新機能の一つとして、JDK 11以降Java SEの一部になったHttpClientが、HTTP/3をサポートするようになりました。
APIの詳細について議論する前に、HTTP/3がどのようなものか簡単におさらいしましょう。HTTPプロトコルという観点では、機能面においてHTTP/2と大きく異なるわけではありません。しかし、根本的な違いは基盤となるトランスポートプロトコルにあります。HTTP/2がTCP上で動作するのに対し、HTTP/3はUDPを使用します。HTTP/3はQUICプロトコルを基盤として構築されています。詳細については、JEP 517をご参照ください。
JEP 517: HTTP/3 for the HTTP Client API
https://openjdk.org/jeps/517
Using the HttpClient API
それでは、java.net.http.HttpClient APIに備わったHTTP/3 サポートを活用する方法を説明します。この API を初めてご利用になる場合は、Javadocから始めることをお勧めいたします。
java.net.http (Java SE 25 & JDK 25)
https://docs.oracle.com/en/java/javase/25/docs/api/java.net.http/java/net/http/package-summary.html
まとめると、以下のようです。
- アプリケーションは
java.net.http.HttpClientインスタンスを作成し、通常はアプリケーションの存続期間中これを維持する。 - HTTPリクエストを発行する際、アプリケーションコードは
java.net.http.HttpRequestインスタンスを構築し、HttpClient.send(...)メソッドを使用してリクエストを送信し、java.net.http. HttpResponseを取得する。 - より高度な使用例では、アプリケーションが応答を待機したくない場合、
HttpClient.sendAsync(...)メソッドを使用して非同期でリクエストを送信できる。このメソッドはjava.util.concurrent.CompletableFutureを返すので、アプリケーションは後でこれを使用して関連するHttpResponseを取得できる。
HttpResponseは、レスポンス本文、HTTPレスポンスコード、使用されたプロトコルバージョンなどを取得するためのメソッドを提供します。以下は典型的な使用例です。
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
これらはすべてJDK 11から存在するAPIですので、特に新しい内容ではありません。それでは、JDK 26の新機能と、HttpClientでHTTP/3サポートを有効化する方法について見ていきましょう。
デフォルトでは、HttpClient(JDK 26 においても)はリクエスト発行時に優先HTTPバージョンとしてHTTP/2を使用します。優先バージョンを設定することで、HttpClientインスタンスごとのデフォルト動作を上書きできます。
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.build();
このコードは、発行するすべてのリクエストで優先バージョンとしてHTTP/1.1を使用するクライアントを構築します。クライアントレベルで設定されたHTTPプロトコルバージョンは、以下の例のようにHttpRequestレベルでも上書きできます。
HttpRequest req = HttpRequest.newBuilder()
.uri(reqURI)
.version(HttpClient.Version.HTTP_2)
.build();
この例では、リクエストを発行する際に、HttpRequestで指定された優先バージョン(この場合はHTTP/2)を使用します。サーバーがHTTP/2をサポートしていない場合、内部のHttpClient実装は自動的にリクエストをHTTP/1.1プロトコルにダウングレードし、サーバーとの間でHTTP/1.1リクエスト/レスポンス交換を確立し、アプリケーションに関連するHTTP/1.1レスポンスを提供します。
この動作は、以前のHttpClient実装にも存在していました。JDK 26で新たに導入されたのは、新しいプロトコルバージョン値であるHttpClient.Version.HTTP_3です。アプリケーションは、HttpClientインスタンスレベルで優先バージョンとして設定するか、特定のHttpRequestインスタンスに対して設定することで、HTTP/3 プロトコルバージョンの使用を選択できます。
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_3)
.build();
もしくは、特定の HttpRequest インスタンスに対して設定します。
HttpRequest req = HttpRequest.newBuilder()
.uri(reqURI)
.version(HttpClient.Version.HTTP_3)
.build();
いずれの場合も、HTTP_3を優先バージョンとして設定すると、HttpClient実装は、ターゲットサーバーへのUDPベースの接続(HTTP/3はUDP上で動作するため)の確立を試みます。UDPベースのQUIC接続の試行が失敗した場合(サーバーがHTTP/3をサポートしていない場合、または接続が所定時間以内に完了しない場合)、HttpClient実装は自動的にプロトコルバージョンをHTTP/2(TCP経由)にダウングレードし、HTTP/2を使用してリクエストの完了を試みます。サーバーがHTTP/2をサポートしていない場合、リクエストはこれまで通り、さらにHTTP/1.1にダウングレードされます。
したがって、アプリケーションコードは以前見たものとほぼ同様になりますが、HttpClientインスタンスまたはHttpRequestインスタンスの構築時に優先バージョンをHTTP/3に設定する点が異なります:
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
HTTP/3を優先バージョンとして設定しても、リクエストが必ずしもHTTP/3プロトコルバージョンを使用するとは限りません。これが「優先(preferred)」バージョンと呼ばれる理由です。HttpClientは、リクエスト送信先のサーバーが実際にHTTP/3をサポートしているかどうかを事前に判断できません。
したがって最初のリクエストでは、HttpClientインスタンスは内部実装固有のアルゴリズムを使用します。これは、対象サーバーに対してTCPベースの通信(HTTP/2)またはUDPベースの通信(HTTP/3)の確立を試みるものです。どちらかが最初に成功した場合、その通信モードが採用され、当該リクエストで使用されるHTTPプロトコルバージョンが決定されます。
HTTP/3バージョンの検出に関する詳細は、Http3DiscoveryModeのJavadocをご参照ください。
HttpOption.Http3DiscoveryMode (Java SE 26 & JDK 26 [build 22])
https://download.java.net/java/early_access/jdk26/docs/api/java.net.http/java/net/http/HttpOption.Http3DiscoveryMode.html
この背景を踏まえ、具体的なケースと使用方法を示すコード例をいくつか見ていきましょう。
アプリケーションが HTTP/3 の使用を強制したい場合、つまり QUIC(UDP 上)経由でのみサーバーとの通信を試み、その後 HTTP/3 リクエストを発行したい場合を考えてみましょう。そしてこの試みが失敗した場合でも、接続をHTTP/2へダウングレードしてはいけないものとします。アプリケーションがこのような処理を行うのは、通常、リクエストURIで使用されるホストとポートの組み合わせにおいて、対象サーバー(リクエストURIで使用されるホストとポートで表される)が確実にHTTP/3をサポートしている場合に限定されます。
例えば、google.com を対象サーバーとしてリクエストを発行する場合を考えてみましょう。過去の実験から、google.com は HTTP/2(または HTTP/1.1)をサポートしているのと同じホスト/ポートで HTTP/3 もサポートしていることが分かっています。この場合のコードは以下のようになります。
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
HttpClientインスタンスを優先バージョンとしてversion(Version.HTTP_3)で設定する以外に、このコードにおけるもう一つの重要な詳細は、以下の行でHttpRequestを設定している点です。
setOption(HttpOption.H3_DISCOVERY, Http3DiscoveryMode.HTTP_3_URI_ONLY) // enforce that only HTTP/3 is used
H3_DISCOVERYオプションにHttp3DiscoveryMode.HTTP_3_URI_ONLYの値を設定することで、HttpClientインスタンスに対し、このリクエストでは必ずHTTP/3を使用するよう指示します。もしそれが不可能な場合、リクエストは例外を発生して失敗します。
現時点では、www.google.comへのリクエストでHTTP/3をサポートしていることは確実なので(実際に実証済みです)、HttpRequestでは以下に示すようにHTTP/3の使用を強制できます。
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));
}
}
}
}
JDK 26 早期アクセスビルドを使用し、以下のように実行した場合、
java Http3Usage.java
以下の出力が表示されるはずです。
issuing first request: https://www.google.com/ GET
received response, status code: 200 HTTP protocol version used: HTTP_3
レスポンスのプロトコルバージョンがHTTP_3であることにご注目ください。このリクエストでは確かにHTTP/3がプロトコルバージョンとして使用されました。
次に、HttpClientにHTTP/3の使用を強制するよう指示していなかった場合の結果を確認してみましょう。 そのため、上記のコードから
.setOption(HttpOption.H3_DISCOVERY, Http3DiscoveryMode.HTTP_3_URI_ONLY)
を削除(またはコメントアウト)し、残りのコードはそのままにしておきます。
この変更を加え、再度
java Http3Usage.java
でプログラムを実行すると、以下の出力が確認できるはずです。
issuing first request: https://www.google.com/ GET
received response, status code: 200 HTTP protocol version used: HTTP_2
レスポンスのプロトコルバージョンの違いにご注目ください。HTTP/3が優先バージョンとして設定されていたにもかかわらず、リクエスト/レスポンスのやり取りにはHTTP/2が使用されました。前述の通り、これはHttpClientインスタンスが、指定されたホストとポートのサーバーが「優先」されるHTTP/3バージョンをサポートしていることを保証できないため、想定される動作です。したがって、HttpClientの実装では内部アルゴリズムが使用され、結果として最初にTCPベースの接続を確立し、それを用いてHTTP/2リクエストを発行しました。
この例をさらに掘り下げると、HttpClientが時間の経過とともに特定のホストとポートのサーバーがHTTP/3をサポートしていることを学習できるのでは?と思われる方もいらっしゃるかもしれません。
答えはYesです。これにはいくつかのメカニズムが存在します。実際、HTTP Alternative Services標準(RFC 7838)がそのようなアプローチの一つを定義しています。
RFC 7838 – HTTP Alternative Services
https://datatracker.ietf.org/doc/html/rfc7838
HTTP Alternative Services(以下Alt-Servicesと呼びます)は、サーバーがサポートする代替サービスを広告するための標準的なメカニズムです。このRFCでは、代替サービスの告知方法が複数提案されていますが、一般的な手法として、サーバーが特定のHTTPリクエストに対してalt-svcという名前のHTTPレスポンスヘッダーを返す方法があります。このレスポンスヘッダーの値には、サーバーがサポートするAlt-Servicesの詳細と、各サービスがサポートされるホスト/ポートの組み合わせを記載する必要があります。
例えば、以下の形式のalt-svcヘッダー
alt-svc=h3=":443"
は、HTTPリクエストに使用されたホスト上のサーバーがポート443でHTTP/3プロトコルをサポートしていることを示します(h3はHTTP/3サポートのためのALPN [Application-Layer Protocol Negotiation])。サーバーがこのようなレスポンスヘッダーを広告すると、HttpClientはこれを標準ヘッダーとして認識し、この詳細を記録します。したがって、次回同じサーバーとポートに対してリクエストが発行されると、HttpClientは内部レジストリを確認し、そのサーバーに対して以前にh3 Alt-Serviceが広告されていたかどうかを判断します。広告されていた場合、代替のホストとポートを使用してHTTP/3接続の確立を試みます。
この動作を実際に確認してみましょう。前回と同様に、HttpClientインスタンスをHTTP/3を優先バージョンとして設定しますが、今回はHttpRequestに対してHTTP/3を強制しません。その後、同じHttpClientインスタンスを使用して同じGoogle URIに2つのリクエストを送信し、その動作を観察します。
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));
}
}
}
}
JDK 26 EA ビルドで再度実行してみましょう。
java Http3Usage.java
このコマンドは以下を出力するはずです。
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
最初のリクエストではHTTP/2が使用され、同じリクエストURIに対して同じHttpClientインスタンスを使用した2番目のリクエストでは、優先されるHTTP/3バージョンが使用されたことにご留意ください。これは、HttpClientインスタンスが特定のホスト/ポートのサーバーがHTTP/3をサポートしているかどうかを判断し、アプリケーションがそのプロトコルバージョンを優先する場合、その知識を活用してそのサーバーに対してHTTP/3リクエストを発行できることを示しています。
先程、サーバーがレスポンスヘッダーを通じてAlt-Servicesを広告する方法について説明しました。当コードはHttpResponseにアクセス可能ですので、www.google.comが実際にレスポンスヘッダーでh3 Alt-Serviceを広告していたか確認してみましょう。
上記のコードは、プログラム引数--print-response-headersを指定して実行するとレスポンスヘッダーを出力します。
java Http3Usage.java --print-response-headers
最初のリクエスト/レスポンス交換ではHTTP/2が使用され、2回目の交換ではHTTP/3が使用されたことが確認できます。出力にはさらに多くの行が表示されています。これはHttpResponseの全レスポンスヘッダーが印刷されるためです。これらの行の中からalt-svcを検索すると、以下が見つかるはずです。
alt-svc=[h3=":443"; ma=2592000,h3-29=":443"; ma=2592000]
このように、www.google.comはリクエストに応答し、HTTP/3サポートを表すAlt-Serviceを広告するこの追加レスポンスヘッダーが含まれていました。
HttpClientインスタンスがHTTP/3サポートを検出する方法は他にもありますが、それらは本記事の範囲を超えるため割愛します。
Conclusion
JavaのHttpClientにおけるHTTP/3サポートはメインラインJDKリポジトリに統合され、JDK 26早期アクセスビルドで利用可能です。
OpenJDK JDK 26 Early-Access Builds
https://jdk.java.net/26/
APIの強化は使用方法の観点からは(設計上)些細なものに見えますが、QUICおよびそれに基づくHTTP/3のサポートは、JDK内部における数年にわたる開発努力の成果です。
新たなテストを追加し、広範な手動テストも実施しました。しかしながら、この実装はまだ新しく、JDK開発チーム以外での使用例はあまり見られません。JDKのHttpClientにおけるHTTP/3サポートに関心をお持ちでしたら、JDK 26の早期アクセスビルドを使用してアプリケーション実行や実験の上、net-dev OpenJDKメーリングリストにてフィードバックやバグ報告を提供していただけますと大変ありがたいです。
net-dev — Technical discussion about the development of the networking part of the the core libraries
https://mail.openjdk.org/mailman/listinfo/net-dev