Computer scienceBackendKtorKtor Client

Ktor Client: Engines

20 minutes read

You already know how to integrate and use Ktor Client in your applications. Today, we will take a closer look at the engines we can use together with Ktor Client.

Ktor Engines

When you connect Ktor Client to an application, you see that in addition to the Ktor Client core itself, you have to connect an HTTP engine. In our case, it was CIO:

dependencies {
    implementation("io.ktor:ktor-client-core:2.3.3")
    implementation("io.ktor:ktor-client-cio:2.3.3")
}

You might be wondering why we need any HTTP engine if we are using Ktor Client. Doesn't it provide the functionality we need?

The answer is that Ktor Client is a multi-platform library, meaning that it can be used not only on the Java Virtual Machine (JVM) but also on other platforms such as Android or JavaScript. For each platform there are different engines that facilitate HTTP and WebSocket requests.

The HTTP engine in Ktor is a critical component that manages the actual process of sending and receiving HTTP requests and responses, serving essentially as the core of network communication in Ktor. Ktor supports various HTTP client engines, and each engine has its own set of features and configuration options.

Instead of reinventing the wheel, the developers of Ktor Client decided to make it possible to use one of these existing engines. This decision provides flexibility and allows using the engine that best suits the specific requirements of your project and the platform it's targeting.

So, all you need to do is import the engine you need and pass it to the HttpClient constructor. This is what we did when we looked at the Ktor Client:

import io.ktor.client.*
import io.ktor.client.engine.cio.*

fun main(args: Array<String>) {
    val client = HttpClient(CIO)
}

Before importing, of course, the engine must be specified in the dependencies of build.gradle.kts.

We are not limited to a single CIO. There are many other engines available for us to use. Let's take a closer look at them.

Ktor Client engines for JVM

Before you add an engine to a project, you must first select it. The choice depends on the platform and other engine-specific features.

On the JVM platform, we can use the following three main engines in addition to the CIO: Apache, Java, Jetty.

  • Jetty is typically used when HTTP/2 support is needed.
  • Java is useful if you need support for full-duplex communication over WebSockets.
  • Apache is a good choice if a proxy is required because Jetty does not support proxy and Java does not run on the JAVA 8 API.

All these details can be seen in the documentation. There are handy tables that show support of various features by available engines:

HTTP/2 and WebSockets availability for different engines

There, you can also see the list of available engines and platforms on which a particular engine can be used:

Availability of engines on different platforms

In this topic, we will only cover connecting and configuring JVM engines, because all other engines can be connected and configured in the same way. The documentation page above explains this in detail.

Connecting engine to project

Once you have chosen the engine you want, you need to add the corresponding dependency to build.gradle.kts.

For Apache:

implementation("io.ktor:ktor-client-apache5:$ktor_version")

For Java:

implementation("io.ktor:ktor-client-java:$ktor_version")

For Jetty:

implementation("io.ktor:ktor-client-jetty:$ktor_version")

Where $ktor_version is the version of Ktor Client used. For example: 2.3.3.

Remember that in addition to the engine, you must specify the Ktor Client core in build.gradle.kts:

implementation("io.ktor:ktor-client-core:$ktor_version")

Once the Gradle dependencies are updated, the engine can be imported into the application code.

Importing the engine in application code

To attach the engine to the Ktor Client, simply pass it to the HttpClient constructor.

For Apache:

import io.ktor.client.*
import io.ktor.client.engine.apache.*

fun main(args: Array<String>) {
    val client = HttpClient(Apache)
}

For Java:

import io.ktor.client.*
import io.ktor.client.engine.java.*

fun main(args: Array<String>) {
    val client = HttpClient(Java)
}

For Jetty:

import io.ktor.client.*
import io.ktor.client.engine.jetty.*

fun main(args: Array<String>) {
    val client = HttpClient(Jetty)
}

After that, you can make requests without going into the API of a particular engine. Ktor Client will take care of the interaction with the engine.

If you need to switch to another engine or platform, you can simply pass another engine to the HttpClient constructor, and that's it. Your other code that uses Ktor Client API and makes requests can be left unchanged.

By the way, if you call the HttpClient constructor without an argument, the client will choose an engine automatically depending on the artifacts added in a build script:

import io.ktor.client.*

fun main(args: Array<String>) {
    val client = HttpClient()
}

Engine Configuration

The last thing we need to look at is the configuration parameters of the engine. These parameters are usually unique for each engine, so the configuration is one of the few things you need to change when you switch to a different engine.

To set the configuration parameters, we will use a special Kotlin syntax based on trailing lambdas and receivers. It looks pretty simple.

val client = HttpClient() {
    engine {
        //configuration parameters
    }
}

After the HttpClient constructor we make a block of curly braces in which the engine method is used. Inside its curly braces we specify the engine configuration parameters.

The set of parameters depends on the engine passed to the constructor. However, there are parameters that are common to all engines. There are currently 3 of them. You can see them in documentation.

We can set them as follows:

val client = HttpClient() {
    engine {
        pipelining = true
        threadsCount = 4
        proxy = null
    }
}

pipelining enables HTTP pipelining advice. threadsCount specifies network threads count advice. proxy parameter is responsible for setting the proxy.

If you don't know what a particular setting is responsible for, don't specify it and Ktor Client will set it to the default value.

Parameters unique to a particular engine are specified in the same way. For example, for Apache:

val client = HttpClient(Apache) {
    engine {
        followRedirects = true

        socketTimeout = 10_000
        connectTimeout = 10_000
        connectionRequestTimeout = 10_000

        customizeClient {
            setProxy(HttpHost("127.0.0.1", 8080))
        }
    }
}

followRedirects tells Apache to follow the redirects if it receives the appropriate header.

socketTimeout, connectTimeout and connectionRequestTimeout set timeouts (in milliseconds) in various situations.

customizeClient method specifies the address of the proxy server to be used.

A complete list of Apache settings is available here. For other engines, these parameters do not work.

For example, here is how to specify proxy when working with Java:

val client = HttpClient(Java) {
    engine {
        proxy = ProxyBuilder.http("http://proxy-server.com/")
    }
}

You can find the complete list of parameters for Java and Jetty. For the other engines, everything happens the same way.

The complete list of parameters for any engine is available in the documentation.

Conclusion

In this topic, we learned about different Ktor engines that can be used on the client side.

We've learned:

  • Why do we need HTTP engines?
  • Where to look for features and limitations of the engines.
  • How to connect and import the engine into the application.
  • How to specify engine configuration parameters.

Now, let's put what we've learned into practice.

How did you like the theory?
Report a typo