Skip to main content

Kotlin HTTP Client

In today's interconnected world, most applications need to communicate with external services or APIs over the internet. HTTP (Hypertext Transfer Protocol) is the foundation of data communication on the web, and Kotlin provides several ways to work with HTTP requests and responses. This guide will explore different HTTP client options in Kotlin, from the standard library to popular third-party libraries.

Introduction to HTTP Clients

An HTTP client is a tool that allows your application to send requests to web servers and process their responses. Common operations include:

  • Fetching data (GET requests)
  • Submitting data (POST requests)
  • Updating resources (PUT/PATCH requests)
  • Deleting resources (DELETE requests)

In Kotlin, you have multiple options for making HTTP requests:

  1. Java's HttpURLConnection (built into the JDK)
  2. Kotlin's ktor-client library
  3. Square's OkHttp library
  4. Spring's RestTemplate or WebClient (if you're using Spring)

Let's explore each of these options with practical examples.

Using Java's HttpURLConnection

The most basic approach is using Java's built-in HttpURLConnection class, which requires no additional dependencies.

kotlin
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL

fun main() {
val url = URL("https://api.github.com/users/kotlin")
val connection = url.openConnection() as HttpURLConnection

connection.requestMethod = "GET"
connection.setRequestProperty("Accept", "application/json")
connection.connectTimeout = 5000
connection.readTimeout = 5000

val responseCode = connection.responseCode
println("Response Code: $responseCode")

if (responseCode == HttpURLConnection.HTTP_OK) {
val reader = BufferedReader(InputStreamReader(connection.inputStream))
val response = StringBuilder()
var line: String?

while (reader.readLine().also { line = it } != null) {
response.append(line)
}
reader.close()

println("Response: $response")
} else {
println("Error: Unable to fetch data")
}

connection.disconnect()
}

Output:

Response Code: 200
Response: {"login":"kotlin","id":1446536,"node_id":"MDEyOk9yZ2FuaXphdGlvbjE0NDY1MzY=","avatar_url":"https://avatars.githubusercontent.com/u/1446536?v=4",...}

This approach works but has several downsides:

  • Verbose code
  • Manual stream handling
  • No built-in support for modern features like async requests
  • Exception handling is cumbersome

Using Ktor Client

Ktor is a Kotlin-first framework developed by JetBrains that includes a powerful HTTP client. Ktor Client supports coroutines, making it ideal for asynchronous operations.

First, add the dependencies to your build.gradle.kts:

kotlin
dependencies {
implementation("io.ktor:ktor-client-core:2.3.5")
implementation("io.ktor:ktor-client-cio:2.3.5") // CIO engine
implementation("io.ktor:ktor-client-content-negotiation:2.3.5")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.5")
}

Now you can use Ktor Client:

kotlin
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

@Serializable
data class GithubUser(
val login: String,
val id: Int,
val avatar_url: String,
val name: String? = null,
val bio: String? = null
)

fun main() = runBlocking {
val client = HttpClient(CIO) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
prettyPrint = true
isLenient = true
})
}
}

try {
// Basic GET request
val response: HttpResponse = client.get("https://api.github.com/users/kotlin")
println("Response status: ${response.status}")

// Parse JSON response into a Kotlin object
val user: GithubUser = response.body()
println("User: ${user.login}, ID: ${user.id}")
println("Avatar: ${user.avatar_url}")
println("Name: ${user.name}")
println("Bio: ${user.bio}")

} catch (e: Exception) {
println("Error: ${e.message}")
} finally {
client.close()
}
}

Output:

Response status: 200 OK
User: kotlin, ID: 1446536
Avatar: https://avatars.githubusercontent.com/u/1446536?v=4
Name: Kotlin
Bio: The Kotlin Programming Language

POST Request with Ktor

Here's how to make a POST request with JSON data:

kotlin
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

@Serializable
data class Post(val title: String, val body: String, val userId: Int)

@Serializable
data class PostResponse(val id: Int, val title: String, val body: String, val userId: Int)

fun main() = runBlocking {
val client = HttpClient(CIO) {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
})
}
}

try {
val post = Post(
title = "Kotlin HTTP Client Example",
body = "This is a post request example using Ktor client",
userId = 1
)

val response: HttpResponse = client.post("https://jsonplaceholder.typicode.com/posts") {
contentType(ContentType.Application.Json)
setBody(post)
}

println("Status: ${response.status}")
val createdPost: PostResponse = response.body()
println("Created post with ID: ${createdPost.id}")
println("Title: ${createdPost.title}")
println("Body: ${createdPost.body}")

} finally {
client.close()
}
}

Output:

Status: 201 Created
Created post with ID: 101
Title: Kotlin HTTP Client Example
Body: This is a post request example using Ktor client

Using OkHttp

OkHttp by Square is another popular HTTP client that works well with Kotlin. It's known for its efficiency and ease of use.

Add the dependency:

kotlin
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.11.0")
}

Simple GET request with OkHttp:

kotlin
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.IOException

fun main() {
val client = OkHttpClient()

val request = Request.Builder()
.url("https://api.github.com/users/kotlin")
.header("Accept", "application/json")
.build()

try {
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
throw IOException("Unexpected response code: ${response.code}")
}

val responseBody = response.body?.string()
println("Response Code: ${response.code}")
println("Response Body: $responseBody")
}
} catch (e: IOException) {
println("Error: ${e.message}")
}
}

POST Request with JSON in OkHttp

kotlin
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.io.IOException

