Ktor 3.0.0-rc-1 Help

Custom plugins

Starting with v2.0.0, Ktor provides a new API for creating custom 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 the onCall, onCallReceive, and onCallRespond handlers.

Create and install your first plugin

In this section, we'll demonstrate how to create and install your first plugin. You can use an application created in the Create, open and run a new Ktor project tutorial as a starting project.

  1. To create a plugin, call the createApplicationPlugin function and pass a plugin name:

    import io.ktor.server.application.* val SimplePlugin = createApplicationPlugin(name = "SimplePlugin") { println("SimplePlugin is installed!") }

    This function returns the ApplicationPlugin instance that will be used in the next step to install a plugin.

  2. To install a plugin, pass the created ApplicationPlugin instance to the install function in the application's initialization code:

    fun Application.module() { install(SimplePlugin) }
  3. Finally, run your application to see the plugin's greeting in the console output:

    2021-10-14 14:54:08.269 [main] INFO Application - Autoreload is disabled because the development mode is off. SimplePlugin is installed! 2021-10-14 14:54:08.900 [main] INFO Application - Responding at http://0.0.0.0:8080

You can find the full example here: SimplePlugin.kt. In the following sections, we'll look at how to handle calls on different stages and provide a plugin configuration.

Handle calls

In your custom plugin, you can handle requests and responses by using a set of handlers that provide access to different stages of a call:

  • onCall allows you to get request/response information, modify response parameters (for instance, append custom headers), and so on.

  • onCallReceive allows you to obtain and transform data received from the client.

  • onCallRespond allows you to transform data before sending it to the client.

  • on(...) allows you to invoke specific hooks that might be useful to handle other stages of a call or exceptions that happened during a call.

  • If required, you can share a call state between different handlers using call.attributes.

onCall

The onCall handler accepts the ApplicationCall as a lambda argument. This allows you to access request/response information and modify response parameters (for instance, append custom headers). If you need to transform a request/response body, use onCallReceive/onCallRespond.

Example 1: Request logging

The example below shows how to use onCall to create a custom plugin for logging incoming requests:

val RequestLoggingPlugin = createApplicationPlugin(name = "RequestLoggingPlugin") { onCall { call -> call.request.origin.apply { println("Request URL: $scheme://$localHost:$localPort$uri") } } }

If you install this plugin, the application will show requested URLs in a console, for example:

Request URL: http://0.0.0.0:8080/ Request URL: http://0.0.0.0:8080/index

Example 2: Custom header

This example demonstrates how to create a plugin that appends a custom header to each response:

val CustomHeaderPlugin = createApplicationPlugin(name = "CustomHeaderPlugin") { onCall { call -> call.response.headers.append("X-Custom-Header", "Hello, world!") } }

As a result, a custom header will be added to all responses:

HTTP/1.1 200 OK X-Custom-Header: Hello, world!

Note that a custom header name and value in this plugin are hardcoded. You can make this plugin more flexible by providing a configuration for passing the required custom header name/value.

onCallReceive

The onCallReceive handler provides the transformBody function and allows you to transform data received from the client. Suppose the client makes a sample POST request that contains 10 as text/plain in its body:

POST http://localhost:8080/transform-data Content-Type: text/plain 10

To receive this body as an integer value, you need to create a route handler for POST requests and call call.receive with the Int parameter:

post("/transform-data") { val data = call.receive<Int>() }

Now let's create a plugin that receives a body as an integer value and adds 1 to it. To do this, we need to handle transformBody inside onCallReceive as follows:

val DataTransformationPlugin = createApplicationPlugin(name = "DataTransformationPlugin") { onCallReceive { call -> transformBody { data -> if (requestedType?.type == Int::class) { val line = data.readUTF8Line() ?: "1" line.toInt() + 1 } else { data } } } }

transformBody in the code snippet above works as follows:

  1. TransformBodyContext is a lambda receiver that contains type information about the current request. In the example above, the TransformBodyContext.requestedType property is used to check the requested data type.

  2. data is a lambda argument that allows you to receive a request body as ByteReadChannel and convert it to the required type. In the example above, ByteReadChannel.readUTF8Line is used to read a request body.

  3. Finally, you need to transform and return data. In our example, 1 is added to the received integer value.

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

onCallRespond

onCallRespond also provides the transformBody handler and allows you to transform data to be sent to the client. This handler is executed when the call.respond function is invoked in a route handler. Let's continue with the example from onCallReceive where an integer value is received in a POST request handler:

post("/transform-data") { val data = call.receive<Int>() call.respond(data) }

Calling call.respond invokes onCallRespond, which in turn allows you to transform data to be sent to the client. For example, the code snippet below shows how to add 1 to the initial value:

onCallRespond { call -> transformBody { data -> if (data is Int) { (data + 1).toString() } else { data } } }

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

Other useful handlers

Apart from the onCall, onCallReceive, and onCallRespond handlers, Ktor provides a set of specific hooks that might be useful to handle other stages of a call. You can handle these hooks using the on handler that accepts a Hook as a parameter. These hooks include:

  • CallSetup is invoked as a first step in processing a call.

  • ResponseBodyReadyForSend is invoked when a response body comes through all transformations and is ready to be sent.

  • ResponseSent is invoked when a response is successfully sent to a client.

  • CallFailed is invoked when a call fails with an exception.

  • AuthenticationChecked is executed after authentication credentials are checked. The following example shows how to use this hook to implement authorization: custom-plugin-authorization.

The example below shows how to handle CallSetup:

