Ktor 3.0.0-rc-1 Help

Custom plugins

Starting with v2.2.0, Ktor provides a new API for creating custom client plugins. In general, this API doesn't require an understanding of internal Ktor concepts, such as pipelines, phases, and so on. Instead, you have access to different stages of handling requests and responses using a set of handlers, such as onRequest, onResponse, and so on.

Create and install your first plugin

In this section, we'll demonstrate how to create and install your first plugin that adds a custom header to each request:

  1. To create a plugin, call the createClientPlugin function and pass a plugin name as an argument:

    package com.example.plugins import io.ktor.client.plugins.api.* val CustomHeaderPlugin = createClientPlugin("CustomHeaderPlugin") { // Configure the plugin ... }

    This function returns the ClientPlugin instance that will be used to install the plugin.

  2. To append a custom header to each request, you can use the onRequest handler, which provides access to request parameters:

    package com.example.plugins import io.ktor.client.plugins.api.* val CustomHeaderPlugin = createClientPlugin("CustomHeaderPlugin") { onRequest { request, _ -> request.headers.append("X-Custom-Header", "Default value") } }
  3. To install the plugin, pass the created ClientPlugin instance to the install function inside the client's configuration block:

    import com.example.plugins.* val client = HttpClient(CIO) { install(CustomHeaderPlugin) }

You can find the full example here: CustomHeader.kt. In the following sections, we'll look at how to provide a plugin configuration and handle requests and responses.

Provide plugin configuration

The previous section demonstrates how to create a plugin that appends a predefined custom header to each response. Let's make this plugin more useful and provide a configuration for passing any custom header name and value:

  1. First, you need to define a configuration class:

    class CustomHeaderPluginConfig { var headerName: String = "X-Custom-Header" var headerValue: String = "Default value" }
  2. To use this configuration in a plugin, pass a configuration class reference to createApplicationPlugin:

    import io.ktor.client.plugins.api.* val CustomHeaderConfigurablePlugin = createClientPlugin("CustomHeaderConfigurablePlugin", ::CustomHeaderPluginConfig) { val headerName = pluginConfig.headerName val headerValue = pluginConfig.headerValue onRequest { request, _ -> request.headers.append(headerName, headerValue) } }

    Given that plugin configuration fields are mutable, saving them in local variables is recommended.

  3. Finally, you can install and configure the plugin as follows:

    val client = HttpClient(CIO) { install(CustomHeaderConfigurablePlugin) { headerName = "X-Custom-Header" headerValue = "Hello, world!" } }

Handle requests and responses

Custom plugins provide access to different stages of handling requests and responses using a set of dedicated handlers, for example:

  • onRequest and onResponse allow you to handle requests and responses, respectively.

  • transformRequestBody and transformResponseBody can be used to apply necessary transformations to request and response bodies.

There is also the on(...) handler that allows you to invoke specific hooks that might be useful to handle other stages of a call. The tables below list all handlers in the order they are executed:

Handler

Description

onRequest

This handler is executed for each HTTP request and allows you to modify it.

Example: Custom header

transformRequestBody

Allows you to transform a request body. In this handler, you need to serialize the body into OutgoingContent (for example, TextContent, ByteArrayContent, or FormDataContent) or return null if your transformation is not applicable.

Example: Data transformation

onResponse

This handler is executed for each incoming HTTP response and allows you to inspect it in various ways: log a response, save cookies, and so on.

Examples: Logging headers, Response time

transformResponseBody

Allows you to transform a response body. This handler is invoked for each HttpResponse.body call. You need to deserialize the body into an instance of requestedType or return null if your transformation is not applicable.

Example: Data transformation

onClose

Allows you to clean resources allocated by this plugin. This handler is called when the client is closed.

Handler

Description

on(SetupRequest)

The SetupRequest hook is executed first in request processing.

onRequest

This handler is executed for each HTTP request and allows you to modify it.

Example: Custom header

transformRequestBody

Allows you to transform a request body. In this handler, you need to serialize the body into OutgoingContent (for example, TextContent, ByteArrayContent, or FormDataContent) or return null if your transformation is not applicable.

Example: Data transformation

on(Send)

The Send hook provides the ability to inspect a response and initiate additional requests if needed. This might be useful for handling redirects, retrying requests, authentication, and so on.

Example: Authentication

on(SendingRequest)

The SendingRequest hook is executed for every request, even if it's not initiated by a user. For example, if a request results in a redirect, the onRequest handler will be executed only for the original request, while on(SendingRequest) will be executed for both original and redirected requests. Similarly, if you used on(Send) to initiate an additional request, handlers will be ordered as follows:

--> onRequest --> on(Send) --> on(SendingRequest) <-- onResponse --> on(SendingRequest) <-- onResponse

Examples: Logging headers, Response time

onResponse

This handler is executed for each incoming HTTP response and allows you to inspect it in various ways: log a response, save cookies, and so on.

Examples: Logging headers, Response time

transformResponseBody

Allows you to transform a response body. This handler is invoked for each HttpResponse.body call. You need to deserialize the body into an instance of requestedType or return null if your transformation is not applicable.

Example: Data transformation

onClose

Allows you to clean resources allocated by this plugin. This handler is called when the client is closed.

Share call state

Custom plugins allow you to share any value related to a call so that you can access this value inside any handler processing this call. This value is stored as an attribute with a unique key in the call.attributes collection. The example below demonstrates how to use attributes to calculate the time between sending a request and receiving a response:

import io.ktor.client.plugins.api.* import io.ktor.util.* val ResponseTimePlugin = createClientPlugin("ResponseTimePlugin") { val onCallTimeKey = AttributeKey<Long>("onCallTimeKey") on(SendingRequest) { request, content -> val onCallTime = System.currentTimeMillis() request.attributes.put(onCallTimeKey, onCallTime) } onResponse { response -> val onCallTime = response.call.attributes[onCallTimeKey] val onCallReceiveTime = System.currentTimeMillis() println("Read response delay (ms): ${onCallReceiveTime - onCallTime}") } }

You can find the full example here: ResponseTime.kt.

Access client configuration

You can access your client configuration using the client property, which returns the HttpClient instance. The example below shows how to get the proxy address used by the client:

import io.ktor.client.plugins.api.* val SimplePlugin = createClientPlugin("SimplePlugin") { val proxyAddress = client.engineConfig.proxy?.address() println("Proxy address: $proxyAddress") }

Examples

The code samples below demonstrate several examples of custom plugins. You can find the resulting project here: client-custom-plugin.

Custom header

Shows how to create a plugin that adds a custom header to each request:

package com.example.plugins import io.ktor.client.plugins.api.* val CustomHeaderConfigurablePlugin = createClientPlugin("CustomHeaderConfigurablePlugin", ::CustomHeaderPluginConfig) { val headerName = pluginConfig.headerName val headerValue = pluginConfig.headerValue onRequest { request, _ -> request.headers.append(headerName, headerValue) } } class CustomHeaderPluginConfig { var headerName: String = "X-Custom-Header" var headerValue: String = "Default value" }

Logging headers

Demonstrates how to create a plugin that logs request and response headers:

package com.example.plugins import io.ktor.client.plugins.api.* val LoggingHeadersPlugin = createClientPlugin("LoggingHeadersPlugin") { on(SendingRequest) { request, content -> println("Request headers:") request.headers.entries().forEach { entry -> printHeader(entry) } } onResponse { response -> println("Response headers:") response.headers.entries().forEach { entry -> printHeader(entry) } } } private fun printHeader(entry: Map.Entry<String, List<String>>) { var headerString = entry.key + ": " entry.value.forEach { headerValue -> headerString += "${headerValue};" } println("-> $headerString") }

Response time

Shows how to create a plugin that measures the time between sending a request and receiving a response:

package com.example.plugins import io.ktor.client.plugins.api.* import io.ktor.util.* val ResponseTimePlugin = createClientPlugin("ResponseTimePlugin") { val onCallTimeKey = AttributeKey<Long>("onCallTimeKey") on(SendingRequest) { request, content -> val onCallTime = System.currentTimeMillis() request.attributes.put(onCallTimeKey, onCallTime) } onResponse { response -> val onCallTime = response.call.attributes[onCallTimeKey] val onCallReceiveTime = System.currentTimeMillis() println("Read response delay (ms): ${onCallReceiveTime - onCallTime}") } }

Data transformation

Shows how to transform request and response bodies using the transformRequestBody and transformResponseBody hooks:

package com.example.plugins import com.example.model.* import io.ktor.client.plugins.api.* import io.ktor.http.* import io.ktor.http.content.* import io.ktor.utils.io.* val DataTransformationPlugin = createClientPlugin("DataTransformationPlugin") { transformRequestBody { request, content, bodyType -> if (bodyType?.type == User::class) { val user = content as User TextContent(text="${user.name};${user.age}", contentType = ContentType.Text.Plain) } else { null } } transformResponseBody { response, content, requestedType -> if (requestedType.type == User::class) { val receivedContent = content.readUTF8Line()!!.split(";") User(receivedContent[0], receivedContent[1].toInt()) } else { content } } }
package com.example import com.example.model.* import com.example.plugins.* import com.example.server.* import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.engine.cio.* import io.ktor.client.request.* import io.ktor.client.statement.* import kotlinx.coroutines.* fun main() { startServer() runBlocking { val client = HttpClient(CIO) { install(DataTransformationPlugin) } val bodyAsText = client.post("http://0.0.0.0:8080/post-data") { setBody(User("John", 42)) }.bodyAsText() val user = client.get("http://0.0.0.0:8080/get-data").body<User>() println("Userinfo: $bodyAsText") println("Username: ${user.name}, age: ${user.age}") } }
package com.example.model data class User(val name: String, val age: Int)

You can find the full example here: client-custom-plugin-data-transformation.

Authentication

A sample Ktor project showing how to use the on(Send) hook to add a bearer token to the Authorization header if an unauthorized response is received from the server:

package com.example.plugins import io.ktor.client.plugins.api.* import io.ktor.http.* val AuthPlugin = createClientPlugin("AuthPlugin", ::AuthPluginConfig) { val token = pluginConfig.token on(Send) { request -> val originalCall = proceed(request) originalCall.response.run { // this: HttpResponse if(status == HttpStatusCode.Unauthorized && headers["WWW-Authenticate"]!!.contains("Bearer")) { request.headers.append("Authorization", "Bearer $token") proceed(request) } else { originalCall } } } } class AuthPluginConfig { var token: String = "" }
package com.example import com.example.plugins.* import com.example.server.* import io.ktor.client.* import io.ktor.client.engine.cio.* import io.ktor.client.request.* import io.ktor.client.statement.* import kotlinx.coroutines.* fun main() { startServer() runBlocking { val client = HttpClient(CIO) { install(AuthPlugin) { token = "abc123" } } val response = client.get("http://0.0.0.0:8080/") println(response.bodyAsText()) } }

You can find the full example here: client-custom-plugin-auth.

Last modified: 02 April 2024