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:
- Java's
HttpURLConnection
(built into the JDK) - Kotlin's
ktor-client
library - Square's OkHttp library
- 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.
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
:
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:
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:
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:
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.11.0")
}
Simple GET request with OkHttp:
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
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:
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:
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:
- Java's HttpURLConnection - Built-in but verbose and lacks modern features
- Ktor Client - Kotlin-first with coroutines support, type-safe, and feature-rich
- 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
-
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.
-
Build a GitHub repository explorer that fetches and displays information about a user's repositories.
-
Create a function that downloads a file from a URL, showing progress updates as it downloads.
-
Implement a basic HTTP client that can handle authentication (Basic Auth or OAuth).
-
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! :)