KtorMiddleTechnical

Что такое плагин 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» в реальном сервисе.

Sources

Related topics