on(CallSetup) { call-> // ... }

Share call state

Custom plugins allow you to share any value related to a call, so 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 receiving a request and reading a body:

val DataTransformationBenchmarkPlugin = createApplicationPlugin(name = "DataTransformationBenchmarkPlugin") { val onCallTimeKey = AttributeKey<Long>("onCallTimeKey") onCall { call -> val onCallTime = System.currentTimeMillis() call.attributes.put(onCallTimeKey, onCallTime) } onCallReceive { call -> val onCallTime = call.attributes[onCallTimeKey] val onCallReceiveTime = System.currentTimeMillis() println("Read body delay (ms): ${onCallReceiveTime - onCallTime}") } }

If you make a POST request, the plugin prints a delay in a console:

Request URL: http://localhost:8080/transform-data Read body delay (ms): 52

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

Handle application events

The on handler provides the ability to use the MonitoringEvent hook to handle events related to an application's lifecycle. For example, you can pass the following predefined events to the on handler:

  • ApplicationStarting

  • ApplicationStarted

  • ApplicationStopPreparing

  • ApplicationStopping

  • ApplicationStopped

The code snippet below shows how to handle application shutdown using ApplicationStopped:

package com.example.plugins import io.ktor.events.EventDefinition import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.application.hooks.* val ApplicationMonitoringPlugin = createApplicationPlugin(name = "ApplicationMonitoringPlugin") { on(MonitoringEvent(ApplicationStarted)) { application -> application.log.info("Server is started") } on(MonitoringEvent(ApplicationStopped)) { application -> application.log.info("Server is stopped") // Release resources and unsubscribe from events application.monitor.unsubscribe(ApplicationStarted) {} application.monitor.unsubscribe(ApplicationStopped) {} } on(ResponseSent) { call -> if (call.response.status() == HttpStatusCode.NotFound) { this@createApplicationPlugin.application.monitor.raise(NotFoundEvent, call) } } } val NotFoundEvent: EventDefinition<ApplicationCall> = EventDefinition()

This might be useful to release application resources.

Provide plugin configuration

The Custom header example 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 the required custom header name/value.

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

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

    val CustomHeaderPlugin = createApplicationPlugin( name = "CustomHeaderPlugin", createConfiguration = ::PluginConfiguration ) { val headerName = pluginConfig.headerName val headerValue = pluginConfig.headerValue pluginConfig.apply { onCall { call -> call.response.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 a plugin as follows:

    install(CustomHeaderPlugin) { headerName = "X-Custom-Header" headerValue = "Hello, world!" }

Configuration in a file

Ktor allows you to specify plugin settings in a configuration file. Let's see how to achieve this for CustomHeaderPlugin:

  1. First, add a new group with the plugin settings to the application.conf or application.yaml file:

    http { custom_header { header_name = X-Another-Custom-Header header_value = Some value } }
    http: custom_header: header_name: X-Another-Custom-Header header_value: Some value

    In our example, the plugin settings are stored in the http.custom_header group.

  2. To get access to configuration file properties, pass ApplicationConfig to the configuration class constructor. The tryGetString function returns the specified property value:

    class CustomHeaderConfiguration(config: ApplicationConfig) { var headerName: String = config.tryGetString("header_name") ?: "Custom-Header-Name" var headerValue: String = config.tryGetString("header_value") ?: "Default value" }
  3. Finally, assign the http.custom_header value to the configurationPath parameter of the createApplicationPlugin function:

    val CustomHeaderPluginConfigurable = createApplicationPlugin( name = "CustomHeaderPluginConfigurable", configurationPath = "http.custom_header", createConfiguration = ::CustomHeaderConfiguration ) { val headerName = pluginConfig.headerName val headerValue = pluginConfig.headerValue pluginConfig.apply { onCall { call -> call.response.headers.append(headerName, headerValue) } } }

Access application settings

Configuration

You can access your server configuration using the applicationConfig property, which returns the ApplicationConfig instance. The example below shows how to get a host and port used by the server:

val SimplePlugin = createApplicationPlugin(name = "SimplePlugin") { val host = applicationConfig?.host val port = applicationConfig?.port println("Listening on $host:$port") }

Environment

To access the application's environment, use the environment property. For example, this property allows you to determine whether the development mode is enabled:

val SimplePlugin = createApplicationPlugin(name = "SimplePlugin") { val isDevMode = environment?.developmentMode onCall { call -> if (isDevMode == true) { println("handling request ${call.request.uri}") } } }

Miscellaneous

Store plugin state

To store a plugin's state, you can capture any value from handler lambda. Note that it is recommended to make all state values thread safe by using concurrent data structures and atomic data types:

val SimplePlugin = createApplicationPlugin(name = "SimplePlugin") { val activeRequests = AtomicInteger(0) onCall { activeRequests.incrementAndGet() } onCallRespond { activeRequests.decrementAndGet() } }

Databases

  • Can I use a custom plugin with suspendable databases?

    Yes. All the handlers are suspending functions, so you can perform any suspendable database operations inside your plugin. But don't forget to deallocate resources for specific calls (for example, by using on(ResponseSent)).

  • How to use a custom plugin with blocking databases?

    As Ktor uses coroutines and suspending functions, making a request to a blocking database can be dangerous because a coroutine that performs a blocking call can be blocked and then suspended forever. To prevent this, you need to create a separate CoroutineContext:

    val databaseContext = newSingleThreadContext("DatabaseThread")

    Then, once your context is created, wrap each call to your database into withContext call:

    onCall { withContext(databaseContext) { database.access(...) // some call to your database } }
Last modified: 30 April 2024