HTTP Client in Kotlin: Making Requests Properly — Advanced Level
When developing modern applications in Kotlin, working with HTTP requests goes far beyond simply calling URL.readText(). At an advanced level, it is necessary to consider connection management, timeouts, retries, logging, asynchrony, and proper error handling. In this article, we will look at how to configure an HTTP client in Kotlin for a production environment using two popular libraries: OkHttp and Ktor Client. The material is intended for developers already familiar with the basics of Kotlin and HTTP.
1. Choosing a Library: OkHttp vs Ktor Client
Before writing code, it is important to understand the difference between the approaches. OkHttp is a mature, time-tested library from Square that works great in conjunction with Retrofit. Ktor Client is a native multiplatform library from JetBrains, fully integrated with Kotlin coroutines. For server projects on Spring Boot, OkHttp is often chosen, while for Ktor applications or multiplatform projects, Ktor Client is preferred.
We will look at both options so you can choose the one suitable for your stack.
2. Advanced OkHttp Configuration
Basic OkHttp configuration is trivial, but production requires fine-tuning. Let's look at creating a Singleton client with custom parameters.
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 Custom Interceptors
Interceptors are a powerful mechanism for modifying requests and responses. Below is an example of a logging interceptor and a retry interceptor.
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 Asynchronous Requests with Coroutines
OkHttp supports synchronous and asynchronous (callback-based) requests. To integrate with Kotlin coroutines, we use suspendCancellableCoroutine.
suspend fun makeRequest(url: String): String { return suspendCancellableCoroutine { continuation -> val request = Request.Builder().url(url).build() val call = HttpClientProvider.getClient().newC