HTTP клиент на Kotlin: делаем запросы правильно — продвинутый уровень
При разработке современных приложений на Kotlin работа с HTTP-запросами выходит далеко за рамки простого вызова URL.readText(). На продвинутом уровне необходимо учитывать управление соединениями, таймауты, повторные попытки, логирование, асинхронность и корректную обработку ошибок. В этой статье мы разберём, как настроить HTTP-клиент на Kotlin для production-среды, используя две популярные библиотеки: OkHttp и Ktor Client. Материал рассчитан на разработчиков, уже знакомых с основами Kotlin и HTTP.
1. Выбор библиотеки: OkHttp vs Ktor Client
Прежде чем писать код, важно понять разницу между подходами. OkHttp — это зрелая, проверенная временем библиотека от Square, которая отлично работает в связке с Retrofit. Ktor Client — это нативная мультиплатформенная библиотека от JetBrains, полностью интегрированная с корутинами Kotlin. Для серверных проектов на Spring Boot часто выбирают OkHttp, а для Ktor-приложений или мультиплатформенных проектов — Ktor Client.
Мы рассмотрим оба варианта, чтобы вы могли выбрать подходящий для своего стека.
2. Продвинутая настройка OkHttp
Базовая настройка OkHttp тривиальна, но для production требуется тонкая конфигурация. Рассмотрим создание Singleton-клиента с кастомными параметрами.
import okhttp3.*
import java.util.concurrent.TimeUnit
object HttpClientProvider {
private val client: OkHttpClient by lazy {
OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.addInterceptor(LoggingInterceptor())
.addInterceptor(RetryInterceptor(maxRetries = 3))
.connectionPool(ConnectionPool(5, 5, TimeUnit.MINUTES))
.build()
}
fun getClient(): OkHttpClient = client
}
2.1 Кастомные перехватчики (Interceptors)
Перехватчики — это мощный механизм для модификации запросов и ответов. Ниже приведён пример логирующего перехватчика и перехватчика для повторных попыток.
class LoggingInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
println("--> ${request.method} ${request.url}")
val startTime = System.nanoTime()
val response = chain.proceed(request)
val duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)
println("<-- ${response.code} ${response.message} (${duration}ms)")
return response
}
}
class RetryInterceptor(private val maxRetries: Int) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var retryCount = 0
var response: Response? = null
while (retryCount < maxRetries) {
try {
response = chain.proceed(chain.request())
if (response.isSuccessful) return response
} catch (e: Exception) {
println("Retry #${retryCount + 1} failed: ${e.message}")
}
retryCount++
if (retryCount < maxRetries) Thread.sleep(1000L * retryCount)
}
return response ?: throw IOException("Max retries exceeded")
}
}
2.2 Асинхронные запросы с корутинами
OkHttp поддерживает синхронные и асинхронные (callback-based) запросы. Для интеграции с Kotlin корутинами используем suspendCancellableCoroutine.
suspend fun makeRequest(url: String): String {
return suspendCancellableCoroutine { continuation ->
val request = Request.Builder().url(url).build()
val call = HttpClientProvider.getClient().newCall(request)
continuation.invokeOnCancellation { call.cancel() }
call.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
continuation.resumeWithException(e)
}
override fun onResponse(call: Call, response: Response) {
response.use {
if (!response.isSuccessful) {
continuation.resumeWithException(HttpException(response.code()))
} else {
continuation.resume(response.body?.string() ?: "")
}
}
}
})
}
}
3. Продвинутая работа с Ktor Client
Ktor Client изначально спроектирован для корутин и предоставляет более лаконичный API. Рассмотрим настройку с таймаутами, пулом соединений и кастомными плагинами.
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
object KtorClientProvider {
val client = HttpClient(OkHttp) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
isLenient = true
})
}
install(HttpTimeout) {
requestTimeoutMillis = 30_000
connectTimeoutMillis = 10_000
socketTimeoutMillis = 30_000
}
install(HttpRequestRetry) {
retryOnServerErrors(maxRetries = 3)
exponentialDelay()
}
engine {
config {
retryOnConnectionFailure(true)
connectionPool(ConnectionPool(5, 5, TimeUnit.MINUTES))
}
}
}
}
3.1 Выполнение запросов с десериализацией
Ktor Client позволяет легко парсить JSON в data-классы. Пример для получения списка пользователей:
@Serializable
data class User(val id: Int, val name: String, val email: String)
suspend fun fetchUsers(): List<User> {
val response = KtorClientProvider.client.get("https://api.example.com/users")
return response.body()
}
3.2 Обработка ошибок и кастомные заголовки
Для production важно корректно обрабатывать HTTP-статусы. Используйте sealed class для представления результата.
sealed class ApiResult<out T> {
data class Success<T>(val data: T) : ApiResult<T>()
data class Error(val code: Int, val message: String) : ApiResult<Nothing>()
data class NetworkError(val exception: Throwable) : ApiResult<Nothing>()
}
suspend fun getUser(id: Int): ApiResult<User> {
return try {
val response = KtorClientProvider.client.get("https://api.example.com/users/$id") {
header("Authorization", "Bearer token123")
header("Accept", "application/json")
}
if (response.status.value in 200..