fun main() {
val client = OkHttpClient()

// Create JSON payload
val json = JSONObject().apply {
put("title", "Kotlin OkHttp Example")
put("body", "This is a post request using OkHttp")
put("userId", 1)
}

val requestBody = json.toString().toRequestBody("application/json; charset=utf-8".toMediaType())

val request = Request.Builder()
.url("https://jsonplaceholder.typicode.com/posts")
.post(requestBody)
.build()

try {
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
throw IOException("Unexpected response code: ${response.code}")
}

val responseBody = response.body?.string()
println("Response Code: ${response.code}")
println("Response Body: $responseBody")
}
} catch (e: IOException) {
println("Error: ${e.message}")
}
}

Output:

Response Code: 201
Response Body: {
"id": 101,
"title": "Kotlin OkHttp Example",
"body": "This is a post request using OkHttp",
"userId": 1
}

Practical Example: Weather API Client

Let's build a simple weather API client using Ktor that retrieves current weather data for a city:

kotlin
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

@Serializable
data class WeatherResponse(
val main: Main,
val weather: List<Weather>,
val name: String
)

@Serializable
data class Main(
val temp: Double,
val feels_like: Double,
val temp_min: Double,
val temp_max: Double,
val humidity: Int
)

@Serializable
data class Weather(
val id: Int,
val main: String,
val description: String
)

class WeatherApiClient(private val apiKey: String) {
private val client = HttpClient(CIO) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
prettyPrint = true
})
}
}

suspend fun getWeatherForCity(city: String): WeatherResponse {
val response: HttpResponse = client.get("https://api.openweathermap.org/data/2.5/weather") {
parameter("q", city)
parameter("appid", apiKey)
parameter("units", "metric")
}

return response.body()
}

fun close() {
client.close()
}
}

fun main() = runBlocking {
// Replace with your actual API key
val apiKey = "your_openweathermap_api_key"
val weatherClient = WeatherApiClient(apiKey)

try {
val city = "London"
println("Getting weather for $city...")

val weather = weatherClient.getWeatherForCity(city)

println("\nWeather in ${weather.name}:")
println("Temperature: ${weather.main.temp}°C")
println("Feels like: ${weather.main.feels_like}°C")
println("Humidity: ${weather.main.humidity}%")
println("Conditions: ${weather.weather.firstOrNull()?.description ?: "unknown"}")
} catch (e: Exception) {
println("Error getting weather: ${e.message}")
} finally {
weatherClient.close()
}
}

Sample Output:

Getting weather for London...

Weather in London:
Temperature: 14.2°C
Feels like: 13.8°C
Humidity: 82%
Conditions: overcast clouds

Error Handling and Timeouts

When working with HTTP clients, it's important to handle errors gracefully and implement timeouts to prevent your application from hanging:

kotlin
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.json.Json
import kotlin.time.Duration.Companion.seconds

fun main() = runBlocking {
val client = HttpClient(CIO) {
// Set timeout configurations
install(HttpTimeout) {
requestTimeoutMillis = 5000 // 5 seconds for the whole request
connectTimeoutMillis = 2000 // 2 seconds to establish connection
socketTimeoutMillis = 5000 // 5 seconds for socket read/write
}

install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
}

// Handle HTTP errors
HttpResponseValidator {
validateResponse { response ->
val statusCode = response.status.value
when (statusCode) {
in 300..399 -> println("Redirection: $statusCode")
in 400..499 -> throw ClientRequestException(response, "Client error: $statusCode")
in 500..599 -> throw ServerResponseException(response, "Server error: $statusCode")
}
}
}
}

try {
// Use withTimeoutOrNull to handle overall timeout
val result = withTimeoutOrNull(10.seconds) {
client.get("https://api.github.com/users/kotlin")
}

if (result != null) {
println("Request successful: ${result.status}")
val bodyText = result.body<String>()
println("Response length: ${bodyText.length} characters")
} else {
println("Request timed out after 10 seconds")
}

} catch (e: ClientRequestException) {
println("Client error: ${e.message}")
} catch (e: ServerResponseException) {
println("Server error: ${e.message}")
} catch (e: Exception) {
println("Other error: ${e.message}")
} finally {
client.close()
}
}

Summary

In this guide, we've explored several ways to make HTTP requests in Kotlin:

  1. Java's HttpURLConnection - Built-in but verbose and lacks modern features
  2. Ktor Client - Kotlin-first with coroutines support, type-safe, and feature-rich
  3. OkHttp - Efficient and popular cross-platform HTTP client

Each approach has its own advantages:

  • Use HttpURLConnection for simple requests when you want to avoid additional dependencies
  • Use Ktor Client when you need a modern, coroutine-based API with strong Kotlin integration
  • Use OkHttp when you need a battle-tested library with broader ecosystem integration

Remember to always handle errors gracefully, set proper timeouts, and close your client resources when done.

Additional Resources and Exercises

Resources

Exercises

  1. Create a simple command-line weather app that takes a city name as input and displays the current weather using the example from this guide.

  2. Build a GitHub repository explorer that fetches and displays information about a user's repositories.

  3. Create a function that downloads a file from a URL, showing progress updates as it downloads.

  4. Implement a basic HTTP client that can handle authentication (Basic Auth or OAuth).

  5. Create a parallel downloader that fetches multiple URLs simultaneously using coroutines and displays the responses.

Remember to handle errors gracefully in all these exercises and consider implementing retry logic for transient failures!



If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)