В JDK 26 появилась долгожданная поддержка HTTP/3 в стандартном классе HttpClient. Хотя само API почти не изменилось, теперь можно явно указывать предпочтение использования HTTP/3 как на уровне клиента, так и на уровне запроса. В новом переводе от команды Spring АйО подробно описывается, как работает выбор версии HTTP, что такое Http3DiscoveryMode, как принудительно использовать HTTP/3 и как HttpClient "обучается" на основе заголовков alt-svc.
Одна из новых возможностей JDK 26 — это поддержка HTTP/3 в классе HttpClient, который входит в стандарт Java SE начиная с JDK 11.
Прежде чем перейти к подробностям API, кратко напомним, что такое HTTP/3. С точки зрения функциональности протокола HTTP, он не сильно отличается от HTTP/2. Однако основное различие заключается в транспортном уровне: если HTTP/2 работает поверх TCP, то HTTP/3 использует UDP. HTTP/3 построен на базе протокола QUIC. Для получения дополнительной информации см. JEP 517.
Использование API HttpClient
Теперь рассмотрим, как использовать поддержку HTTP/3 с API java.net.http.HttpClient. Если вы ранее не работали с этим API, хорошей отправной точкой будет его документация Javadoc.
Вкратце, приложение создает экземпляр 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(); // создаем экземпляр HttpClient
...
URI reqURI = new URI("https://www.google.com/");
HttpRequest req = HttpRequest.newBuilder().uri(reqURI).build(); // создаем экземпляр запроса
final HttpResponse.BodyHandler<String> bodyHandler = BodyHandlers.ofString(StandardCharsets.UTF_8);
HttpResponse<String> resp = client.send(req, bodyHandler); // отправляем запрос и получаем ответ в виде строки
System.out.println("status code: " + resp.statusCode() + " HTTP protocol version: " + resp.version()); // выводим код ответа и версию использованного протокола HTTP
Всё это не является новшеством, поскольку данный API существует с JDK 11. Поэтому теперь давайте посмотрим, что нового появилось в JDK 26 и как включить поддержку HTTP/3 в HttpClient.
По умолчанию HttpClient (даже в JDK 26) использует HTTP/2 в качестве предпочтительной версии HTTP при отправке запросов. Вы можете переопределить это поведение для конкретного экземпляра 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();
В этом примере HttpClient будет использовать предпочитаемую версию, указанную в HttpRequest, то есть HTTP/2, при отправке запроса. Если сервер не поддерживает HTTP/2, внутренняя реализация HttpClient автоматически понизит версию запроса до протокола HTTP/1.1, установит обмен запросом/ответом по HTTP/1.1 с сервером и передаст приложению соответствующий ответ в формате HTTP/1.1.
Такое поведение присутствовало и в предыдущих реализациях HttpClient. Новшество JDK 26 заключается во введении нового значения версии протокола: HttpClient.Version.HTTP_3. Приложения теперь могут использовать протокол HTTP/3, установив его в качестве предпочитаемой версии либо на уровне экземпляра HttpClient:
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) с целевым сервером. Если попытка установить соединение по протоколу QUIC на основе UDP завершится неудачей — либо потому что сервер не поддерживает HTTP/3, либо потому что соединение не было установлено своевременно — реализация HttpClient автоматически понизит версию протокола до HTTP/2 (поверх TCP) и попытается завершить запрос с использованием HTTP/2. Если сервер не поддерживает и HTTP/2, запрос будет дополнительно понижен до HTTP/1.1, как и раньше.
Таким образом, код приложения будет выглядеть аналогично ранее рассмотренному примеру, за исключением одного изменения — установки предпочитаемой версии HTTP/3 либо при создании экземпляра HttpClient, либо при создании экземпляра HttpRequest:
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_3)
.build(); // создаем экземпляр HttpClient с HTTP/3 в качестве предпочитаемой версии
...
URI reqURI = new URI("https://www.google.com/");
HttpRequest req = HttpRequest.newBuilder()
.uri(reqURI)
.build(); // создаем экземпляр запроса
final HttpResponse.BodyHandler<String> bodyHandler = BodyHandlers.ofString(StandardCharsets.UTF_8);
HttpResponse<String> resp = client.send(req, bodyHandler); // отправляем запрос и получаем ответ в виде строки
System.out.println("status code: " + resp.statusCode() +
" HTTP protocol version: " + resp.version()); // выводим код ответа и версию использованного протокола HTTP
Определение версии HTTP
Обратите внимание: установка HTTP/3 в качестве предпочтительной версии не гарантирует, что запрос действительно будет выполнен с использованием HTTP/3. Именно поэтому это называется «предпочтительной» версией. HttpClient не может заранее определить, поддерживает ли сервер, на который отправляется запрос, протокол HTTP/3.
Поэтому при первом запросе экземпляр HttpClient будет использовать внутренний алгоритм реализации, который предполагает попытку установить соединение по TCP (HTTP/2) или по UDP (HTTP/3) с соответствующим сервером. Тот способ соединения, который успешно установится первым, будет использован для дальнейшего общения и, соответственно, определит, какая версия протокола HTTP будет использоваться для этого запроса.
Для получения дополнительной информации об определении версии HTTP/3 см. документацию по Http3DiscoveryMode.
Имея это в виду, давайте рассмотрим несколько конкретных случаев и примеры кода, демонстрирующие использование этой функциональности.
Рассмотрим ситуацию, когда приложение хочет принудительно использовать только HTTP/3 — то есть попытаться установить соединение с сервером исключительно через QUIC (UDP) и затем отправить запрос по HTTP/3. Если это не удастся, соединение не должно быть понижено до HTTP/2. Приложения, как правило, делают это только в тех случаях, когда точно известно, что целевой сервер (указанный в URI запроса по хосту и порту) поддерживает HTTP/3 на этой комбинации хоста и порта.
Например, рассмотрим сервер google.com, к которому будет отправлен запрос. На основе предварительных тестов мы знаем, что google.com поддерживает HTTP/3 на том же хосте и порту, что и HTTP/2 (или HTTP/1.1). Вот как может выглядеть код в этом случае:
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) // настраиваем HTTP/3 как предпочтительную версию клиента
.build();
URI reqURI = new URI("https://www.google.com/");
HttpRequest req = HttpRequest.newBuilder()
.uri(reqURI)
.setOption(HttpOption.H3_DISCOVERY, Http3DiscoveryMode.HTTP_3_URI_ONLY) // указываем использовать только HTTP/3
.build();
final HttpResponse.BodyHandler<String> bodyHandler = BodyHandlers.ofString(StandardCharsets.UTF_8);
HttpResponse<String> resp = client.send(req, bodyHandler); // отправляем запрос и получаем ответ в виде строки
System.out.println("status code: " + resp.statusCode() + " HTTP protocol version: " + resp.version()); // выводим код ответа и использованную версию протокола HTTP
Помимо настройки экземпляра HttpClient с использованием version(Version.HTTP_3) в качестве предпочтительной версии, другой важной деталью в этом коде является строка, в которой мы настраиваем HttpRequest с помощью:
setOption(HttpOption.H3_DISCOVERY, Http3DiscoveryMode.HTTP_3_URI_ONLY) // указываем использовать только HTTP/3
Опция H3_DISCOVERY со значением Http3DiscoveryMode.HTTP_3_URI_ONLY сообщает экземпляру HttpClient, что данный запрос должен использовать только HTTP/3. Если это невозможно, запрос должен завершиться с ошибкой (исключением).
На этом этапе мы точно знаем (и даже продемонстрировали), что запросы к www.google.com поддерживают HTTP/3, поэтому можно безопасно принудительно задать использование 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 (early access) и запускаете этот код с помощью:
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?
Ответ — да, существуют несколько механизмов, позволяющих это. Один из них описан в стандарте HTTP Alternative Services (RFC 7838). Этот механизм, далее именуемый как Alt-Services, представляет собой стандартный способ, с помощью которого серверы могут информировать клиентов о поддерживаемых ими альтернативных службах. В RFC описано несколько методов объявления альтернативных служб. Один из самых распространённых — включение в ответ HTTP-заголовка с именем alt-svc. Значение этого заголовка содержит информацию об альтернативных службах, поддерживаемых сервером, и соответствующих комбинациях хостов и портов.
Например, заголовок ответа следующего вида:
alt-svc=h3=":443"
указывает, что сервер, к которому был выполнен HTTP-запрос, по порту 443 поддерживает протокол HTTP/3 (h3 — это обозначение ALPN для HTTP/3). Когда такой заголовок alt-svc присутствует в ответе, HttpClient распознаёт его как стандартный заголовок и сохраняет эту информацию. При следующем запросе к тому же серверу и порту HttpClient проверит свой внутренний реестр, чтобы определить, был ли ранее объявлен Alt-Service h3 для этого сервера. Если да — он попытается установить соединение через HTTP/3, используя указанный альтернативный хост и порт.
Давайте посмотрим, как это работает на практике. Как и раньше, мы настроим экземпляр HttpClient с HTTP/3 в качестве предпочтительной версии, но не будем принудительно указывать HTTP/3 на уровне HttpRequest. Затем отправим два запроса к одному и тому же URI Google с использованием одного и того же экземпляра HttpClient и проследим за его поведением.
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 использовал предпочтительную версию — HTTP/3. Это демонстрирует, что экземпляр HttpClient способен определить, поддерживает ли сервер по конкретному хосту и порту HTTP/3, и использовать эту информацию при последующих запросах, если приложение предпочитает этот протокол.
Ранее мы обсуждали, как серверы объявляют Alt-Services через заголовки ответа. Поскольку наш код получает доступ к объекту HttpResponse, давайте проверим, действительно ли www.google.com включил в ответ заголовок Alt-Service для h3.
Код выше выведет заголовки ответа, если программа будет запущена с аргументом --print-response-headers:
java Http3Usage.java --print-response-headers
В выводе видно, что первый обмен запросом/ответом прошёл по протоколу HTTP/2, а второй — по HTTP/3. Поскольку печатаются все заголовки ответа, вы увидите много строк. Среди них, если поискать строку с alt-svc, можно найти:
alt-svc=[h3=":443"; ma=2592000,h3-29=":443"; ma=2592000]
Это означает, что www.google.com ответил на запрос с включённым заголовком alt-svc, в котором указано, что он поддерживает HTTP/3 (h3) на порту 443.
Существуют и другие способы, с помощью которых экземпляр HttpClient может обнаружить поддержку HTTP/3, но они выходят за рамки данной статьи.
Заключение
Комментарий от эксперта Spring АйО Евгения Сулейманова
Что это даёт разработчику?
- Нулевая кривая обучения: тот же java.net.http.HttpClient - без сторонних библиотек и обёрток. Быстрый POC за час.
- Тонкое включение фичи: выбор HTTP_3 и режима (ANY / ALT_SVC / HTTP_3_URI_ONLY) на уровне клиента или запроса -> можно катить по фича-флагу, на процентах трафика, для отдельных эндпоинтов.
- Устойчивость в "шумных" сетях: QUIC убирает TCP head-of-line, поддерживает миграцию соединения (смена сети/адреса) - меньше "залипаний" на мобильном/Wi-Fi.
- Потенциально ниже TTFB/латентность на "дальних" трассах: 1-RTT рукопожатие + "гонка" в режиме ANY. (Не всегда быстрее - зависит от сети и политики UDP.)
- Лучшее мультиплексирование: параллельные запросы в одном соединении без взаимных блокировок (в сравнении с H2 поверх TCP на потерях).
- Простая тестируемость: можно проверить response.version() == HTTP_3, мерить время установления соединения и сравнивать с H2 в CI.
- Диагностика из коробки: включаем логи HttpClient, печатаем заголовок alt-svc, быстро понимаем, почему случился даунгрейд.
Поддержка HTTP/3 в HttpClient из Java была интегрирована в основную ветку репозитория JDK и доступна в сборках JDK 26 (early access builds).
Хотя улучшения API выглядят незначительными с точки зрения использования (так задумано), реализация поддержки QUIC, а затем и HTTP/3 поверх QUIC — это результат многолетней работы команды разработчиков JDK.
Добавлены новые тесты, а также проведено обширное ручное тестирование. Тем не менее, реализация всё ещё новая и пока что не получила широкого распространения за пределами команды разработчиков JDK.
Комментарий от эксперта Spring АйО Евгения Сулейманова
Сводная таблица по Discovery Mode.
Режим |
Суть (как работает) |
Что делает на 1-м запросе |
Фолбэк/поведение при отсутствии H3 |
Когда выбирать |
Пример кода |
ANY |
Имплементация сама выбирает способ установить соединение; обычно "гонка": параллельно пробует H3 (QUIC) и TLS/TCP и берёт то, что установится быстрее. |
Может пойти и по H3, и по H2/H1 - зависит от того, что быстрее установится; если preferredVersion=HTTP_3, клиент может дать приоритет H3. |
Разрешен автоматический даунгрейд до H2/H1 - цель: минимальная задержка/макс. доступность. |
Интернет-клиенты без прокси/файрволов; когда важен быстрый старт и "пусть выберет сам". |
builder.version(HTTP_3); req.setOption(H3_DISCOVERY, ANY); |
ALT_SVC |
Использует только Alt-Svc (RFC 7838) для нахождения H3-ендпойнта. Пока Alt-Svc не получен, общается по H2/H1. |
Первый запрос почти всегда H2/H1; после получения alt-svc: h3=... последующие - по H3. |
Если Alt-Svc не приходит, останется на H2/H1; даунгрейд допустим. |
С CDN/балансировщиками, где корректно рекламируется H3 через Alt-Svc; когда нужен "безболезненный" переход после прогрева. |
builder.version(HTTP_3); req.setOption(H3_DISCOVERY, ALT_SVC); |
HTTP_3_URI_ONLY |
Пробует только прямой H3 на origin (host:port из URI). Alt-Svc не используется. |
Немедленная попытка QUIC к origin. |
Нет даунгрейда: если H3 недоступен - запрос падает с ошибкой. |
Когда заранее известно, что origin слушает H3 на том же host:port; для жестких проверок/тестов и контролируемых систем. |
builder.version(HTTP_3); req.setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); |

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.