Что такое плагин HttpRequestRetry и как настраивать логику повторных попыток?
HttpRequestRetry — плагин Ktor Client, автоматически повторяющий запросы при сетевых ошибках или 5xx; настраивается через retryOnServerErrors(), retryIf{}, exponentialDelay() и modifyRequest{}; по умолчанию не повторяет POST-запросы во избежание дублирования.
Что такое HttpRequestRetry
HttpRequestRetry — плагин Ktor HTTP Client (артефакт io.ktor:ktor-client-core, подключается через install(HttpRequestRetry)), реализующий автоматические повторные попытки при ошибках сети или серверных ответах 5xx. Плагин работает на уровне конвейера клиента и прозрачен для кода вызова.
Минимальная настройка
val client = HttpClient(CIO) {
install(HttpRequestRetry) {
retryOnServerErrors(maxRetries = 3)
exponentialDelay()
}
}
retryOnServerErrors повторяет запрос при HTTP 500–599. exponentialDelay() задаёт задержку по формуле base * 2^attempt + jitter, где по умолчанию base = 0.5 с, max = 60 с.
Полная конфигурация с объяснением ключей
install(HttpRequestRetry) {
// Максимальное число повторных попыток (не считая оригинальный запрос)
maxRetries = 5
// Условие: повторять при IOException (сеть оборвалась)
retryOnException(maxRetries = 5, retryOnTimeout = true)
// Условие: повторять при 429, 500, 502, 503, 504
retryIf { _, response ->
response.status.value.let { it == 429 || it in 500..504 }
}
// Задержка: экспоненциальная с jitter
exponentialDelay(
base = 2.0, // множитель (секунды)
maxDelayMs = 30_000L, // не более 30 секунд
randomizationMs = 500L // ±500 мс jitter
)
// Кастомный delay — например, уважать Retry-After заголовок
delayMillis { retry ->
val retryAfter = response?.headers?.get(HttpHeaders.RetryAfter)
?.toLongOrNull()?.times(1000)
retryAfter ?: (1000L * (1 shl retry)) // fallback exponential
}
// Изменить запрос перед повтором (например, обновить токен)
modifyRequest { request ->
request.headers.remove(HttpHeaders.Authorization)
request.headers.append(
HttpHeaders.Authorization,
"Bearer ${tokenStore.getFreshToken()}"
)
}
}
Идемпотентность: когда повторять безопасно
По умолчанию плагин повторяет только идемпотентные методы (GET, HEAD, OPTIONS, PUT, DELETE). POST и PATCH не повторяются автоматически, чтобы избежать дублирования данных. Для явного разрешения POST:
install(HttpRequestRetry) {
maxRetries = 3
retryIf { request, response ->
// Только если сервер явно сигнализирует о повторяемости
request.method == HttpMethod.Post &&
response.status == HttpStatusCode.ServiceUnavailable
}
exponentialDelay()
}
Мониторинг попыток
install(HttpRequestRetry) {
retryOnServerErrors(maxRetries = 3)
exponentialDelay()
// Коллбэк после каждой попытки
retryIf { request, response ->
val shouldRetry = !response.status.isSuccess()
if (shouldRetry) {
application.log.warn(
"Retry attempt for ${request.url}: ${response.status}"
)
}
shouldRetry
}
}
Интеграция с CircuitBreaker (Resilience4j)
HttpRequestRetry хорошо сочетается с паттерном circuit breaker: retry пробует несколько раз, circuit breaker отрубает поток при систематических ошибках.
val circuitBreaker = CircuitBreakerRegistry.ofDefaults()
.circuitBreaker("payment-service")
suspend fun callPaymentApi(request: PaymentRequest): PaymentResponse {
return circuitBreaker.executeSuspendFunction {
httpClient.post("https://payments.example.com/charge") {
contentType(ContentType.Application.Json)
setBody(request)
}.body()
}
}
Подводные камни
- POST-запросы не повторяются по умолчанию — если ваш API принимает POST для идемпотентных операций (например, GET через POST из-за большого тела), нужно явно разрешать повторы через
retryIf. - Тело запроса потребляется: при повторе с ByteArray-телом Ktor корректно буферизует его, но если тело задано как Flow или Channel — повтор невозможен без явной буферизации.
- Retry-After в секундах vs дата: заголовок Retry-After может быть как числом секунд, так и HTTP-датой — парсить нужно оба формата, иначе получите NumberFormatException.
- Суммарное время ожидания: 5 попыток с экспоненциальным delay могут занять > 60 с суммарно — добавляйте общий таймаут на уровне клиента через
install(HttpTimeout). - Конфликт с HttpTimeout: если таймаут одного запроса меньше суммы всех задержек retry — плагин будет прерываться досрочно; согласовывайте значения.
- 429 и Retry-After: без явного чтения заголовка
Retry-AfterвdelayMillisсервер будет получать повторы слишком быстро и продолжит отвечать 429. - modifyRequest и изменяемые данные: если
modifyRequestобращается к внешнему токен-стору, убедитесь, что он thread-safe и не вызывает блокирующий ввод-вывод вне Dispatchers.IO.
Common mistakes
- Путать термин «http request retry» с соседним механизмом Ktor.
- Не называть границу lifecycle, transaction, thread или request для «http request retry».
- Игнорировать production-эффекты «http request retry»: latency, SQL shape, memory, security или observability.
What the interviewer is testing
- Попросить объяснить механизм «http request retry» на минимальном примере.
- Проверить, видит ли кандидат failure mode и диагностику для «http request retry».
- Уточнить, какие настройки или API меняют «http request retry» в реальном сервисе.