Welcome Ktor Client — Your next Http client for Kotlin based Project. Part II.
In the first part of this series, we explored the basics of Ktor-client and how it stands out as a modern, lightweight HTTP client compared to alternative HTTP clients. If you missed the first part, I would recommend to go through and get familiar with it.
Now that we’ve covered the foundational aspects, we will dive deeper and highlight the following features:
- Http Timeout
- Authorization / Authentication
- Exploring other methods — POST, PUT, DELETE, GET
- Exploring Ktor-client coroutine model with high concurrency
HTTP Timeout
During the periods of high load or server overload, the response time of the request may be high or increased. This leads to the issue known as — HTTP Timeout and this often results in the exception. To simulate this scenario, I have written a server application with endpoint that responds in 15 seconds.
Let’s run the program and see the output.
As we can see, when the server responds slowly, we get HttpRequestTimeoutException — Request timeout has expired [url=http://localhost:8080/posts/titles, request_timeout=unknown ms]
To solve this issue we need to configure the client HTTP Timeout. To do so, let’s install the Timeout plugin to the client. This plugin doesn’t need a specific dependency and comes with ktor-client-core dependency.
install(HttpTimeout) {
requestTimeoutMillis = 20_000
connectTimeoutMillis = 20_000
socketTimeoutMillis = 20_000
}
// configuring Timeout settings
- request timeout — a time period required to process an HTTP call: from sending a request to receiving a response.
- connection timeout — a time period in which a client should establish a connection with a server.
- socket timeout — a maximum time of inactivity between two data packets when exchanging data with a server.
Let’s run program with this set up and check the result
Having this configuration everything works fine.
Furthermore, we can fine tune our client for specific requests. Imagine that response time for the requests is less than 5 seconds and only one request takes around 15 seconds. For this scenario, we can configure the HttpTimeout globally to be 5 seconds and for one specific request to be 20 seconds.
The block of code below shows how to configure client for specific requests and this setting overrides the global configuration.
val response = httpClient.get("http://localhost:8080/posts/titles") {
timeout {
connectTimeoutMillis = 20_000
requestTimeoutMillis = 20_000
socketTimeoutMillis = 20_000
}
}
With this setup we have a client with a global configuration and one-time configuration for a specific request.
Authorization / Authentication
In most of the cases when we send requests, the servers are secured and the requests should contain security tokens. Let’s configure our client to contain security tokens.
Similar to HttpTimeout, we can configure security tokens globally and locally. Global security configuration is used when one long-term token is used to access secured server resources and this token rarely changes.
Local security configuration is used when security token is specified for each separate request.
Ktor’s Authentication mechanism supports the following Http schemes:
- Basic — uses
Base64
encoding to provide a username and password. Generally is not recommended if not used in combination with HTTPS. - Digest — an authentication method that communicates user credentials in an encrypted form by applying a hash function to the username and password.
- Bearer — an authentication scheme that involves security tokens called bearer tokens.
To specify security tokens, we need to include a specific dependency for Authentication— implementation(“io.ktor:ktor-client-auth:$ktor_version”).
The final build.gradle.kts will be as following:
val ktor_version = "2.3.12"
val log4j2_version = "2.23.1"
plugins {
kotlin("jvm") version "1.9.22"
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.10"
}
group = "com.atomic-coding"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
// CORE LIBRARIES
implementation("io.ktor:ktor-client-core:$ktor_version")
implementation("io.ktor:ktor-client-cio-jvm:$ktor_version")
// Logging
implementation("io.ktor:ktor-client-logging:$ktor_version")
// FOR JSON AND Serialization
implementation("io.ktor:ktor-client-content-negotiation:$ktor_version")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")
// Authentication
implementation("io.ktor:ktor-client-auth:$ktor_version")
implementation("org.apache.logging.log4j:log4j-core:$log4j2_version")
implementation("org.apache.logging.log4j:log4j-api:$log4j2_version")
implementation("org.apache.logging.log4j:log4j-slf4j-impl:$log4j2_version")
testImplementation("org.jetbrains.kotlin:kotlin-test")
}
tasks.test {
useJUnitPlatform()
}
kotlin {
jvmToolchain(19)
}
Let’s install Auth plugin to our client. Inside install(Auth) { } we can specify what Http scheme we want to use — Basic, Digest or Bearer.
val client = HttpClient(CIO) {
install(Auth) {
}
}
Basic Authentication
val client = HttpClient(CIO) {
install(Auth) {
basic {
credentials {
BasicAuthCredentials(username = "atomicCoding", password = "hashed-password")
}
}
}
}
Bearer Authentication
val client = HttpClient(CIO) {
install(Auth) {
bearer {
loadTokens {
BearerTokens(
accessToken = "hashedAccessToken",
refreshToken = "hashedRefreshToken"
)
}
}
}
}
Let’s add Auth plugin to our client, it will be as following. In this example, we configured a global authentication for our client.
In cases where we need to use a different security token for each request, we can specify the authentication mechanism at the call site.
Local authentication mechanism in action.
val response = httpClient.get("http://localhost:8080/posts/titles") {
bearerAuth("UserSpecificAccessToken")
}
The client will be as following:
It is also important to mention that you cannot specify new security tokens for specific requests if the client has been configured globally to use other tokens.
The globally configured security token will have higher prioririty and it will be used in the request. Hence, it is better to use separate Ktor-clients, one globally configured with long-term token and another one for cases when different security token is used.
Exploring other methods — POST, PUT, DELETE
So far we have explored most of the features provided by Ktor-client, it is time to walk through other Http methods and give some examples. Since the previous examples already demonstrated the GET method, I will omit it in this section. The client is sending requests to the {JSON} Placeholder server, which provides a free public API for testing and prototyping
POST
This method is usually used to create some resource. Let’s create a Post class that will serve as request body.
@Serializable
data class Post(
val id: Int? = null,
val userId: Int,
val title: String,
val body: String
)
To send a POST request, the request should contain request body, aka payload, and contentType should be specified. In the example below, we create a new Post object that is used as request body and specify ContentType as Json.
As you can see, the request was successful, and the server responded with 201 Created.
PUT
This method is typically used to update a resource, a post in our case. Let’s update the post with id = 1. First let’s fetch the post by id — originalPost is the post we fetched. Next we send PUT request to update the post.
Similarly to POST request, in PUT request we need to specify the request body and contentType, below is an example of PUT request.
val updatedPost = httpClient.put("https://jsonplaceholder.typicode.com/posts/$postIdToUpdate") {
val updatedPost = originalPost.copy(
title = title, body = body
)
setBody(updatedPost)
contentType(ContentType.Application.Json)
}.body<Post>()
In the example below, we first fetch the original post, log its data. Then send a PUT request, log the response and check that the response, or a updated post, has the title and body we specified in the request.
As we can see from the logs, the PUT request was completed successfully, the resource was updated and assert statements passed as well.
DELETE
This method is used to delete a resource and probably easiest method. The syntax nearly identical to that of GET request.
The DELETE request takes the arguments from the endpoint or url to determine which resource to delete. https://jsonplaceholder.typicode.com/posts/$postIdToDelete
is the endpoint for sending the request to the server, postIdToDelete
is the id of the resource that is used by the server to determine which resource to delete
Below is an example of DELETE request. We call delete() method and specify the endpoint. Pretty easy, right?
As the logs indicate, the DELETE request was completed successfully.
Exploring Ktor-client coroutine model with high concurrency
This section is probably is most interesting and exciting. Ktor’s non-blocking architecture allows to perform efficiently under heavy loads thanks to suspending functions. Ktor’s suspend functions, in combination with Kotlin coroutines, allow to write highly scalable and efficient code by leveraging structured concurrency, non-blocking I/O, lightweight coroutines, cancellation and timeouts, structured exception handling, and concurrency primitives.
To demonstrate this, let’s simulate a heavy load case where we need to send multiple requests. The simulation will include a series of requests in a loop and will measure the execution time of two cases — one is with a sequential approach and the other is with concurrent approach
We will send by one request for each Http method inside a loop. Http requests will be the ones we discussed earlier.
Let’s go with testing and measure metrics — execution time of the program.
Sequential Implementation
In the example below, we send four distinct requests, each utilizing a different HTTP method, and we execute the sending requests inside a loop that runs 10 times. Overall it adds up to 40 requests.
The whole logic of loop and sending requests runs inside a measureTimeMillis {} that measures execution time in milliseconds. Then we log the execution time of the code.
Let’s review the results: the execution time for the code was approximately 12 seconds, which is quite lengthy. This is due to the fact that each request is executed sequentially, and there is no concurrency in the code. Although Ktor’s methods are suspending, they do not make the code concurrent on their own; coroutines need to be leveraged for that purpose.
Let’s leverage coroutines to enhance the scalability and efficiency of our code.
Concurrent approach.
Let’s review the results of the concurrent approach. With a few small refinements, we’ve made our code concurrent and achieved significantly faster results. As you can see, I simply wrapped the entire logic for sending requests inside a loop using coroutineScope {}
and placed each HTTP call inside a launch {}
block, which is responsible for starting a coroutine and enabling concurrency in our code. The results are impressive—the response times range between 550ms and 800ms. In fact, the responses now take less than a second, whereas the same logic took around 12 seconds to execute with the sequential approach. Thanks to coroutines and structured concurrency, writing concurrent code becomes much easier, leading to such remarkable efficiency.
Furthermore, we can further fine tune our client by configuring the specific features of the client such as Engine, Caching, Retry-mechanism, etc. Thanks to Ktor’s flexibility and extensibility, it is all possible and easy to implement. Don’t forget to drop a visit to Ktor’s official documentation.
Throughout these posts, we’ve explored various aspects of working with Ktor-client, from setting up logging and handling HTTP timeouts to making different types of requests and optimizing concurrency with coroutines. By understanding and implementing these features, you can build robust and efficient HTTP clients that perform well under various conditions. Ktor-client provides the tools to get the job done efficiently.
Try out these techniques in your next project to experience the power and simplicity of Ktor.
If you have questions or suggestions, please drop me comments. I will be happy for the feedback. Happy coding and see you in the next posts.