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.
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.To install a plugin, pass the created
ApplicationPlugin
instance to theinstall
function in the application's initialization code:fun Application.module() { install(SimplePlugin) }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:
If you install this plugin, the application will show requested URLs in a console, for example:
Example 2: Custom header
This example demonstrates how to create a plugin that appends a custom header to each response:
As a result, a custom header will be added to all responses:
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:
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:
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:
transformBody
in the code snippet above works as follows:
TransformBodyContext
is a lambda receiver that contains type information about the current request. In the example above, theTransformBodyContext.requestedType
property is used to check the requested data type.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.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:
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:
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
:
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:
If you make a POST
request, the plugin prints a delay in a console:
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
:
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.
First, you need to define a configuration class:
class PluginConfiguration { var headerName: String = "Custom-Header-Name" var headerValue: String = "Default value" }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.
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
:
First, add a new group with the plugin settings to the
application.conf
orapplication.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 valueIn our example, the plugin settings are stored in the
http.custom_header
group.To get access to configuration file properties, pass
ApplicationConfig
to the configuration class constructor. ThetryGetString
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" }Finally, assign the
http.custom_header
value to theconfigurationPath
parameter of thecreateApplicationPlugin
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:
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:
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:
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 } }