HTTP клиент
Модуль HTTP-клиента описывает исходящие HTTP-вызовы приложения: реализацию транспорта, преобразование запроса,
преобразование ответа, телеметрию и перехватчики. В Kora типизированные клиенты описываются декларативно через @HttpClient
и @HttpRoute, либо используется общий интерфейс HttpClient напрямую, когда запрос нужно собрать в коде.
Декларативный подход подходит для большинства интеграций с внешними службами: контракт метода становится контрактом удаленного вызова,
а Kora во время компиляции создает реализацию без использования Reflection во время работы. Императивный подход полезен для низкоуровневых
или динамических сценариев, где путь, заголовки, параметры или тело запроса удобнее собирать вручную.
Все вызовы HTTP-клиента в Kora синхронные и блокирующие: HttpClient.execute() возвращает HttpClientResponse напрямую,
а декларативный метод сразу возвращает результат. Параллелизм обеспечивается виртуальными потоками,
а не реактивными или корутинными типами возврата.
Совет
Мы советуем использовать подход, при котором первичен контракт в формате OpenAPI,
а клиенты создаются с помощью генератора.
Такой подход помогает сохранить согласованность контракта между потребителем и владельцем контракта
и быстрее обновлять клиент при изменении контракта за счет замены файла описания.
Подробнее про генератор смотрите в разделе про генерацию из OpenAPI.
Если нужен пошаговый разбор перед справочным описанием, смотрите HTTP-клиент и продвинутый HTTP-клиент.
OkHttp¶
Реализация HTTP-клиента на базе библиотеки OkHttp.
Сам модуль Kora написан на Java, но библиотека OkHttp написана на Kotlin и тянет собственные зависимости.
Этот транспорт стоит выбирать, если нужны HTTP/2 или HTTP/3, сжатие GZip либо иные специфичные для OkHttp настройки.
Подключение¶
Зависимость build.gradle:
Модуль:
Зависимость build.gradle.kts:
Модуль:
Реализацией интерфейса HttpClient выступает OkHttpClient из пакета io.koraframework.http.client.ok.
Конфигурация¶
Основные параметры конфигурации клиента OkHttp:
- Максимальное время установки соединения (по умолчанию:
5s) - Максимальное время чтения ответа (по умолчанию:
2m)
Полная конфигурация
Пример полной конфигурации, описанной в классах OkHttpClientConfig
и HttpClientConfig (указаны значения по умолчанию либо примерные значения):
httpClient {
ok {
followRedirects = true //(1)!
retryOnConnectionFailure = true //(2)!
httpVersion = "HTTP_1_1" //(3)!
}
connectTimeout = "5s" //(4)!
readTimeout = "2m" //(5)!
useEnvProxy = false //(6)!
proxy {
host = "localhost" //(7)!
port = 8090 //(8)!
user = "user" //(9)!
password = "password" //(10)!
nonProxyHosts = [ "host1", "host2" ] //(11)!
}
}
- Следовать ли HTTP-перенаправлениям (по умолчанию:
true) - Повторять ли запрос после сбоя соединения; это может влиять на максимальное время установки соединения (по умолчанию:
true) - Максимальная используемая версия протокола
HTTP, доступные значения:HTTP_1_1/HTTP_2/HTTP_3(по умолчанию:HTTP_1_1) - Максимальное время установки соединения (по умолчанию:
5s) - Максимальное время чтения ответа (по умолчанию:
2m) - Использовать ли переменные окружения
https_proxy/HTTPS_PROXY/http_proxy/HTTP_PROXYиno_proxy/NO_PROXYдля настройки прокси (по умолчанию:false) - Хост прокси-сервера (обязательный, если секция
proxyприсутствует, без значения по умолчанию) - Порт прокси-сервера (обязательный, если секция
proxyприсутствует, без значения по умолчанию) - Пользователь прокси-сервера (опционально, без значения по умолчанию)
- Пароль прокси-сервера (опционально, без значения по умолчанию)
- Хосты, которые исключаются из проксирования (опционально, без значения по умолчанию)
httpClient:
ok:
followRedirects: true #(1)!
retryOnConnectionFailure: true #(2)!
httpVersion: "HTTP_1_1" #(3)!
connectTimeout: "5s" #(4)!
readTimeout: "2m" #(5)!
useEnvProxy: false #(6)!
proxy:
host: "localhost" #(7)!
port: 8090 #(8)!
user: "user" #(9)!
password: "password" #(10)!
nonProxyHosts: [ "host1", "host2" ] #(11)!
- Следовать ли HTTP-перенаправлениям (по умолчанию:
true) - Повторять ли запрос после сбоя соединения; это может влиять на максимальное время установки соединения (по умолчанию:
true) - Максимальная используемая версия протокола
HTTP, доступные значения:HTTP_1_1/HTTP_2/HTTP_3(по умолчанию:HTTP_1_1) - Максимальное время установки соединения (по умолчанию:
5s) - Максимальное время чтения ответа (по умолчанию:
2m) - Использовать ли переменные окружения
https_proxy/HTTPS_PROXY/http_proxy/HTTP_PROXYиno_proxy/NO_PROXYдля настройки прокси (по умолчанию:false) - Хост прокси-сервера (обязательный, если секция
proxyприсутствует, без значения по умолчанию) - Порт прокси-сервера (обязательный, если секция
proxyприсутствует, без значения по умолчанию) - Пользователь прокси-сервера (опционально, без значения по умолчанию)
- Пароль прокси-сервера (опционально, без значения по умолчанию)
- Хосты, которые исключаются из проксирования (опционально, без значения по умолчанию)
Телеметрия не настраивается в секции транспорта: логирование, метрики и трассировка настраиваются для каждого
декларативного клиента по пути httpClient.<clientName>.telemetry, смотрите Конфигурацию клиента.
Метрики модуля описаны в разделе Описание метрик.
Конфигуратор¶
Построитель транспорта настраивается компонентом Configurer<OkHttpClient.Builder>.
Kora применяет его последним, уже после собственной настройки:
Configurer находится в пакете io.koraframework.common и является общим контрактом настройки всех транспортов Kora.
Apache HttpClient¶
Реализация HTTP-клиента на базе Apache HttpClient 5.
Используется классический (блокирующий) API и пул соединений, что напрямую соответствует синхронному контракту клиента Kora.
Подключение¶
Зависимость build.gradle:
Модуль:
Зависимость build.gradle.kts:
Модуль:
Реализацией интерфейса HttpClient выступает ApacheHttpClient из пакета io.koraframework.http.client.apache.
Конфигурация¶
Основные параметры конфигурации Apache HttpClient:
- Максимальное время установки соединения (по умолчанию:
5s) - Максимальное время чтения ответа, отображается на response timeout Apache (по умолчанию:
2m)
Полная конфигурация
Пример полной конфигурации, описанной в классах ApacheHttpClientConfig
и HttpClientConfig (указаны значения по умолчанию либо примерные значения):
httpClient {
apache {
followRedirects = true //(1)!
maxRedirects = 3 //(2)!
maxConnections = 1000 //(3)!
}
connectTimeout = "5s" //(4)!
readTimeout = "2m" //(5)!
useEnvProxy = false //(6)!
proxy {
host = "localhost" //(7)!
port = 8090 //(8)!
user = "user" //(9)!
password = "password" //(10)!
nonProxyHosts = [ "host1", "host2" ] //(11)!
}
}
- Следовать ли HTTP-перенаправлениям (по умолчанию:
true) - Максимальное количество перенаправлений для одного запроса (по умолчанию:
3) - Максимальное количество соединений в пуле, применяется и суммарно, и на маршрут (по умолчанию: количество доступных процессоров, умноженное на
250) - Максимальное время установки соединения (по умолчанию:
5s) - Максимальное время чтения ответа (по умолчанию:
2m) - Использовать ли переменные окружения
https_proxy/HTTPS_PROXY/http_proxy/HTTP_PROXYиno_proxy/NO_PROXYдля настройки прокси (по умолчанию:false) - Хост прокси-сервера (обязательный, если секция
proxyприсутствует, без значения по умолчанию) - Порт прокси-сервера (обязательный, если секция
proxyприсутствует, без значения по умолчанию) - Пользователь прокси-сервера (опционально, без значения по умолчанию)
- Пароль прокси-сервера (опционально, без значения по умолчанию)
- Хосты, которые исключаются из проксирования (опционально, без значения по умолчанию)
httpClient:
apache:
followRedirects: true #(1)!
maxRedirects: 3 #(2)!
maxConnections: 1000 #(3)!
connectTimeout: "5s" #(4)!
readTimeout: "2m" #(5)!
useEnvProxy: false #(6)!
proxy:
host: "localhost" #(7)!
port: 8090 #(8)!
user: "user" #(9)!
password: "password" #(10)!
nonProxyHosts: [ "host1", "host2" ] #(11)!
- Следовать ли HTTP-перенаправлениям (по умолчанию:
true) - Максимальное количество перенаправлений для одного запроса (по умолчанию:
3) - Максимальное количество соединений в пуле, применяется и суммарно, и на маршрут (по умолчанию: количество доступных процессоров, умноженное на
250) - Максимальное время установки соединения (по умолчанию:
5s) - Максимальное время чтения ответа (по умолчанию:
2m) - Использовать ли переменные окружения
https_proxy/HTTPS_PROXY/http_proxy/HTTP_PROXYиno_proxy/NO_PROXYдля настройки прокси (по умолчанию:false) - Хост прокси-сервера (обязательный, если секция
proxyприсутствует, без значения по умолчанию) - Порт прокси-сервера (обязательный, если секция
proxyприсутствует, без значения по умолчанию) - Пользователь прокси-сервера (опционально, без значения по умолчанию)
- Пароль прокси-сервера (опционально, без значения по умолчанию)
- Хосты, которые исключаются из проксирования (опционально, без значения по умолчанию)
Конфигуратор¶
Транспорт Apache принимает два конфигуратора: Configurer<RequestConfig.Builder> для настроек запроса по умолчанию
и Configurer<HttpClientBuilder> для самого клиента. Оба опциональны:
@Component
public final class SomeRequestConfigurer implements Configurer<RequestConfig.Builder> {
@Override
public RequestConfig.Builder configure(RequestConfig.Builder builder) {
return builder.setConnectionRequestTimeout(1, TimeUnit.SECONDS);
}
}
@Component
public final class SomeClientConfigurer implements Configurer<HttpClientBuilder> {
@Override
public HttpClientBuilder configure(HttpClientBuilder builder) {
return builder.setUserAgent("my-service");
}
}
@Component
class SomeRequestConfigurer : Configurer<RequestConfig.Builder> {
override fun configure(builder: RequestConfig.Builder): RequestConfig.Builder {
return builder.setConnectionRequestTimeout(1, TimeUnit.SECONDS)
}
}
@Component
class SomeClientConfigurer : Configurer<HttpClientBuilder> {
override fun configure(builder: HttpClientBuilder): HttpClientBuilder {
return builder.setUserAgent("my-service")
}
}
Java клиент¶
Реализация HTTP-клиента на базе встроенного в JDK клиента.
Kora запускает его на исполнителе виртуальных потоков, поэтому у транспорта нет собственных настроек пула потоков.
Подключение¶
Зависимость build.gradle:
Модуль:
Зависимость build.gradle.kts:
Модуль:
Реализацией интерфейса HttpClient выступает JdkHttpClient из пакета io.koraframework.http.client.jdk.
Конфигурация¶
Основные параметры конфигурации JDK HttpClient:
- Максимальное время установки соединения (по умолчанию:
5s) - Ограничение времени запроса по умолчанию, передаётся в
HttpRequest.Builder.timeout; явныйrequestTimeoutзапроса имеет приоритет (по умолчанию:2m)
Полная конфигурация
Пример полной конфигурации, описанной в классах JdkHttpClientConfig
и HttpClientConfig (указаны значения по умолчанию либо примерные значения):
httpClient {
jdk {
followRedirects = true //(1)!
httpVersion = "HTTP_1_1" //(2)!
}
connectTimeout = "5s" //(3)!
readTimeout = "2m" //(4)!
useEnvProxy = false //(5)!
proxy {
host = "localhost" //(6)!
port = 8090 //(7)!
user = "user" //(8)!
password = "password" //(9)!
nonProxyHosts = [ "host1", "host2" ] //(10)!
}
}
- Следовать ли HTTP-перенаправлениям (по умолчанию:
true) - Какую версию протокола
HTTPиспользовать, доступные значения:HTTP_1_1/HTTP_2(по умолчанию:HTTP_1_1) - Максимальное время установки соединения (по умолчанию:
5s) - Ограничение времени запроса по умолчанию, передаётся в
HttpRequest.Builder.timeout; явныйrequestTimeoutзапроса имеет приоритет (по умолчанию:2m) - Использовать ли переменные окружения
https_proxy/HTTPS_PROXY/http_proxy/HTTP_PROXYиno_proxy/NO_PROXYдля настройки прокси (по умолчанию:false) - Хост прокси-сервера (обязательный, если секция
proxyприсутствует, без значения по умолчанию) - Порт прокси-сервера (обязательный, если секция
proxyприсутствует, без значения по умолчанию) - Пользователь прокси-сервера (опционально, без значения по умолчанию)
- Пароль прокси-сервера (опционально, без значения по умолчанию)
- Хосты, которые исключаются из проксирования (опционально, без значения по умолчанию)
httpClient:
jdk:
followRedirects: true #(1)!
httpVersion: "HTTP_1_1" #(2)!
connectTimeout: "5s" #(3)!
readTimeout: "2m" #(4)!
useEnvProxy: false #(5)!
proxy:
host: "localhost" #(6)!
port: 8090 #(7)!
user: "user" #(8)!
password: "password" #(9)!
nonProxyHosts: [ "host1", "host2" ] #(10)!
- Следовать ли HTTP-перенаправлениям (по умолчанию:
true) - Какую версию протокола
HTTPиспользовать, доступные значения:HTTP_1_1/HTTP_2(по умолчанию:HTTP_1_1) - Максимальное время установки соединения (по умолчанию:
5s) - Ограничение времени запроса по умолчанию, передаётся в
HttpRequest.Builder.timeout; явныйrequestTimeoutзапроса имеет приоритет (по умолчанию:2m) - Использовать ли переменные окружения
https_proxy/HTTPS_PROXY/http_proxy/HTTP_PROXYиno_proxy/NO_PROXYдля настройки прокси (по умолчанию:false) - Хост прокси-сервера (обязательный, если секция
proxyприсутствует, без значения по умолчанию) - Порт прокси-сервера (обязательный, если секция
proxyприсутствует, без значения по умолчанию) - Пользователь прокси-сервера (опционально, без значения по умолчанию)
- Пароль прокси-сервера (опционально, без значения по умолчанию)
- Хосты, которые исключаются из проксирования (опционально, без значения по умолчанию)
В JDK-транспорте readTimeout — положительное значение, используемое как ограничение запроса по умолчанию; нулевое или отрицательное значение его отключает. Это ограничение JDK HttpRequest, а не отдельный таймер каждого чтения из возвращённого InputStream. При ручном создании JdkHttpClient передайте длительность вторым аргументом конструктора: перегрузка только с java.net.http.HttpClient не задаёт это значение.
Конфигуратор¶
Декларативный клиент¶
Для создания декларативного клиента предлагается использовать специальные аннотации:
@HttpClient- указывает, что интерфейс является декларативным HTTP-клиентом@HttpRoute- указывает тип HTTP-запроса и путь запроса
HttpMethod — это контейнер строковых констант (GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH, QUERY),
поэтому запись method = "GET" также корректна.
Интерфейс клиента может наследовать другие интерфейсы: маршруты, объявленные в родителе, тоже будут реализованы, а переопределенный в клиенте метод заменяет унаследованный маршрут.
Конфигурация клиента¶
По умолчанию конфигурация конкретной реализации @HttpClient ищется по пути httpClient.{имя класса с маленькой буквы}.
Если путь требуется указать явно, он передается значением аннотации:
В @HttpClient также можно указать теги внедряемых компонентов:
httpClientTag— тег для выбора конкретного транспортногоHttpClient, когда в графе есть несколько реализаций с разными@TagtelemetryTag— тег для выбора конкретной фабрикиHttpClientTelemetryFactory
Эти теги выбирают, какой компонент внедрить, когда в графе присутствует несколько. Вторая часть — предоставить этот компонент
под тем же @Tag. Например, чтобы дать одному клиенту выделенный транспорт (отдельный пул соединений, другие таймауты,
свой OkHttpConfigurer и т.д.), предоставьте HttpClient с тегом и сошлитесь на тот же класс-тег из httpClientTag:
public final class CustomTransport { } //(1)!
@Module
public interface TransportModule {
@Tag(CustomTransport.class) //(2)!
default HttpClient customHttpClient(okhttp3.OkHttpClient okHttp) {
return new OkHttpClient(okHttp); //(3)!
}
}
- Класс-маркер, используемый только как тег
- Предоставляется под тем же тегом, на который ссылается
httpClientTag ru.tinkoff.kora.http.client.ok.OkHttpClient— транспорт Kora, оборачивающийokhttp3.OkHttpClient
class CustomTransport //(1)!
@Module
interface TransportModule {
@Tag(CustomTransport::class) //(2)!
fun customHttpClient(okHttp: okhttp3.OkHttpClient): HttpClient {
return OkHttpClient(okHttp) //(3)!
}
}
- Класс-маркер, используемый только как тег
- Предоставляется под тем же тегом, на который ссылается
httpClientTag ru.tinkoff.kora.http.client.ok.OkHttpClient— транспорт Kora, оборачивающийokhttp3.OkHttpClient
telemetryTag работает так же для HttpClientTelemetryFactory с тегом. Если тег не указан, используются транспорт и телеметрия по умолчанию.
Основные параметры конфигурации декларативного клиента:
- Базовый
URLсервиса, куда будут отправляться запросы (обязательный, без значения по умолчанию) - Максимальное время запроса (опционально, без значения по умолчанию)
Полная конфигурация
Пример конфигурации для пути httpClient.someClient, описанной в классах DeclarativeHttpClientConfig
и HttpClientTelemetryConfig:
httpClient {
someClient {
url = "https://localhost:8090" //(1)!
requestTimeout = "10s" //(2)!
telemetry {
logging {
enabled = false //(3)!
maskQueries = [ ] //(4)!
maskHeaders = [ "authorization", "cookie", "set-cookie" ] //(5)!
pathFull = false //(6)!
maxRequestBodyLogSize = "2MiB" //(7)!
maxResponseBodyLogSize = "2MiB" //(8)!
}
metrics {
enabled = false //(9)!
slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(10)!
tags = { // (11)!
"key1" = "value1"
"key2" = "value2"
}
}
tracing {
enabled = true //(12)!
pathFull = true //(13)!
attributes = { // (14)!
"key1" = "value1"
"key2" = "value2"
}
}
}
}
}
- Базовый
URLсервиса, куда будут отправляться запросы (обязательный, без значения по умолчанию) - Максимальное время запроса: может включать разрешение
DNS, установку соединения, запись тела запроса, обработку на сервере и чтение тела ответа. Если вызову требуются перенаправления или повторы, все они должны уложиться в один такой период (опционально, без значения по умолчанию) - Включает логирование модуля (по умолчанию:
false) - Имена параметров запроса, значения которых заменяются стратегией маскирования (по умолчанию:
[]) - Имена заголовков запроса и ответа, значения которых заменяются стратегией маскирования (по умолчанию:
[ "authorization", "cookie", "set-cookie" ]) - Писать ли в лог полный путь запроса вместо шаблона маршрута; если значение не указано, полный путь пишется только на уровне
TRACE, а в остальных случаях используется шаблон (опционально, без значения по умолчанию) - Максимальный размер тела запроса, которое еще записывается в лог; тело большего размера пропускается с предупреждением (по умолчанию:
2MiB) - Максимальный размер тела ответа, которое еще записывается в лог; тело большего размера пропускается с предупреждением (по умолчанию:
2MiB) - Включает метрики модуля (по умолчанию:
false) - Настраивает SLO корзины в миллисекундах для метрик (по умолчанию:
io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Настраивает теги метрик (по умолчанию:
{}) - Включает трассировку модуля (по умолчанию:
true) - Записывать ли в span атрибут
url.fullвместо толькоurl.path(по умолчанию:true) - Настраивает атрибуты трассировки (по умолчанию:
{})
httpClient:
someClient:
url: "https://localhost:8090" #(1)!
requestTimeout: "10s" #(2)!
telemetry:
logging:
enabled: false #(3)!
maskQueries: [ ] #(4)!
maskHeaders: [ "authorization", "cookie", "set-cookie" ] #(5)!
pathFull: false #(6)!
maxRequestBodyLogSize: "2MiB" #(7)!
maxResponseBodyLogSize: "2MiB" #(8)!
metrics:
enabled: false #(9)!
slo: [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(10)!
tags: #(11)!
key1: value1
key2: value2
tracing:
enabled: true #(12)!
pathFull: true #(13)!
attributes: #(14)!
key1: value1
key2: value2
- Базовый
URLсервиса, куда будут отправляться запросы (обязательный, без значения по умолчанию) - Максимальное время запроса: может включать разрешение
DNS, установку соединения, запись тела запроса, обработку на сервере и чтение тела ответа. Если вызову требуются перенаправления или повторы, все они должны уложиться в один такой период (опционально, без значения по умолчанию) - Включает логирование модуля (по умолчанию:
false) - Имена параметров запроса, значения которых заменяются стратегией маскирования (по умолчанию:
[]) - Имена заголовков запроса и ответа, значения которых заменяются стратегией маскирования (по умолчанию:
[ "authorization", "cookie", "set-cookie" ]) - Писать ли в лог полный путь запроса вместо шаблона маршрута; если значение не указано, полный путь пишется только на уровне
TRACE, а в остальных случаях используется шаблон (опционально, без значения по умолчанию) - Максимальный размер тела запроса, которое еще записывается в лог; тело большего размера пропускается с предупреждением (по умолчанию:
2MiB) - Максимальный размер тела ответа, которое еще записывается в лог; тело большего размера пропускается с предупреждением (по умолчанию:
2MiB) - Включает метрики модуля (по умолчанию:
false) - Настраивает SLO корзины в миллисекундах для метрик (по умолчанию:
io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Настраивает теги метрик (по умолчанию:
{}) - Включает трассировку модуля (по умолчанию:
true) - Записывать ли в span атрибут
url.fullвместо толькоurl.path(по умолчанию:true) - Настраивает атрибуты трассировки (по умолчанию:
{})
Метрики и логирование выключены по умолчанию
В Kora 2.0 telemetry.metrics.enabled и telemetry.logging.enabled по умолчанию false, а telemetry.tracing.enabled — true.
При выключенных метриках ничего не падает и ничего не пишется в лог — метрика http.client.request.duration просто не появляется.
Включайте их явно для каждого клиента.
Конфигурация метода¶
Для конкретного метода часть параметров настраивается отдельно. Путь конфигурации метода складывается из пути клиента и имени метода:
если путь клиента httpClient.someClient, то итоговый путь для метода hello — httpClient.someClient.hello.
Конфигурация метода накладывается поверх конфигурации клиента: requestTimeout метода заменяет значение клиента,
а настройки телеметрии метода переопределяют только явно указанные поля.
Основные параметры конфигурации метода:
Полная конфигурация
Пример полной конфигурации метода, описанной в классе HttpClientOperationConfig.
Все поля опциональны: пропущенное поле наследует значение клиента.
httpClient {
someClient {
hello {
requestTimeout = "10s" //(1)!
telemetry {
logging {
enabled = false //(2)!
maskQueries = [ ] //(3)!
maskHeaders = [ "authorization", "cookie", "set-cookie" ] //(4)!
pathFull = false //(5)!
maxRequestBodyLogSize = "2MiB" //(6)!
maxResponseBodyLogSize = "2MiB" //(7)!
}
metrics {
enabled = false //(8)!
slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(9)!
tags = { // (10)!
"key1" = "value1"
"key2" = "value2"
}
}
tracing {
enabled = true //(11)!
pathFull = true //(12)!
attributes = { // (13)!
"key1" = "value1"
"key2" = "value2"
}
}
}
}
}
}
- Максимальное время запроса: может включать разрешение
DNS, установку соединения, запись тела запроса, обработку на сервере и чтение тела ответа. Если вызову требуются перенаправления или повторы, все они должны уложиться в один такой период (опционально, наследует значение клиента) - Включает логирование модуля (опционально, наследует значение клиента)
- Имена параметров запроса, значения которых заменяются стратегией маскирования (опционально, наследует значение клиента)
- Имена заголовков запроса и ответа, значения которых заменяются стратегией маскирования (опционально, наследует значение клиента)
- Писать ли в лог полный путь запроса вместо шаблона маршрута (опционально, наследует значение клиента)
- Максимальный размер тела запроса, которое еще записывается в лог (опционально, наследует значение клиента)
- Максимальный размер тела ответа, которое еще записывается в лог (опционально, наследует значение клиента)
- Включает метрики модуля (опционально, наследует значение клиента)
- Настраивает SLO корзины в миллисекундах для метрик (опционально, наследует значение клиента)
- Настраивает теги метрик (опционально, наследует значение клиента)
- Включает трассировку модуля (опционально, наследует значение клиента)
- Записывать ли в span атрибут
url.fullвместо толькоurl.path(опционально, наследует значение клиента) - Настраивает атрибуты трассировки (опционально, наследует значение клиента)
httpClient:
someClient:
hello:
requestTimeout: "10s" #(1)!
telemetry:
logging:
enabled: false #(2)!
maskQueries: [ ] #(3)!
maskHeaders: [ "authorization", "cookie", "set-cookie" ] #(4)!
pathFull: false #(5)!
maxRequestBodyLogSize: "2MiB" #(6)!
maxResponseBodyLogSize: "2MiB" #(7)!
metrics:
enabled: false #(8)!
slo: [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(9)!
tags: #(10)!
key1: value1
key2: value2
tracing:
enabled: true #(11)!
pathFull: true #(12)!
attributes: #(13)!
key1: value1
key2: value2
- Максимальное время запроса: может включать разрешение
DNS, установку соединения, запись тела запроса, обработку на сервере и чтение тела ответа. Если вызову требуются перенаправления или повторы, все они должны уложиться в один такой период (опционально, наследует значение клиента) - Включает логирование модуля (опционально, наследует значение клиента)
- Имена параметров запроса, значения которых заменяются стратегией маскирования (опционально, наследует значение клиента)
- Имена заголовков запроса и ответа, значения которых заменяются стратегией маскирования (опционально, наследует значение клиента)
- Писать ли в лог полный путь запроса вместо шаблона маршрута (опционально, наследует значение клиента)
- Максимальный размер тела запроса, которое еще записывается в лог (опционально, наследует значение клиента)
- Максимальный размер тела ответа, которое еще записывается в лог (опционально, наследует значение клиента)
- Включает метрики модуля (опционально, наследует значение клиента)
- Настраивает SLO корзины в миллисекундах для метрик (опционально, наследует значение клиента)
- Настраивает теги метрик (опционально, наследует значение клиента)
- Включает трассировку модуля (опционально, наследует значение клиента)
- Записывать ли в span атрибут
url.fullвместо толькоurl.path(опционально, наследует значение клиента) - Настраивает атрибуты трассировки (опционально, наследует значение клиента)
Запрос¶
В этом разделе описаны преобразования HTTP-запроса для декларативного HTTP-клиента.
Параметры запроса задаются специальными аннотациями.
Преобразование параметров¶
HttpClientParameterWriter<T> преобразует значение параметра в строку, прежде чем Kora подставит его в путь, параметр запроса,
заголовок или куки. У интерфейса один метод:
String, Integer, Long, Boolean и примитивы Java записываются напрямую и вообще не требуют писателя.
Для любого другого типа Kora ищет компонент HttpClientParameterWriter<T> по точному типу параметра.
Если параметр имеет тип Map<String, T>, писатель ищется для типа значения T; если используется Map<String, List<T>>,
он применяется к каждому элементу списка; для List<T> / Set<T> / Collection<T> — к каждому элементу коллекции.
Встроенные писатели доступны для Boolean, Short, Integer, Long, Double, Float, UUID, BigDecimal, BigInteger,
Duration, OffsetTime, OffsetDateTime, LocalTime, LocalDate, LocalDateTime, ZonedDateTime и Instant.
Типы даты и времени записываются в формате ISO. Для собственных типов нужно предоставить компонент HttpClientParameterWriter<T>:
После этого тип можно использовать в параметрах клиента:
Для перечислений есть EnumHttpClientParameterWriter из пакета io.koraframework.http.client.common.request.mapper:
он строит писатель из констант перечисления и функции преобразования, и именно его использует генератор OpenAPI.
HttpClientParameterWriter<T> was not found
Сборка падает с ошибкой No component found for dependency: HttpClientParameterWriter<T>.
Либо тип собственный и компонента-писателя нет, либо у писателя есть @Tag, которого нет у параметра.
Объявите компонент HttpClientParameterWriter<T> для этого точного типа.
Параметр пути¶
@Path - обозначает значение части пути запроса, сам параметр указывается в {кавычках} в пути,
а имя параметра указывается в value либо по умолчанию равно имени аргумента метода.
Значения пути кодируются в URL, поэтому пробел превращается в %20.
Каждому placeholder-у {name} в пути должен соответствовать параметр @Path, иначе сборка падает с ошибкой
Path template contains parameters that have no matching @Path method parameter.
Параметр запроса¶
@Query - значение параметра запроса, имя указывается в value либо по умолчанию равно имени аргумента метода.
Поддерживаются одиночные значения, List<T>, Set<T>, Collection<T>, Map<String, T> и Map<String, List<T>>.
Для нестроковых значений используется доступный HttpClientParameterWriter<T>.
Пустая коллекция отправляется как параметр без значения.
Параметры запроса можно отправлять в формате ключ-значение через Map, где ключ является именем параметра и должен быть String.
Если значением Map является список, каждый его элемент отправляется как отдельное значение того же параметра.
Если элемент списка равен null, параметр отправляется без значения.
Заголовок¶
@Header - значение заголовка запроса, имя параметра указывается в value либо по умолчанию равно имени аргумента метода.
Поддерживаются одиночные значения, List<T>, Set<T>, Collection<T>, Map<String, T> и готовый объект HttpHeaders.
Заголовки можно отправлять в формате ключ-значение через HttpHeaders или Map, где ключ является именем заголовка и должен быть String.
Для нестроковых значений используется доступный HttpClientParameterWriter<T>:
Тело запроса¶
Тело запроса указывается аргументом метода без специальных аннотаций.
Из коробки поддерживаются byte[], ByteBuffer, String, HttpBodyOutput, FormUrlEncoded и FormMultipart,
поскольку HttpClientRequestMapperModule предоставляет реализации HttpClientRequestMapper именно для этих типов.
Json¶
Чтобы указать, что тело является JSON и для него требуется внедрить JsonWriter<T>, используя тег-аннотация @Json:
Требуется модуль Json, а также наличие JsonWriter<MyBody> — обычно это достигается аннотацией @Json на самом типе.
Текстовая форма¶
Используйте FormUrlEncoded (из ru.tinkoff.kora.http.common.form) как тип аргумента тела, чтобы отправить тело с
типом содержимого application/x-www-form-urlencoded (форма данных).
Аннотации @Json или @Mapping не требуются — для этого типа в Kora есть встроенный writer.
FormUrlEncoded — это набор именованных частей, где каждая часть может содержать одно или несколько значений:
FormUrlEncoded.FormPart(String name, String value)— часть с одним значениемFormUrlEncoded.FormPart(String name, List<String> values)— часть с несколькими значениями (поле повторяется для каждого значения)new FormUrlEncoded(FormPart...)/new FormUrlEncoded(List<FormPart>)/new FormUrlEncoded(Map<String, FormPart>)— создание формы; части с одинаковым именем объединяются в одну
Объявите метод клиента с параметром FormUrlEncoded:
Пример вызова метода с такой формой выглядит так:
Бинарная форма¶
Используйте FormMultipart (из ru.tinkoff.kora.http.common.form) как тип аргумента тела, чтобы отправить тело
multipart/form-data (бинарная форма), обычно используется для загрузки файлов вместе с текстовыми полями.
Аннотации @Json или @Mapping не требуются.
FormMultipart — это список частей, создаваемых через статические фабричные методы:
FormMultipart.data(String name, String value)— текстовое полеFormMultipart.file(String name, String fileName, String contentType, byte[] content)— файловая часть, загруженная в память (fileNameиcontentTypeмогут бытьnull)FormMultipart.file(String name, String fileName, String contentType, Flow.Publisher<ByteBuffer> content)— потоковая файловая часть для больших данных, которые не следует полностью буферизовать в памятиnew FormMultipart(List<? extends FormPart>)— создание формы из частей
Объявите метод клиента с параметром FormMultipart:
Пример вызова метода с такой формой выглядит так:
var response = someClient.formMultipart(new FormMultipart(List.of(
FormMultipart.data("field1", "some data content"), //(1)!
FormMultipart.file("field2", "example1.txt", "text/plain",
"some file content".getBytes(StandardCharsets.UTF_8)) //(2)!
)));
- Текстовое поле
- Файловая часть с именем файла и типом содержимого
val response = someClient.formMultipart(
FormMultipart(
listOf(
FormMultipart.data("field1", "some data content"), //(1)!
FormMultipart.file(
"field2",
"example1.txt",
"text/plain",
"some file content".toByteArray(StandardCharsets.UTF_8)
) //(2)!
)
)
)
- Текстовое поле
- Файловая часть с именем файла и типом содержимого
Метод FormMultipart.file(String name, String fileName, HttpBodyOutput content) отправляет часть формы потоком, а не массивом байт.
Свое тело¶
Если тело требуется записать способом, отличным от стандартных механизмов,
можно использовать специальный интерфейс HttpClientRequestMapper для реализации собственной логики:
@HttpClient
public interface SomeClient {
record UserBody(String id) {}
final class UserRequestMapper implements HttpClientRequestMapper<UserBody> {
@Override
public HttpBodyOutput apply(UserBody value) {
return HttpBody.plaintext(value.id());
}
}
@HttpRoute(method = HttpMethod.POST, path = "/hello/world")
HttpResponseEntity<String> hello(@Mapping(UserRequestMapper.class) UserBody body);
}
@HttpClient
interface SomeClient {
data class UserBody(val id: String)
class UserRequestMapper : HttpClientRequestMapper<UserBody> {
override fun apply(value: UserBody): HttpBodyOutput {
return HttpBody.plaintext(value.id)
}
}
@HttpRoute(method = HttpMethod.POST, path = "/hello/world")
fun hello(@Mapping(UserRequestMapper::class) body: UserBody): HttpResponseEntity<String>
}
Пример: сериализация Protobuf
Обратите внимание, что HttpBody.of принимает сначала тип содержимого, а затем полезную нагрузку:
@HttpClient
public interface ProtobufClient {
final class ProtobufRequestMapper implements HttpClientRequestMapper<MyMessage> {
@Override
public HttpBodyOutput apply(MyMessage value) {
byte[] protobufBytes = value.toByteArray();
return HttpBody.of("application/x-protobuf", protobufBytes);
}
}
@HttpRoute(method = HttpMethod.POST, path = "/message")
void sendMessage(@Mapping(ProtobufRequestMapper.class) MyMessage message);
}
@HttpClient
interface ProtobufClient {
class ProtobufRequestMapper : HttpClientRequestMapper<MyMessage> {
override fun apply(value: MyMessage): HttpBodyOutput {
val protobufBytes = value.toByteArray()
return HttpBody.of("application/x-protobuf", protobufBytes)
}
}
@HttpRoute(method = HttpMethod.POST, path = "/message")
fun sendMessage(@Mapping(ProtobufRequestMapper::class) message: MyMessage)
}
Когда мапперу нужен @Component
Маппер, указанный в @Mapping, который является final (Java) либо не open (Kotlin) и имеет единственный публичный
конструктор без аргументов, создается самим сгенерированным клиентом — компонентом графа он быть не должен.
Любой другой маппер — с зависимостями в конструкторе, например JsonReader<T>, открытый класс или класс с несколькими
конструкторами — берется из контейнера зависимостей и потому должен быть объявлен как @Component.
Ориентироваться нужно на конструктор, а не на аннотацию над методом.
Куки¶
@Cookie - значение Cookie, имя параметра указывается в value либо по умолчанию равно имени аргумента метода.
Поддерживаются одиночные значения, List<T>, Set<T>, Collection<T>, Map<String, T> и готовый объект Cookie.
Каждая кука записывается отдельным значением заголовка Cookie в формате name=value.
Обязательные параметры¶
По умолчанию все аргументы, объявленные в методе, считаются обязательными (NotNull).
По умолчанию все аргументы метода, которые не используют синтаксис Kotlin Nullability, считаются обязательными (NotNull).
Необязательные параметры¶
Если аргумент метода необязательный, то есть его может не быть,
можно использовать аннотацию @Nullable:
@HttpClient
public interface SomeClient {
@HttpRoute(method = HttpMethod.GET, path = "/hello/world")
void hello(@Nullable @Query("queryValue") String queryValue); //(1)!
}
- Kora построена на JSpecify, поэтому рекомендуется
org.jspecify.annotations.Nullable; принимается любая аннотация с простым именемNullable.
Предполагается использование синтаксиса Kotlin Nullability и пометка такого параметра как Nullable:
Параметр запроса, заголовок или кука со значением null просто не попадают в запрос.
Ответ¶
В разделе описано преобразование HTTP-ответа для декларативного HTTP-клиента.
Тело ответа¶
Kora поставляет реализации HttpClientResponseMapper для ограниченного набора типов, все они объявлены в HttpClientResponseMapperModule:
| Тип возврата | Требует |
|---|---|
void |
ничего, тело не читается |
String |
ничего |
byte[] |
ничего |
ByteBuffer |
ничего |
HttpBodyInput |
ничего, тело остается потоком |
T с @Json |
JsonReader<T> |
HttpResponseEntity<T> |
HttpClientResponseMapper<T> для полезной нагрузки |
Either<T, E> |
по одному HttpClientResponseMapper для T и для E |
Для любого другого типа требуется собственный маппер, смотрите Свой ответ.
Json¶
Если тело требуется читать как Json, над методом нужно использовать аннотацию @Json.
Требуется модуль Json.
Ответ с метаданными¶
Если нужно прочитать тело и вдобавок получить заголовки и код статуса ответа,
предназначен HttpResponseEntity — обертка над телом ответа, которая предоставляет code(), headers() и body().
Ниже пример, аналогичный примеру с Json, но с оберткой HttpResponseEntity:
Обертку Kora строит сама из маппера полезной нагрузки, поэтому для HttpResponseEntity<Void> — обычного приема, когда от ответа
нужен только код статуса, — в графе должен быть HttpClientResponseMapper<Void>. Встроенного нет, поэтому его объявляют компонентом
и не указывают через @Mapping: с @Mapping маппер обязан произвести весь тип HttpResponseEntity<Void>,
тогда как шаблонная фабрика фреймворка ждет маппер полезной нагрузки и оборачивает его в entity сама.
@HttpClient("httpClient.userApi")
public interface UserApiClient {
@Component
final class VoidResponseMapper implements HttpClientResponseMapper<Void> {
@Override
public Void apply(HttpClientResponse response) throws IOException {
try (var body = response.body()) {
body.asInputStream().readAllBytes();
}
return null;
}
}
@HttpRoute(method = HttpMethod.DELETE, path = "/users/{userId}")
HttpResponseEntity<Void> deleteUser(@Path String userId);
}
@HttpClient("httpClient.userApi")
interface UserApiClient {
@Component
class VoidResponseMapper : HttpClientResponseMapper<Void> {
override fun apply(response: HttpClientResponse): Void? {
response.body().use { body ->
body.asInputStream().readAllBytes()
}
return null
}
}
@HttpRoute(method = HttpMethod.DELETE, path = "/users/{userId}")
fun deleteUser(@Path userId: String): HttpResponseEntity<Void>
}
Без этого компонента сборка падает с ошибкой No component found for dependency: HttpClientResponseMapper<java.lang.Void>.
Either¶
Either<T, E> описывает вызов, для которого неуспешный код статуса является нормальным исходом, а не исключением.
Ответ 2xx Kora преобразует маппером типа T в Either.Left, любой другой код статуса — маппером типа E в Either.Right,
и HttpClientResponseException для такого метода никогда не бросается.
@HttpClient
public interface SomeClient {
record Success(String id) {}
record Error(String message) {}
@HttpRoute(method = HttpMethod.GET, path = "/users/{id}")
Either<@Json Success, @Json Error> get(@Path String id); //(1)!
}
@Jsonздесь используется как аннотация над типом, поэтому успешную и ошибочную полезные нагрузки можно помечать независимо
@HttpClient
interface SomeClient {
data class Success(val id: String)
data class Error(val message: String)
@HttpRoute(method = HttpMethod.GET, path = "/users/{id}")
fun get(@Path id: String): Either<@Json Success, @Json Error> //(1)!
}
@Jsonздесь используется как аннотация над типом, поэтому успешную и ошибочную полезные нагрузки можно помечать независимо
Either предоставляет isLeft() / isRight(), nullable-аксессоры left() / right() и fold(leftMapper, rightMapper), который сводит любую из сторон к одному результату (см. Either).
Также поддерживается HttpResponseEntity<Either<T, E>>, когда дополнительно нужны код статуса и заголовки.
Свой ответ¶
Если ответ нужно прочитать иначе, можно использовать специальный интерфейс HttpClientResponseMapper:
@HttpClient
public interface SomeClient {
record MyResponse(String name) { }
final class ResponseMapper implements HttpClientResponseMapper<MyResponse> {
@Override
public MyResponse apply(HttpClientResponse response) throws IOException, HttpClientDecoderException {
try (var is = response.body().asInputStream()) {
final byte[] bytes = is.readAllBytes();
var body = new String(bytes, StandardCharsets.UTF_8);
return new MyResponse(body);
}
}
}
@Mapping(ResponseMapper.class)
@HttpRoute(method = HttpMethod.GET, path = "/hello/world")
MyResponse hello();
}
@HttpClient
interface SomeClient {
data class MyResponse(val name: String)
class ResponseMapper : HttpClientResponseMapper<MyResponse> {
override fun apply(response: HttpClientResponse): MyResponse {
response.body().asInputStream().use {
val bytes: ByteArray = it.readAllBytes()
val body = String(bytes, StandardCharsets.UTF_8)
return MyResponse(body)
}
}
}
@Mapping(ResponseMapper::class)
@HttpRoute(method = HttpMethod.GET, path = "/hello/world")
fun hello(): MyResponse
}
Маппер из @Mapping обрабатывает любой код статуса
Если у метода объявлен @Mapping, Kora перестает проверять успешность кода статуса и передает мапперу любой ответ,
включая 4xx и 5xx. Если неуспешный код должен оставаться ошибкой, бросайте исключение из маппера сами.
Аннотация @Tag над методом лишь выбирает, какой компонент HttpClientResponseMapper будет внедрен: проверка на 2xx
при этом сохраняется, и неуспешный код по-прежнему приводит к HttpClientResponseException.
Пример: обработка ошибок в маппере
@HttpClient
public interface ApiClient {
record ApiResponse(String status, String data) {}
@Component
final class SafeResponseMapper implements HttpClientResponseMapper<ApiResponse> {
private final JsonReader<ApiResponse> jsonReader;
public SafeResponseMapper(JsonReader<ApiResponse> jsonReader) {
this.jsonReader = jsonReader;
}
@Override
public ApiResponse apply(HttpClientResponse response) throws IOException {
int code = response.code();
final byte[] body;
try (var is = response.body().asInputStream()) {
body = is.readAllBytes();
}
if (code >= 400) {
// Handle error: log or throw exception
throw new HttpClientResponseException(code, response.headers(), body);
}
if (body.length == 0) {
return null;
}
}
}
@HttpRoute(method = HttpMethod.GET, path = "/api/data")
@Mapping(SafeResponseMapper.class)
ApiResponse getData();
}
@HttpClient
interface ApiClient {
data class ApiResponse(val status: String, val data: String)
@Component
class SafeResponseMapper(
private val jsonReader: JsonReader<ApiResponse>
) : HttpClientResponseMapper<ApiResponse> {
override fun apply(response: HttpClientResponse): ApiResponse {
val code = response.code()
val body = response.body().asInputStream().use { it.readAllBytes() }
if (code >= 400) {
// Handle error: log or throw exception
throw HttpClientResponseException(code, response.headers(), body)
}
if (body.isEmpty()) {
return null
}
}
}
@HttpRoute(method = HttpMethod.GET, path = "/api/data")
@Mapping(SafeResponseMapper::class)
fun getData(): ApiResponse
}
Этот маппер принимает JsonReader в конструкторе, поэтому он является компонентом графа и помечен @Component.
Ошибка ответа¶
По умолчанию, когда не указаны ни маппер в @Mapping, ни @ResponseCodeMapper,
преобразование применяется только для кодов ответа 2xx.
Для всех остальных кодов бросается HttpClientResponseException. Он содержит код ответа HTTP, тело ответа и заголовки ответа.
Исключение из этого правила — Either<T, E> и HttpResponseEntity<Either<T, E>>: они преобразуют любой код статуса и никогда не бросают исключение.
Исключения клиента¶
Все стандартные исключения HTTP-клиента наследуются от HttpClientException, который является RuntimeException.
Это позволяет ловить как конкретный тип ошибки, так и все ошибки клиента одним общим типом:
try {
client.getUser("123");
} catch (HttpClientResponseException e) {
var code = e.getCode();
var headers = e.getHeaders();
var body = e.getBytes();
} catch (HttpClientException e) {
throw e;
}
Основные типы исключений:
HttpClientResponseException— ответ получен, но его код не был обработан как успешный. СодержитgetCode(),getHeaders()иgetBytes().HttpClientTimeoutException— истекло время ожидания запроса или чтения.HttpClientConnectionException— ошибка при установке или поддержании соединения с удаленным хостом, в том числе истекшее время ожидания соединения.HttpClientEncoderException— ошибка при преобразовании пользовательского значения в тело запроса.HttpClientDecoderException— ошибка при преобразовании тела ответа в пользовательский тип.HttpClientUnknownException— прочая ошибка транспортного клиента, не попавшая в более конкретную категорию.
HttpClientResponseException создается методом HttpClientResponseException.fromResponse(response) после чтения тела ответа.
Если тело уже полностью буферизовано, оно сохраняется целиком; иначе в getBytes() попадают только первые 4096 байт,
чтобы неуспешный вызов не буферизовал произвольно большую страницу ошибки.
Преобразование по коду¶
Если требуются разные преобразования в зависимости от кода статуса HTTP ответа,
можно использовать аннотацию @ResponseCodeMapper, чтобы задать соответствие между кодом статуса HTTP и обработчиком HttpClientResponseMapper.
Также можно использовать ResponseCodeMapper.DEFAULT, чтобы задать поведение по умолчанию для всех неперечисленных кодов HTTP.
Если для кода указан mapper, используется именно этот HttpClientResponseMapper.
Если указан type, Kora подбирает маппер ответа для этого типа, а затем приводит результат к типу возврата метода.
Это удобно для закрытых иерархий ответов, где разным статусам HTTP соответствуют разные подтипы результата.
Если не указано ни то, ни другое, Kora запрашивает у графа HttpClientResponseMapper для типа возврата метода
(HttpClientResponseMapper<Void> для метода с void).
Код статуса, который не перечислен и для которого нет записи DEFAULT, по-прежнему приводит к HttpClientResponseException.
@HttpClient
public interface SomeClient {
record UserResponse(UserResponse.Payload payload, UserResponse.Error error) {
public record Error(int code, String message) {}
public record Payload(String message) {}
}
@ResponseCodeMapper(code = ResponseCodeMapper.DEFAULT, mapper = ResponseErrorMapper.class)
@ResponseCodeMapper(code = 200, mapper = ResponseSuccessMapper.class)
@HttpRoute(method = HttpMethod.GET, path = "/hello/world")
UserResponse hello();
}
@HttpClient
interface SomeClient {
data class UserResponse(val payload: Payload?, val error: Error?) {
data class Error(val code: Int, val message: String)
data class Payload(val message: String)
}
@ResponseCodeMapper(code = ResponseCodeMapper.DEFAULT, mapper = ResponseErrorMapper::class)
@ResponseCodeMapper(code = 200, mapper = ResponseSuccessMapper::class)
@HttpRoute(method = HttpMethod.GET, path = "/hello/world")
fun hello(): UserResponse
}
В примере выше для кода статуса 200 будет использован ResponseSuccessMapper,
а для всех остальных кодов статуса — ResponseErrorMapper.
Пример с параметром type:
@HttpClient
public interface SomeClient {
sealed interface UserResponse permits Success, Error {}
record Success(String id) implements UserResponse {}
record Error(String message) implements UserResponse {}
@Json
@ResponseCodeMapper(code = 200, type = Success.class)
@ResponseCodeMapper(code = 404, type = Error.class)
@HttpRoute(method = HttpMethod.GET, path = "/users/{id}")
UserResponse get(@Path String id);
}
@HttpClient
interface SomeClient {
sealed interface UserResponse
data class Success(val id: String) : UserResponse
data class Error(val message: String) : UserResponse
@Json
@ResponseCodeMapper(code = 200, type = Success::class)
@ResponseCodeMapper(code = 404, type = Error::class)
@HttpRoute(method = HttpMethod.GET, path = "/users/{id}")
fun get(@Path id: String): UserResponse
}
Если указанный type нельзя присвоить типу возврата метода, Kora трактует результат маппера как исключение и бросает его
вместо возврата — так ветку ошибки можно смоделировать как выбрасываемое исключение для конкретного кода статуса.
Сигнатуры¶
Методы декларативного HTTP-клиента блокирующие:
Под T подразумевается тип возвращаемого значения. Это может быть тип тела (void, String, byte[], тип с @Json и т.д.) либо
HttpResponseEntity<T> для чтения также статуса и заголовков. Возврат @Nullable T допускает пустое успешное тело.
T myMethod()void myMethod()
Под T подразумевается тип возвращаемого значения — T, T? (nullable для пустого успешного тела) или T?, либо Unit.
T может быть типом тела либо HttpResponseEntity<T> для чтения также статуса и заголовков.
myMethod(): T
По умолчанию ответ со статусом не 2xx бросает HttpClientResponseException независимо от сигнатуры; используйте @ResponseCodeMapper или HttpResponseEntity, чтобы обрабатывать другие статусы без исключения.
Асинхронные сигнатуры не поддерживаются
В Kotlin метод клиента с suspend приводит к ошибке компиляции: Suspend methods are not supported by the HTTP client generator.
В Java тип возврата CompletionStage<T> или Mono<T> дает только предупреждение Method has async signature, this might not work correctly —
сгенерированный код все равно выполняет блокирующий вызов, и такой тип не будет удовлетворен.
Независимые вызовы выполняйте параллельно на виртуальных потоках, например через StructuredTaskScope:
try (var scope = StructuredTaskScope.open(StructuredTaskScope.Joiner.<Object>awaitAllSuccessfulOrThrow())) {
var profile = scope.fork(() -> profileHttpClient.getProfile(userId));
var recommendations = scope.fork(() -> recommendationsHttpClient.getForUser(userId));
scope.join();
return new Dashboard(profile.get(), recommendations.get());
}
Перехватчики¶
Для изменения или расширения поведения можно создавать перехватчики через интерфейс HttpClientInterceptor.
Перехватчики подключаются аннотацией @InterceptWith — либо к конкретному методу, либо ко всему интерфейсу @HttpClient.
Kora предоставляет готовые перехватчики авторизации, а собственный перехватчик можно реализовать самостоятельно, см. Свой перехватчик.
public interface HttpClientInterceptor {
HttpClientResponse processRequest(InterceptChain chain, HttpClientRequest request) throws Exception; //(1)!
interface InterceptChain {
HttpClientResponse process(HttpClientRequest request) throws Exception; //(2)!
}
}
- Вызывается для каждого запроса перехватываемого метода
- Передает запрос дальше по цепочке и возвращает ответ
Запрос неизменяемый, поэтому измененный запрос создается через request.toBuilder().
Для императивного HttpClient перехватчик подключается через httpClient.with(interceptor) вместо @InterceptWith.
Свой перехватчик¶
Перехватчик на метод:
@HttpClient
public interface SomeClient {
@Component
final class MethodInterceptor implements HttpClientInterceptor {
private final Component1 component1;
public MethodInterceptor(Component1 component1) {
this.component1 = component1;
}
@Override
public HttpClientResponse processRequest(InterceptChain chain, HttpClientRequest request) throws Exception {
component1.doSomething();
return chain.process(request);
}
}
@InterceptWith(MethodInterceptor.class)
@HttpRoute(method = HttpMethod.GET, path = "/hello/world")
void hello();
}
@HttpClient
interface SomeClient {
@Component
class MethodInterceptor(val component1: Component1) : HttpClientInterceptor {
override fun processRequest(
chain: HttpClientInterceptor.InterceptChain,
request: HttpClientRequest
): HttpClientResponse {
component1.doSomething()
return chain.process(request)
}
}
@InterceptWith(MethodInterceptor::class)
@HttpRoute(method = HttpMethod.GET, path = "/hello/world")
fun hello()
}
Перехватчик берется из контейнера зависимостей, поэтому для него действует то же правило, что и для маппера:
@Component нужен тогда, когда у него есть зависимости в конструкторе.
У @InterceptWith также есть атрибут tag для выбора реализации перехватчика по тегу.
Пример: добавление заголовка
public final class RequestIdInterceptor implements HttpClientInterceptor {
@Override
public HttpClientResponse processRequest(InterceptChain chain, HttpClientRequest request) throws Exception {
var modified = request.toBuilder()
.header("x-request-id", UUID.randomUUID().toString())
.build();
return chain.process(modified);
}
}
class RequestIdInterceptor : HttpClientInterceptor {
override fun processRequest(
chain: HttpClientInterceptor.InterceptChain,
request: HttpClientRequest
): HttpClientResponse {
val modified = request.toBuilder()
.header("x-request-id", UUID.randomUUID().toString())
.build()
return chain.process(modified)
}
}
Порядок выполнения перехватчиков:
Перехватчики, объявленные на клиенте, выполняются раньше перехватчиков, объявленных на методе, а в пределах одного элемента — в порядке объявления. Каждый перехватчик может:
- Изменить запрос перед отправкой
- Вызвать следующий перехватчик в цепочке (
chain.process(request)) - Изменить или проанализировать полученный ответ
- Бросить исключение и прервать цепочку
Request → Client interceptors → Method interceptors → Telemetry → HTTP Server
Response ← Client interceptors ← Method interceptors ← Telemetry ← HTTP Server
Перехватчик на клиент¶
Если перехватчик должен применяться ко всем методам клиента, @InterceptWith указывается на интерфейсе:
Если перехватчики указаны и на клиенте, и на методе, для такого вызова применяются оба набора.
Реестра перехватчиков HTTP-клиента на все приложение не существует: перехватчик действует только там, где он назван в @InterceptWith.
Авторизация¶
Kora предоставляет из коробки перехватчики для авторизации Basic/ApiKey/Bearer/OAuth.
Basic¶
Требуется настроить перехватчик и конфигурацию для авторизации Basic:
@Module
public interface BasicAuthModule {
@ConfigSource("openapiAuth.basicAuth")
interface BasicAuthConfig {
String username();
String password();
}
default BasicAuthHttpClientInterceptor basicAuther(BasicAuthConfig config) {
return new BasicAuthHttpClientInterceptor(config.username(), config.password());
}
}
@Module
interface BasicAuthModule {
@ConfigSource("openapiAuth.basicAuth")
interface BasicAuthConfig {
fun username(): String
fun password(): String
}
fun basicAuther(config: BasicAuthConfig): BasicAuthHttpClientInterceptor {
return BasicAuthHttpClientInterceptor(config.username(), config.password())
}
}
Конструктор с двумя аргументами оборачивает учетные данные в BasicAuthHttpClientTokenProvider.
Если правила получения секретов отличаются, в конструктор можно передать собственную реализацию HttpClientTokenProvider.
Затем перехватчик добавляется на весь HTTP-клиент либо на конкретные методы.
ApiKey¶
Требуется настроить перехватчик и конфигурацию для авторизации ApiKey.
ApiKeyLocation поддерживает значения HEADER, QUERY и COOKIE:
@Module
public interface ApiKeyAuthModule {
@ConfigSource("openapiAuth.apiKeyAuth")
interface ApiKeyAuthConfig {
String apiKey();
}
default ApiKeyHttpClientInterceptor apiKeyAuther(ApiKeyAuthConfig config) {
return new ApiKeyHttpClientInterceptor(ApiKeyLocation.HEADER, "X-API-KEY", config.apiKey());
}
}
Затем перехватчик добавляется на весь HTTP-клиент либо на конкретные методы.
Bearer¶
Требуется настроить перехватчик для авторизации Bearer:
Предоставление токена Bearer реализуется самостоятельно через собственную реализацию HttpClientTokenProvider
либо используется конструктор, принимающий статический Bearer Token.
public interface HttpClientTokenProvider {
@Nullable
String getToken(HttpClientRequest request); //(1)!
}
- Если вернуть
null, запрос остается без изменений и заголовокAuthorizationне добавляется
Затем перехватчик добавляется на весь HTTP-клиент либо на конкретные методы.
OAuth¶
Авторизация через OAuth аналогична Bearer:
нужно самостоятельно реализовать HttpClientTokenProvider и поместить его в контейнер зависимостей.
Предоставление токена¶
HttpClientTokenProvider — интерфейс для динамического получения токенов авторизации.
Используется, когда токен требуется обновлять либо получать из внешнего источника (например, из token endpoint OAuth2).
Метод блокирующий, поэтому токен можно получать прямо в нем.
Пример реализации:
@Component
public final class MyTokenProvider implements HttpClientTokenProvider {
private final OAuthClient oauthClient;
private volatile String cachedToken;
private volatile long tokenExpiry;
public MyTokenProvider(OAuthClient oauthClient) {
this.oauthClient = oauthClient;
}
@Override
public String getToken(HttpClientRequest request) {
if (cachedToken != null && System.currentTimeMillis() < tokenExpiry) {
return cachedToken;
}
var response = oauthClient.refreshToken();
this.cachedToken = response.accessToken();
this.tokenExpiry = System.currentTimeMillis() + response.expiresIn() * 1000;
return this.cachedToken;
}
}
@Component
class MyTokenProvider(
private val oauthClient: OAuthClient
) : HttpClientTokenProvider {
@Volatile
private var cachedToken: String? = null
@Volatile
private var tokenExpiry: Long = 0
override fun getToken(request: HttpClientRequest): String? {
val token = cachedToken
if (token != null && System.currentTimeMillis() < tokenExpiry) {
return token
}
val response = oauthClient.refreshToken()
cachedToken = response.accessToken()
tokenExpiry = System.currentTimeMillis() + response.expiresIn() * 1000
return cachedToken
}
}
Использование с BearerAuthHttpClientInterceptor:
Генератор OpenAPI ожидает тот же интерфейс, помеченный сгенерированным классом-маркером ApiSecurity.
Обработка исключений¶
Во время HTTP-запросов могут возникать разные исключения. Все они наследуются от базового HttpClientException,
который является непроверяемым RuntimeException из пакета io.koraframework.http.client.common.exception.
Иерархия исключений:
HttpClientException
├── HttpClientTimeoutException
├── HttpClientConnectionException
├── HttpClientResponseException
├── HttpClientEncoderException
├── HttpClientDecoderException
└── HttpClientUnknownException
Пример обработки:
@Component
public final class SomeService {
private final SomeClient client;
public SomeService(SomeClient client) {
this.client = client;
}
public void call() {
try {
client.hello();
} catch (HttpClientTimeoutException e) {
// Timeout: log, retry
} catch (HttpClientConnectionException e) {
// Connection error: check service availability
} catch (HttpClientResponseException e) {
// Response error: code, body, headers
int code = e.getCode();
byte[] body = e.getBytes();
var headers = e.getHeaders();
} catch (HttpClientEncoderException e) {
// Serialization error: validate data
} catch (HttpClientDecoderException e) {
// Deserialization error: log
} catch (HttpClientUnknownException e) {
// Unknown error: e.getCause()
}
}
}
@Component
class SomeService(
private val client: SomeClient
) {
fun call() {
try {
client.hello()
} catch (e: HttpClientTimeoutException) {
// Timeout: log, retry
} catch (e: HttpClientConnectionException) {
// Connection error: check service availability
} catch (e: HttpClientResponseException) {
// Response error: code, body, headers
val code = e.code
val body = e.bytes
val headers = e.headers
} catch (e: HttpClientEncoderException) {
// Serialization error: validate data
} catch (e: HttpClientDecoderException) {
// Deserialization error: log
} catch (e: HttpClientUnknownException) {
// Unknown error: e.cause
}
}
}
Время ожидания¶
Бросается, когда запрос превысил настроенное время ожидания (requestTimeout или readTimeout).
Соединение, которое не удалось установить за connectTimeout, вместо этого сообщается как HttpClientConnectionException.
Причины:
- Сервер не ответил за
requestTimeout - Превышено время чтения ответа (
readTimeout; в JDK-транспорте задаёт ограничение запроса по умолчанию, если не указанrequestTimeout) - Сетевые задержки
Рекомендации:
- Настраивайте адекватные таймауты в конфигурации, на клиент и на метод
- Применяйте повторы для временных сбоев
- Используйте circuit breaker для защиты от каскадных отказов
Ошибка соединения¶
Бросается, когда не удается установить соединение с сервером.
Каждый транспорт сообщает о неудачном подключении, включая истекший connectTimeout, как о HttpClientConnectionException.
Причины:
- Ошибка разрешения DNS
- Сервер недоступен (порт закрыт, firewall)
- Соединение отклонено
- Превышено время установки соединения (
connectTimeout) - Ошибка SSL/TLS handshake (OkHttp и Apache HttpClient)
Как каждый транспорт отображает ошибки ввода-вывода:
| Транспорт | HttpClientConnectionException |
HttpClientTimeoutException |
|---|---|---|
| OkHttp | Любое другое IOException, включая таймаут подключения |
InterruptedIOException с сообщением timeout, которое OkHttp бросает по таймауту вызова requestTimeout |
| Apache HttpClient | ConnectTimeoutException и любое другое IOException |
SocketTimeoutException (requestTimeout или readTimeout) |
| JDK | ConnectException, HttpConnectTimeoutException, ProtocolException |
HttpTimeoutException (requestTimeout) |
В JDK-транспорте любое другое IOException (например, ошибка TLS handshake) приводит к одному повтору запроса, если его тело еще не было отправлено.
Если тело уже отправлено или повтор завершился ошибкой, отличной от таймаута подключения, ошибки протокола или таймаута запроса, вызов завершается HttpClientUnknownException.
Рекомендации:
- Проверяйте доступность сервиса (health check)
- Используйте переключение на резервный сервис
- Настраивайте повторы с экспоненциальной задержкой
Ошибка клиента и сервера¶
Бросается, когда сервер вернул код статуса HTTP вне диапазона 2xx, а метод не объявляет собственный маппер
через @Mapping или @ResponseCodeMapper и не возвращает Either.
Доступные данные:
getCode()— код статуса HTTP (400, 404, 500 и т.д.)getBytes()— тело ответа, обрезанное до4096байт, если тело не было полностью буферизованоgetHeaders()— заголовки ответа
Рекомендации:
- Используйте
@ResponseCodeMapperдля собственной обработки статусов - Используйте
Either<T, E>, когда неуспешный код является нормальным исходом - Логируйте код и тело для отладки
- Разделяйте клиентские (4xx) и серверные (5xx) ошибки
Ошибка запроса¶
Бросается при ошибке сериализации тела запроса: HttpClientRequestMapper тела бросил исключение.
Причины:
- Ошибка сериализации JSON или бинарного формата
- Некорректные данные в объекте запроса
- Сам маппер не справился со значением
Рекомендации:
- Проверяйте данные перед отправкой
- Убедитесь, что тип тела помечен
@Jsonи для него естьJsonWriter - Логируйте исходное исключение в
cause
Ошибка ответа¶
Бросается при ошибке десериализации тела ответа: HttpClientResponseMapper метода бросил исключение.
Причины:
- Некорректный JSON в ответе сервера
- Несовпадение схемы (сервер вернул неожиданные поля)
- Поток был закрыт или оборван
Рекомендации:
- Проверяйте совместимость версий API
- Логируйте тело ответа для отладки
- Используйте
@ResponseCodeMapperдля обработки иначе устроенных тел ошибок
Ошибка неизвестная¶
Бросается при ошибке, не попадающей в остальные категории, включая любое проверяемое исключение, вышедшее из транспорта.
Доступные данные:
getCause()— исходное исключение
Рекомендации:
- Всегда логируйте
causeдля диагностики - Смотрите логи HTTP-клиента на уровне DEBUG/TRACE
- Заводите баг, если исключение воспроизводится
Устойчивость¶
Приведённые выше рекомендации (повтор, circuit breaker, таймаут, fallback) предоставляются модулем Resilient, а не самим HTTP-клиентом.
Его аннотации применяются напрямую к методам декларативного @HttpClient, поэтому отказоустойчивость можно добавить, не меняя места вызова:
@Retryable— повторить вызов при ошибке@CircuitBreakable— перестать вызывать сбоящую зависимость и быстро завершать вызовы ошибкой до её восстановления@Timeout— ограничить общее время вызова@Fallback— вернуть запасной результат при ошибке вызова
@RetrySpec("resilient.retry.someClient") //(1)!
public interface SomeClientRetry extends Retry { }
@CircuitBreakerSpec("resilient.circuitbreaker.someClient")
public interface SomeClientCircuitBreaker extends CircuitBreaker { }
@HttpClient
public interface SomeClient {
@Retryable(SomeClientRetry.class) //(2)!
@CircuitBreakable(SomeClientCircuitBreaker.class)
@HttpRoute(method = HttpMethod.GET, path = "/hello/world")
HttpResponseEntity<String> hello();
}
- Интерфейс спецификации, привязанный к пути конфигурации повтора
- Аспект ссылается на тип спецификации, см. Спецификации
@RetrySpec("resilient.retry.someClient") //(1)!
interface SomeClientRetry : Retry
@CircuitBreakerSpec("resilient.circuitbreaker.someClient")
interface SomeClientCircuitBreaker : CircuitBreaker
@HttpClient
interface SomeClient {
@Retryable(SomeClientRetry::class) //(2)!
@CircuitBreakable(SomeClientCircuitBreaker::class)
@HttpRoute(method = HttpMethod.GET, path = "/hello/world")
fun hello(): HttpResponseEntity<String>
}
- Интерфейс спецификации, привязанный к пути конфигурации повтора
- Аспект ссылается на тип спецификации, см. Спецификации
Обратите внимание на отличие от транспортного requestTimeout (Конфигурация клиента): requestTimeout ограничивает одну HTTP-попытку,
тогда как @Timeout ограничивает весь вызов метода, включая повторы. Конфигурацию и семантику каждой аннотации см. в модуле Resilient.
Клиент императивный¶
Базовый клиент представлен интерфейсом HttpClient и доступен для внедрения из любого модуля транспорта:
public interface HttpClient {
HttpClientResponse execute(HttpClientRequest request) throws HttpClientException; //(1)!
default HttpClient with(HttpClientInterceptor interceptor); //(2)!
}
- Выполняет запрос и возвращает ответ; ответ обязательно нужно закрыть
- Возвращает новое представление
HttpClientс дополнительным перехватчиком поверх
Ответ держит открытый поток тела, поэтому его нужно закрывать — используйте блок try-with-resources:
var request = HttpClientRequest.post("http://localhost:8090/pets/{petId}")
.pathParam("petId", "1")
.queryParam("page", 1)
.header("token", "12345")
.body(HttpBody.plaintext("refresh"))
.build();
try (var response = httpClient.execute(request)) {
var code = response.code();
var body = new String(response.body().asInputStream().readAllBytes(), StandardCharsets.UTF_8);
}
val request = HttpClientRequest.post("http://localhost:8090/pets/{petId}")
.pathParam("petId", "1")
.queryParam("page", 1)
.header("token", "12345")
.body(HttpBody.plaintext("refresh"))
.build()
httpClient.execute(request).use { response ->
val code = response.code()
val body = String(response.body().asInputStream().readAllBytes(), StandardCharsets.UTF_8)
}
Построитель запроса¶
HttpClientRequestBuilder позволяет собирать HTTP-запросы вручную и создаётся через HttpClientRequest.of(method, uri). Построитель создается одним из фабричных методов
HttpClientRequest — get, head, post, put, delete, connect, options, trace, patch
или of(method, uriTemplate), — а готовый запрос можно превратить обратно в построитель через request.toBuilder().
| Метод | Описание |
|---|---|
pathParam(String name, String \| int \| long \| UUID value) |
Подставляет значение вместо placeholder-а {name} в шаблоне URI |
queryParam(String name) |
Добавляет параметр запроса без значения |
queryParam(String name, String \| int \| long \| boolean \| UUID \| Collection<?> value) |
Добавляет значение параметра запроса |
queryParamRemove(String name) |
Удаляет все значения параметра запроса |
header(String name, String \| List<String> value) |
Устанавливает заголовок запроса |
headerRemove(String name) |
Удаляет заголовок запроса |
requestTimeout(Duration \| int millis) |
Переопределяет время ожидания для этого запроса |
body(HttpBodyOutput body) |
Устанавливает тело запроса |
build() |
Собирает неизменяемый HttpClientRequest |
У собранного HttpClientRequest доступны method(), uri(), uriTemplate(), headers(), body() и requestTimeout().
Именно uriTemplate() телеметрия использует как имя операции — поэтому шаблонные пути удерживают низкую кардинальность метрик и трассировок.
Построитель URI¶
UriQueryBuilder — низкоуровневый помощник, которым сгенерированные декларативные клиенты собирают строку запроса.
Он добавляет параметры по порядку и сам расставляет разделители ? и &:
var query = new UriQueryBuilder(true, false); //(1)!
query.add("page", "1"); //(2)!
query.add("sort", "name age"); //(3)!
query.add("debug"); //(4)!
String uri = "/api/users" + query.build();
// /api/users?page=1&sort=name+age&debug
- Первый аргумент: начинать строку с
?; второй: начинать с&, если базовый путь уже заканчивается параметром запроса - Добавляет пару
name=value, обе части кодируются в URL - Значения кодируются в URL, поэтому пробел превращается в
+ - Добавляет параметр без значения
val query = UriQueryBuilder(true, false) //(1)!
query.add("page", "1") //(2)!
query.add("sort", "name age") //(3)!
query.add("debug") //(4)!
val uri = "/api/users" + query.build()
// /api/users?page=1&sort=name+age&debug
- Первый аргумент: начинать строку с
?; второй: начинать с&, если базовый путь уже заканчивается параметром запроса - Добавляет пару
name=value, обе части кодируются в URL - Значения кодируются в URL, поэтому пробел превращается в
+ - Добавляет параметр без значения
Тело ответа¶
HttpBodyInput описывает тело ответа. Он наследует HttpBody и является Closeable.
| Метод | Возвращает | Описание |
|---|---|---|
asInputStream() |
InputStream |
Читает тело как поток |
getFullContentIfAvailable() |
ByteBuffer |
Возвращает тело целиком, если оно уже буферизовано, иначе null |
contentLength() |
long |
Длина тела, -1 если неизвестна |
contentType() |
String |
Значение заголовка Content-Type, может быть null |
close() |
void |
Освобождает ресурсы соединения |
Исходящий аналог — HttpBodyOutput, который создается через HttpBody.plaintext(...), HttpBody.json(...),
HttpBody.octetStream(...), HttpBody.of(contentType, content) либо HttpBodyOutput.of(contentType, inputStream)
для потоковой передачи тела, которое не помещается в память целиком.
Ответ клиента¶
HttpClientResponse — интерфейс, представляющий HTTP-ответ от сервера. Он является Closeable.
| Метод | Возвращает | Описание |
|---|---|---|
code() |
int |
Код статуса HTTP (200, 404, 500 и т.д.) |
headers() |
HttpHeaders |
Заголовки ответа |
body() |
HttpBodyInput |
Тело ответа |
close() |
void |
Закрывает ответ и освобождает соединение |
Заголовки¶
HttpHeaders дает доступ к заголовкам запроса и ответа. Имена заголовков приводятся к нижнему регистру, поиск регистронезависимый.
| Метод | Возвращает | Описание |
|---|---|---|
getFirst(String name) |
String |
Первое значение заголовка либо null |
getAll(String name) |
List<String> |
Все значения заголовка либо null |
has(String name) |
boolean |
Присутствует ли заголовок |
names() |
Set<String> |
Все имена заголовков |
size() |
int |
Количество заголовков |
isEmpty() |
boolean |
Пуст ли набор заголовков |
toMutable() |
MutableHttpHeaders |
Изменяемая копия |
Чтение заголовков:
var request = HttpClientRequest.get("http://localhost:8090/api/data").build();
try (var response = httpClient.execute(request)) {
HttpHeaders headers = response.headers();
String contentType = headers.getFirst("content-type");
List<String> allValues = headers.getAll("x-custom-header");
boolean hasHeader = headers.has("authorization");
}
val request = HttpClientRequest.get("http://localhost:8090/api/data").build()
httpClient.execute(request).use { response ->
val headers = response.headers()
val contentType = headers.getFirst("content-type")
val allValues = headers.getAll("x-custom-header")
val hasHeader = headers.has("authorization")
}
Сборка заголовков:
HttpHeaders.of(...) возвращает MutableHttpHeaders, который поддерживает set, add и remove:
MutableHttpHeaders headers = HttpHeaders.of();
headers.add("authorization", "Bearer token123");
headers.add("x-custom-header", "value");
headers.set("content-type", "application/json");
var request = HttpClientRequest.post("http://localhost:8090/api/data")
.header("authorization", headers.getFirst("authorization"))
.body(HttpBody.json("{}"))
.build();
val headers = HttpHeaders.of()
headers.add("authorization", "Bearer token123")
headers.add("x-custom-header", "value")
headers.set("content-type", "application/json")
val request = HttpClientRequest.post("http://localhost:8090/api/data")
.header("authorization", headers.getFirst("authorization")!!)
.body(HttpBody.json("{}"))
.build()
Готовый объект HttpHeaders можно также передать прямо в декларативный метод через @Header, смотрите Заголовок.
Cookies¶
Куки — это обычные заголовки: исходящая кука это заголовок Cookie, входящая — заголовок Set-Cookie.
Cookie описывает одну куку, а Cookies — служебный класс, который их разбирает и формирует.
Отправка куки:
Чтение кук из ответа:
try (var response = httpClient.execute(request)) {
var setCookies = response.headers().getAll("set-cookie");
if (setCookies != null) {
for (var header : setCookies) {
Cookie cookie = Cookies.parseSetCookieHeader(header);
String name = cookie.name();
String value = cookie.value();
String domain = cookie.domain();
String path = cookie.path();
}
}
}
- Имена заголовков сопоставляются без учёта регистра
httpClient.execute(request).use { response ->
val setCookies = response.headers().getAll("set-cookie")
if (setCookies != null) {
for (header in setCookies) {
val cookie = Cookies.parseSetCookieHeader(header)
val name = cookie.name()
val value = cookie.value()
val domain = cookie.domain()
val path = cookie.path()
}
}
}
Для декларативного клиента вместо ручной сборки заголовка используйте аннотацию параметра @Cookie.
Телеметрия¶
Телеметрия HTTP-клиента устанавливается как перехватчик: DeclarativeHttpClientConfig запрашивает у HttpClientTelemetryFactory
объект HttpClientTelemetry для каждого метода клиента и оборачивает транспорт в TelemetryInterceptor.
Точки расширения находятся в пакете io.koraframework.http.client.common.telemetry.
Для каждого HTTP-запроса HttpClientTelemetry.observe(request) создает HttpClientObservation,
который видит запрос через observeRequest, ответ через observeResponse, сбой через observeError
и всегда закрывается вызовом end().
Фабрика по умолчанию DefaultHttpClientTelemetryFactory объединяет три опциональные части, каждую из которых можно заменить,
объявив компонент соответствующего типа:
DefaultHttpClientLoggerFactoryсоздает логгеры запроса и ответа;DefaultHttpClientMetricsFactoryсоздает сборщик метрик;DefaultHttpClientBodyConverterпревращает захваченное тело в строку, которая пишется в лог.
Если для метода клиента выключены и логирование, и метрики, и трассировка, фабрика возвращает пустую телеметрию.
Логирование. На каждый метод клиента создаются два логгера, названные по классу клиента, методу и направлению:
com.example.SomeClient.hello.request и com.example.SomeClient.hello.response.
Их уровень определяет объем записи: INFO пишет только операцию, DEBUG добавляет параметры запроса и заголовки,
TRACE добавляет тело. Значения скрываемых параметров и заголовков заменяются стратегией маскирования,
а тело больше maxRequestBodyLogSize / maxResponseBodyLogSize пропускается с предупреждением.
Настройка самих логгеров описана в разделе Логирование.
Метрики. Сборщик по умолчанию пишет таймер http.client.request.duration с корзинами SLO из
telemetry.metrics.slo, размеченный методом HTTP, кодом статуса, маршрутом, адресом сервера и типом ошибки.
Смотрите Описание метрик.
Трассировка. На каждый запрос создается span с именем <METHOD> <шаблон пути> и семантическими атрибутами HTTP
из OpenTelemetry; telemetry.tracing.pathFull выбирает между атрибутами url.full и url.path.
Смотрите Трассировка.
Логирование¶
Логирование клиента пишется через SLF4J под двумя логгерами на каждый метод клиента, названными по каноническому имени интерфейса @HttpClient,
имени метода и направлению: <interface>.<method>.request и <interface>.<method>.response.
Включение логирования в конфигурации (telemetry.logging.enabled = true) активирует телеметрию, но что именно пишется, определяется уровнем логирования этих логгеров,
поэтому детализацией вы управляете из вашего фреймворка логирования (logback и т.д.):
| Уровень лога | Что логируется |
|---|---|
INFO |
Строка начала запроса и конца ответа: метод и шаблон пути, статус ответа, код результата и длительность |
DEBUG |
Дополнительно query-параметры запроса и заголовки запроса и ответа |
TRACE |
Дополнительно тела запроса и ответа, а также полный (нешаблонизированный) путь |
WARN |
Запрос, завершившийся исключением: код результата CONNECTION_ERROR, длительность и тип исключения |
Поля конфигурации формируют вывод (полный список см. в Конфигурации клиента):
pathFull— если задано, принудительно пишет в лог полный путь (true) или шаблон маршрута (false); если не задано, полный путь пишется только наTRACEmaskHeaders— имена заголовков, значения которых маскируются (по умолчанию маскируютсяauthorization,cookie,set-cookie)maskQueries— имена query-параметров, значения которых маскируютсяmaxRequestBodyLogSize/maxResponseBodyLogSize— тело большего размера не логируется, вместо него пишется предупреждение
Для метода hello интерфейса com.example.SomeClient включить полное логирование тел можно так:
<logger name="com.example.SomeClient.hello.request" level="TRACE"/>
<logger name="com.example.SomeClient.hello.response" level="TRACE"/>
Маскирование¶
Значения заголовков из telemetry.logging.maskHeaders и query-параметров из telemetry.logging.maskQueries
(имена сравниваются в нижнем регистре) заменяются результатом компонента MaskingStrategy с тегом @Tag(HttpClientTelemetry.class).
Стратегия получает каждое значение отдельно, а стратегия по умолчанию пишет ***.
Списки имен настраиваются для каждого клиента и метода, а стратегия — один компонент, общий для всех клиентов.
Чтобы изменить способ маскирования, зарегистрируйте свою стратегию с тем же тегом — она заменит стратегию по умолчанию.
Подходит любая MaskingStrategy, в том числе встроенные стратегии модуля логирования.
Тела запроса и ответа, которые пишутся на TRACE, маскируются компонентом DataMasker (io.koraframework.logging.common.masking.raw)
с тегом @Tag(HttpClientTelemetry.class), выбранным по Content-Type тела: json для */json и *+json,
xml для */xml и *+xml, form-urlencoded для application/x-www-form-urlencoded.
По умолчанию ни один маскировщик не зарегистрирован, поэтому без него тело логируется как есть.
Зарегистрируйте JsonDataMasker, XmlDataMasker или FormUrlencodedDataMasker с правилами маскирования, по одному на формат:
@KoraApp
public interface Application extends OkHttpClientModule {
@Tag(HttpClientTelemetry.class)
default MaskingStrategy httpClientMaskingStrategy() {
return new MaskingKeepLast("***", 4); //(1)!
}
@Tag(HttpClientTelemetry.class)
default DataMasker httpClientJsonDataMasker() {
return new JsonDataMasker(MaskingPathRules.builder()
.mask("password", new MaskingFull()) //(2)!
.mask("user.token", new MaskingKeepLast()) //(3)!
.build());
}
}
- Значения заголовков и query-параметров сохраняют последние 4 символа после
*** - Одиночный сегмент совпадает с полем с таким именем на любой глубине;
MaskingFullзаменяет значение на*** - Путь через точку сопоставляется от корня нагрузки,
*совпадает ровно с одним сегментом;MaskingKeepLastсохраняет последние 4 символа после***
@KoraApp
interface Application : OkHttpClientModule {
@Tag(HttpClientTelemetry::class)
fun httpClientMaskingStrategy(): MaskingStrategy = MaskingKeepLast("***", 4) //(1)!
@Tag(HttpClientTelemetry::class)
fun httpClientJsonDataMasker(): DataMasker = JsonDataMasker(
MaskingPathRules.builder()
.mask("password", MaskingFull()) //(2)!
.mask("user.token", MaskingKeepLast()) //(3)!
.build()
)
}
- Значения заголовков и query-параметров сохраняют последние 4 символа после
*** - Одиночный сегмент совпадает с полем с таким именем на любой глубине;
MaskingFullзаменяет значение на*** - Путь через точку сопоставляется от корня нагрузки,
*совпадает ровно с одним сегментом;MaskingKeepLastсохраняет последние 4 символа после***
DefaultHttpClientBodyConverter зарегистрирован как @DefaultComponent и служит точкой переопределения маскирования тел:
подкласс с @Component может переопределить selectRequestDataMasker / selectResponseDataMasker, чтобы выбирать маскировщик для клиента или сообщения,
или convertBody(ByteBuffer, String, DataMasker), чтобы изменить превращение байтов в текст.
Свой логгер¶
Телеметрия по умолчанию собирается из заменяемых компонентов HttpClientModule:
DefaultHttpClientLoggerFactory— создает логгеры запроса и ответа, зарегистрирован как@DefaultComponent;DefaultHttpClientBodyConverter— превращает захваченное тело в текст лога, зарегистрирован как@DefaultComponent;DefaultHttpClientMetricsFactory— создает сборщик метрик; в графе не регистрируется, поэтому компонент этого типа заменяет встроенный экземпляр;HttpClientTelemetryFactory— строит всю телеметрию метода клиента; фабрика с тегом выбирается через@HttpClient(telemetryTag = ...).
Компонент того же типа заменяет компонент по умолчанию. Например, фабрика логгеров, которая пишет все клиенты в одну пару логгеров:
@Component
public final class SharedHttpClientLoggerFactory extends DefaultHttpClientLoggerFactory {
private final MaskingStrategy maskingStrategy;
public SharedHttpClientLoggerFactory(@Tag(HttpClientTelemetry.class) MaskingStrategy maskingStrategy) {
super(maskingStrategy);
this.maskingStrategy = maskingStrategy;
}
@Override
public DefaultHttpClientLogger create(DefaultHttpClientTelemetry.TelemetryContext context) {
var logging = context.config().logging();
return new DefaultHttpClientLogger(
LoggerFactory.getLogger("http-client.request"), //(1)!
LoggerFactory.getLogger("http-client.response"),
logging.maskQueries(), //(2)!
logging.maskHeaders(),
maskingStrategy,
context
);
}
}
- Имена логгеров выбираете вы; запись лога по-прежнему содержит
clientConfigPath - Имена сравниваются в нижнем регистре: фабрика по умолчанию приводит их к нему, здесь они передаются как настроены
@Component
class SharedHttpClientLoggerFactory(
@Tag(HttpClientTelemetry::class) private val maskingStrategy: MaskingStrategy
) : DefaultHttpClientLoggerFactory(maskingStrategy) {
override fun create(context: DefaultHttpClientTelemetry.TelemetryContext): DefaultHttpClientLoggerFactory.DefaultHttpClientLogger {
val logging = context.config().logging()
return DefaultHttpClientLoggerFactory.DefaultHttpClientLogger(
LoggerFactory.getLogger("http-client.request"), //(1)!
LoggerFactory.getLogger("http-client.response"),
logging.maskQueries(), //(2)!
logging.maskHeaders(),
maskingStrategy,
context
)
}
}
- Имена логгеров выбираете вы; запись лога по-прежнему содержит
clientConfigPath - Имена сравниваются в нижнем регистре: фабрика по умолчанию приводит их к нему, здесь они передаются как настроены