Выполнение запросов и обработка ответов

Клиент выполняет методы синхронно. Вызов блокирует текущий поток до получения ответа, истечения тайм-аута или ошибки транспорта. Возвращать retrofit2.Call<T> из интерфейса нельзя: SimpleCallAdapterFactory завершит создание метода с BGException.

Порядок обработки

Вызов проходит через адаптеры в таком порядке:

метод API
  -> Rate Limiter
    -> Circuit Breaker
      -> SimpleCallAdapter: Call.execute()
        -> OkHttp

Rate Limiter является внешним декоратором, поэтому разрешение расходуется и тогда, когда Circuit Breaker уже открыт и не отправляет HTTP-запрос.

Application-interceptor OkHttp выполняются в порядке регистрации:

  1. TimingInterceptor — измерение и событие телеметрии;

  2. HttpLoggingInterceptor — HTTP-лог;

  3. TaggingInterceptor — перенос методных аннотаций в теги запроса;

  4. interceptor провайдера авторизации;

  5. дополнительные interceptors, зарегистрированные приложением.

Из-за такого порядка HTTP-логирование выполняется до добавления заголовков авторизации и дополнительных перехватчиков. Это уменьшает риск вывода токена, но дополнительный interceptor всё равно может записать его самостоятельно.

Преобразование тела

Основной ObjectMapper выбирается параметром @Api.responseType:

  • JSON — обычный ObjectMapper;

  • XML — XmlMapper.

Оба маппера подключают Kotlin module со следующими настройками:

  • null преобразуется в пустую коллекцию или map;

  • null разрешено подменять значением Kotlin-параметра по умолчанию;

  • неизвестные поля ответа игнорируются;

  • включены void-valued properties.

Если интерфейс помечен @ConverterFactory, собственная фабрика добавляется перед Jackson и может обработать запрос или ответ первой. Подробности приведены в разделе расширения.

Успешный ответ и ошибки

Для HTTP 2xx возвращается десериализованное тело. Пустое тело даёт null, даже если метод интерфейса объявлен с non-null типом Kotlin; это нужно учитывать в контракте внешнего API.

Ошибки обрабатываются так:

Ситуация Результат

IOException при выполнении запроса

Логируется предупреждение и выбрасывается ApiException с методом и путём запроса.

HTTP не 2xx без тела ошибки

BGMessageException с HTTP-кодом.

HTTP не 2xx, стандартный errorType

BGMessageException с HTTP-кодом и исходным текстом тела.

HTTP не 2xx, собственный errorType

Тело всегда читается JSON-маппером; в сообщение попадает response.errorMessage.

Собственный errorType не удалось прочитать

Логируется предупреждение, затем BGMessageException с исходным телом.

Rate Limiter не дал разрешение

BGMessageException: превышена частота запросов к сервису.

Circuit Breaker открыт

BGMessageException: сервис временно недоступен.

Если Circuit Breaker включён, любое исключение выполненного действия, кроме CallNotPermittedException, дополнительно оборачивается в BGException. Resilience4j при этом учитывает только TimeoutException, IOException, ApiException и ResourceNotFoundException; BGException игнорируется в статистике. При выключенном Circuit Breaker исходное исключение проходит без этой обёртки.

Модель ошибки

Собственная модель должна наследовать ru.ufanet.retrofit.Response и отдавать пользовательский текст через errorMessage:

@Api(
    name = "catalog",
    errorType = CatalogError::class
)
interface CatalogApi

Даже у XML-клиента ошибка читается JSON-маппером. Если внешний сервис возвращает XML-ошибку, текущий встроенный обработчик не сможет преобразовать её в errorType и покажет исходное тело.

SOAP 1.1

Встроенный SoapConverterFactory оборачивает только методы с @SoapRequest. Интерфейс настраивается так:

import retrofit2.http.Body
import retrofit2.http.POST
import ru.bgcrm.util.api.annotation.Api
import ru.bgcrm.util.api.converter.ConverterFactory
import ru.bgcrm.util.api.converter.soap.SoapConverterFactory
import ru.bgcrm.util.api.tag.SoapRequest

@Api(name = "billing-soap", responseType = Api.ResponseType.XML)
@ConverterFactory(
    factoryClass = SoapConverterFactory.SoapConverterFactoryCreator::class
)
interface BillingSoapApi {
    @POST("services/Billing")
    @SoapRequest(
        namespace = "urn:billing",
        method = "getBalance"
    )
    fun getBalance(@Body request: BalanceRequest): BalanceResponse
}

Запрос получает SOAP 1.1 envelope, пространство имён записывается с префиксом bg, а полезная нагрузка — в bg:<method>. Ответ ищется по ключу <method>Response, после чего возвращается вложенное поле return.

Конвертер не реализует SOAP Fault, WS-Security, несколько значений return или поиск ответа по namespace. Для такого протокола нужен собственный конвертер либо специализированный клиент.

Телеметрия

TimingInterceptor публикует RetrofitRequestEndEvent после каждого завершения, включая транспортную ошибку. Событие содержит:

  • apiName — @Api.name;

  • method — HTTP-метод;

  • endpoint — имя Java/Kotlin-метода интерфейса;

  • status — HTTP-код или failure;

  • duration — время в миллисекундах.

TelemetryListener преобразует события в метрики http_retrofit_request_count_total и http_retrofit_request_duration_milliseconds с теми же тегами. Экспортируемые Prometheus-имена и назначение тегов приведены в каталоге телеметрии.

JMX

Пул соединений, Dispatcher, Rate Limiter и Circuit Breaker публикуются как MBean с именами вида:

BGCRM:type=<Component>,name=api-<Api.name>,description=<api.name или Api.name>

Состав атрибутов и особенности регистрации описаны в каталоге MBean.