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.
The API described in this topic is in effect for v2.0.0 and higher. For the old versions, you can use the base API.
In this section, we'll demonstrate how to create and install your first plugin. You can use ktor-get-started-sample as a starting project.
-
To create a plugin, call the
createApplicationPlugin
function and pass a plugin name:{src="snippets/custom-plugin/src/main/kotlin/com/example/plugins/SimplePlugin.kt" lines="3-7"}
This function returns the
ApplicationPlugin
instance that will be used in the next step to install a plugin.There is also the
createRouteScopedPlugin
function allowing you to create plugins that can be installed to a specific route. -
To install a plugin, pass the created
ApplicationPlugin
instance to theinstall
function in the application's initialization code:{src="snippets/custom-plugin/src/main/kotlin/com/example/Application.kt" lines="11-12,32"}
-
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.
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
.
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.
The example below shows how to use onCall
to create a custom plugin for logging incoming requests:
{src="snippets/custom-plugin/src/main/kotlin/com/example/plugins/RequestLoggingPlugin.kt" lines="6-12"}
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
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.
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:
{src="snippets/custom-plugin/post.http"}
To receive this body as an integer value, you need create a route handler for POST
requests and call call.receive
with the Int
parameter:
{src="snippets/custom-plugin/src/main/kotlin/com/example/Application.kt" lines="27-28,30"}
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:
{src="snippets/custom-plugin/src/main/kotlin/com/example/plugins/DataTransformationPlugin.kt" lines="6-16,27"}
transformBody
in the code snippet above works as follows:
TransformContext
is a lambda receiver that contains type information about the current request. In the example above, theTransformContext.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
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:
{src="snippets/custom-plugin/src/main/kotlin/com/example/Application.kt" lines="27-30"}
Calling call.respond
invokes the onCallRespond
, which is 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:
{src="snippets/custom-plugin/src/main/kotlin/com/example/plugins/DataTransformationPlugin.kt" lines="18-26"}
You can find the full example here: DataTransformationPlugin.kt.
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.
The example below shows how to handle CallSetup
:
on(CallSetup) { call->
// ...
}
There is also the
MonitoringEvent
hook that allows you to handle application events, such as application startup or shutdown.
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 use attributes to calculate a time between receiving a request and reading a body:
{src="snippets/custom-plugin/src/main/kotlin/com/example/plugins/DataTransformationBenchmarkPlugin.kt" lines="6-18"}
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.
You can also access call attributes in a route handler.
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
:
on(MonitoringEvent(ApplicationStopped)) {
println("Server is stopped")
}
This might be useful to release application resources.
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:
{src="snippets/custom-plugin/src/main/kotlin/com/example/plugins/CustomHeaderPlugin.kt" lines="18-21"}
To use this configuration in a plugin, pass a configuration class reference to createApplicationPlugin
:
{src="snippets/custom-plugin/src/main/kotlin/com/example/plugins/CustomHeaderPlugin.kt" lines="5-16"}
Given that plugin configuration fields are mutable, saving them in local variables is recommended.
Finally, you can install and configure a plugin as follows:
{src="snippets/custom-plugin/src/main/kotlin/com/example/Application.kt" lines="15-18"}
You can find the full example here: CustomHeaderPlugin.kt.
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")
}
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}")
}
}
}
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()
}
}
-
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 } }