Http Client

Estimated reading time: 10 minutes

In addition to HTTP serving, Ktor also includes a flexible asynchronous HTTP client. This client support several configurable engines, and has its own set of features.

The main functionality is exposed through the io.ktor:ktor-client-core:$ktor_version artifact. And each engine, is provided in separate artifacts.

Table of contents:

Simple requests

The basic usage is super simple: you just have to instantiate an HttpClient instance, specifying an engine, for example Apache, Jetty or CIO, and start making requests using one of the many convenient methods available.

First you need to instantiate the client:

val client = HttpClient(Apache)

Then, to perform a GET request fully reading a String:

val htmlContent = client.get<String>("")

And in the case you are interested in the raw bits, you can read a ByteArray:

val bytes: ByteArray ="").response.readBytes()

It is possible to customize the request a lot and to stream the request and response payloads , but you can also just call a convenience extension method like HttpClient.get to do a GET request to receive the specified type directly (for example String).

Custom requests

We cannot live only from get requests, and Ktor allows you to build complex requests with any of the HTTP verbs, and process responses in many flexible ways.

The call method

The HttpClient call method, returns an HttpClientCall and allows you to perform simple untyped requests.

You can read the content by using response: HttpResponse. For further information, check the receiving content using HttpResponse section.

val call ="") {
    method = HttpMethod.Get

The request method

In addition to call, there is a request method for performing a typed request, receiving a specific type like String, HttpResponse, or an arbitrary class. You have to specify the URL, and the method when building the request.

val call = client.request<String> {
    method = HttpMethod.Get

The post and get methods

Similar to request, there are several extension methods to perform requests with the most common HTTP verbs: GET and POST.

val text =<String>("")

When calling request methods, you can provide a lambda to build the request parameters like the URL, the HTTP method (verb), the body, or the headers. The HttpRequestBuilder looks like this:

class HttpRequestBuilder : HttpMessageBuilder {
    val url: URLBuilder
    var method: HttpMethod
    val headers: HeadersBuilder
    var body: Any = EmptyContent
    val executionContext: CompletableDeferred<Unit>
    fun headers(block: HeadersBuilder.() -> Unit)
    fun url(block: URLBuilder.(URLBuilder) -> Unit)

The HttpClient class only offers the basic functionality, and all the methods for building requests are exposed as extensions.
You can check the standard available HttpClient build extension methods.

Specifying a body for requests

For POST and PUT requests, you can set the body property:<Unit> {
    body = // ...

The HttpRequestBuilder.body property can be a subtype of OutgoingContent as well as a String instance:

  • body = "HELLO WORLD!"
  • body = TextContent("HELLO WORLD!", ContentType.Text.Plain)
  • body = ByteArrayContent("HELLO WORLD!".toByteArray(Charsets.UTF_8))
  • body = LocalFileContent(File("build.gradle"))
  • body = JarFileContent(File("myjar.jar"), "test.txt", ContentType.fromFileExtension("txt").first())
  • body = URIFileContent(URL(""))

If you install the JsonFeature, and set the content type to application/json you can use arbitrary instances as the body, and they will be serialized as JSON:

data class HelloWorld(val hello: String)

val client = HttpClient(Apache) {
    install(JsonFeature) {
        serializer = GsonSerializer {
            // Configurable .GsonBuilder
}<Unit> {
    contentType(ContentType.Application.Json) // Required
    body = HelloWorld(hello = "world")

Remember that your classes must be top level to be recognized by Gson.
If you try to send a class that is inside a function, the feature will send a null.

Receiving the body of a response

By default you can use HttpResponse or String as possible types for typed HttpClient requests. So for example:

val htmlContent = client.get<String>("")
val response = client.get<HttpResponse>("")

If JsonFeature is configured, and the server returns the header Content-Type: application/json, you can also specify a class for deserializing it.

val helloWorld = client.get<HelloWorld>("")

The HttpResponse class

From an HttpResponse, you can get the response content easily:

  • val bytes: ByteArray = response.readBytes()
  • val text: String = response.readText()
  • val readChannel = response.receive<ByteReadChannel>()
  • val multiPart = response.receive<MultiPartData>()
  • val inputStream = response.receive<InputStream>() Remember that InputStream API is synchronous!
  • response.discardRemaining()

You can also get the additional response information such as its status, headers, internal state, etc.:


  • val status: HttpStatusCode = response.status
  • val headers: Headers = response.headers


  • val call: HttpClientCall =
  • val version: HttpProtocolVersion = response.version
  • val requestTime: Date = response.requestTime
  • val responseTime: Date = response.responseTime
  • val executionContext: Job = response.executionContext

Extensions for headers:

  • val contentType: ContentType? = response.contentType()
  • val charset: Charset? = response.charset()
  • val lastModified: Date? = response.lastModified()
  • val etag: String? = response.etag()
  • val expires: Date? = response.expires()
  • val vary: List<String>? = response.vary()
  • val contentLength: Int? = response.contentLength()
  • val setCookie: List<Cookie> = response.setCookie()


Similar to the server, Ktor supports features on the client. And it has the same design: there is a pipeline for client HTTP requests, and there are interceptors and installable features.


This feature sends an Authorization: Basic with the specified credentials:

val client = HttpClient(HttpClientEngine) {
    install(BasicAuth) {
        username = "username"
        password = "password"

To use this feature, you need to include the ktor-client-auth-basic artifact.

This feature implements the IETF’s RFC 7617.


This feature keeps cookies between calls or forces specific cookies:

val client = HttpClient(HttpClientEngine) {
    install(HttpCookies) {
        // Will keep an in-memory map with all the cookies from previous requests.
        storage = AcceptAllCookiesStorage()
        // Will ignore Set-Cookie and will send the specified cookies.
        storage = ConstantCookieStorage(Cookie("mycookie1", "value"), Cookie("mycookie2", "value"))


This feature discards the body of the response:

val client = HttpClient(HttpClientEngine) {

Use this if you are only interested in the response headers, and you cannot use the HEAD verb. This will use less memory, and will execute faster.


This feature processes the request content as plain text of a specified charset by defaultCharset. Also it will process the response content as plain text too.

val client = HttpClient(HttpClientEngine) {
    install(HttpPlainText) {
        defaultCharset = Charsets.UTF_8

Bear in mind that the default charset is the JVM’s charset that could be different between systems.
That’s why it is recommended to specify the default charset.


Processes the request and the response payload as JSON, serializing and de-serializing them using a specific serializer: JsonSerializer.

val client = HttpClient(HttpClientEngine) {
    install(JsonSerializer) {
        serializer = GsonSerializer()

To use this feature, you need to include io.ktor:ktor-client-json artifact.

Creating Custom Features

If you want to create features, you can use the standard features as reference.

You can also check the HttpRequestPipeline.Phases and HttpResponsePipeline.Phases to understand the interception points available.

Supported engines

Ktor HttpClient lets you to configure the parameters of each engine by calling Engine.config { }.

Every engine config has two common properties that can be set:

  • The dispatcher property is the CoroutineDispatcher used when processing client requests.
  • The sslContext is a allowing you to set custom keys, a trust manager or custom source for secure random data.
val client = HttpClient(MyHttpEngine.config {
    sslContext = SSLContext.getDefault()


Apache is the most configurable HTTP client at this point. It supports HTTP/1.1 and HTTP/2. It is the only one that supports following redirects and allows you to configure timeouts, proxies among other things supported by org.apache.httpcomponents:httpasyncclient.

A sample configuration would look like:

val client = HttpClient(Apache.config {
    followRedirects = true  // Follow http Location redirects - default false. It uses the default number of redirects defined by Apache's HttpClient that is 50.
    // For timeouts: 0 means infinite, while negative value mean to use the system's default value
    socketTimeout = 10_000  // Max time between TCP packets - default 10 seconds
    connectTimeout = 10_000 // Max time to establish an HTTP connection - default 10 seconds
    connectionRequestTimeout = 20_000 // Max time for the connection manager to start a request - 20 seconds
    customizeClient {
        // Apache's HttpAsyncClientBuilder
        setProxy(HttpHost("", 8080))
    customizeRequest {
        // Apache's RequestConfig.Builder

Artifact io.ktor:ktor-client-apache:$ktor_version.
Transitive dependency: org.apache.httpcomponents:httpasyncclient:4.1.3.


CIO (Coroutine-based I/O) is a Ktor implementation with no additional dependencies and fully asynchronous. It only supports HTTP/1.x for now.

CIO provides maxConnectionsCount and a endpointConfig for configuring.

A sample configuration would look like:

val client = HttpClient(CIO.config { 
    maxConnectionsCount = 1000 // Maximum number of socket connections.
    endpointConfig = EndpointConfig().apply {
        maxConnectionsPerRoute = 100 // Maximum number of requests for a specific endpoint route.
        pipelineMaxSize = 20 // Max number of opened endpoints.
        keepAliveTime = 5000 // Max number of milliseconds to keep each connection alive.
        connectTimeout = 5000 // Number of milliseconds to wait trying to connect to the server.
        connectRetryAttempts = 5 // Maximum number of attempts for retrying a connection.

Artifact io.ktor:ktor-client-cio:$ktor_version.
No additional transitive dependencies.


Jetty provides an additional sslContextFactory for configuring. It just supports HTTP/2 for now.

A sample configuration would look like:

val client = HttpClient(Jetty.config { 
    sslContextFactory = SslContextFactory()

Artifact io.ktor:ktor-client-jetty:$ktor_version.
Transitive dependency: org.eclipse.jetty.http2:http2-client:9.4.8.v20171121.


Remember that requests are asynchronous, but when performing a requests, the API is suspending and your function will be suspended until done. If you want to perform several requests at once in the same block, you can use launch or async functions and later get the results. For example:

Sequential requests:

suspend fun sequentialRequests() {
    val client = HttpClient(Apache)
    // Get the content of an URL.
    val bytes1 ="").response.readBytes() // Suspension point.
    // Once the previous request is done, get the content of an URL.
    val bytes2 ="").response.readBytes() // Suspension point.

Parallel requests:

suspend fun parallelRequests() {
    val client = HttpClient(Apache)
    // Start two requests asynchronously.
    val req1 = async {"").response.readBytes() }
    val req2 = async {"").response.readBytes() }
    // Get the request contents without blocking threads, but suspending the function until both
    // requests are done.
    val bytes1 = req1.await() // Suspension point.
    val bytes2 = req2.await() // Suspension point.


Interchanging JSON: Ktor server / Ktor client:

fun main(args: Array<String>) {
    val server = embeddedServer(
        port = 8080,
        module = Application::mymodule
    ).apply {
        start(wait = false)

    runBlocking {
        val client = HttpClient(Apache) {
            install(JsonFeature) {
                serializer = GsonSerializer {
                    // .GsonBuilder

        val message =<HelloWorld> {
            body = HelloWorld(hello = "world")

        println("CLIENT: Message from the server: $message")

        server.stop(1L, 1L, TimeUnit.SECONDS)

data class HelloWorld(val hello: String)

fun Application.mymodule() {
    install(ContentNegotiation) {
        gson {
    routing {
        post("/") {
            val message = call.receive<HelloWorld>()
            println("SERVER: Message from the client: $message")
            call.respond(HelloWorld(hello = "response"))

You can check the ktor-samples and ktor-exercises repositories for samples and exercises.