Custom server plugins
Ktor allows you to create your own custom plugins. In general, this API doesn't require an understanding of internal Ktor concepts, such as pipelines and phases. Instead, you use handlers such as onCall(), onCallReceive(), and onCallRespond() to access different stages of requests and response handling.
Create and install your first plugin
In this section, you'll learn 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 specify a plugin name:import io.ktor.server.application.* val SimplePlugin = createApplicationPlugin(name = "SimplePlugin") { println("SimplePlugin is installed!") }This function returns the
ApplicationPlugininstance that you can install in your application.To install a plugin, pass the created
ApplicationPlugininstance to theApplication.install()function in your application's initialization code:fun Application.module() { install(SimplePlugin) }Run your application to see the plugin message 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
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 access request and response information and modify response parameters, such as headers.onCallValidators()allows you perform call validation. For route-scoped plugins, validators execute according to route nesting.onCallReceive()allows you to transform data received from the client.onCallRespond()allows you to transform data before sending it to the client.on()allows you to handle specific hooks for other stages of call processing or for exceptions that occur during a call.
You can also share call state between handlers using call.attributes.
onCall()
The onCall() handler accepts ApplicationCall as a lambda argument. This allows you to access request and response information and modify response parameters, such as appending custom headers.
To transform a request or response body, use onCallReceive() and onCallRespond().
Example 1: Log requests
The following example uses onCall() to create a plugin that logs incoming request URLs:
When you install this plugin, it prints requested URLs to the console:
Example 2: Add a custom header
The following example creates a plugin that adds a custom header to each response:
The resulting response includes the custom header:
In this example, the header name and value are hardcoded. To make them configurable, provide a plugin configuration.
onCallReceive()
The onCallReceive() handler allows you to transform data received from the client. Inside the handler, call transformBody() to transform the request body before it is passed to call.receive().
Suppose a client sends the following POST request that contains 10 as a text/plain 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:
The following plugin receives a body as an integer value and adds 1 to it:
In the above example:
TransformBodyContextis the lambda receiver. ItsrequestedTypeproperty contains information about the type requested bycall.receive().The
dataargument contains the current request body. In this case, it is aByteReadChannelandByteReadChannel.readLine()reads its contents.If the requested type is
Int, the plugin converts the received value to an integer, adds1, and returns the transformed value. Otherwise, it returns the body unchanged.
onCallRespond()
The onCallRespond() handler allows you to transform data before it is sent to the client. This handler is executed when the call.respond function is invoked in a route handler.
For example, consider the following route:
Calling call.respond invokes onCallRespond(), which in turn allows you to transform data to be sent to the client.
Inside onCallRespond(), use transformBody() to transform the response body. The following example adds 1 to an integer response and converts it to a string:
onCallValidators()
The onCallValidators() handler allows you to perform validation for each incoming call.
When multiple validators are applied to nested routes, validators on parent routes execute before validators on child routes. This allows a validator to use information produced earlier in the route hierarchy, such as an authenticated principal.
For example, the following route-scoped plugin can access a principal provided by an authentication route:
Install the plugin inside the authenticated route to run it after authentication:
Use onCallValidators() when the order of route-scoped validation matters. For general request and response processing that does not depend on other validators, use onCall() instead.
Other useful handlers
In addition to the call handlers described above, Ktor provides a set of hooks for handling other stages of call processing. Use the on() function to register a handler for a specific Hook.
Available hooks include:
CallSetupis invoked at the beginning of call processing.ResponseBodyReadyForSendis invoked after a response body comes through all transformations and is ready to be sent.ResponseSentis invoked after a response is successfully sent to a client.CallFailedis invoked when call processing fails with an exception.AuthenticationCheckedis invoked after authentication credentials are checked. You can use this hook to implement authorization. For an example, see custom-plugin-authorization.
The following example handles the CallSetup hook:
Share call state
Custom plugins can share values associated with a call between different handlers. These values are stored in the call.attributes collection using a unique AttributeKey.
The following example stores the time when onCall() is invoked and uses it in onCallReceive() to calculate the delay before the request body is read:
When you send a POST request, the plugin prints the delay to the console:
Handle application events
The on() handler provides the ability to use the MonitoringEvent hook to handle events related to an application's lifecycle.
Ktor provides the following predefined events to the on() handler:
ApplicationStartingApplicationStartedApplicationStopPreparingApplicationStoppingApplicationStopped
The following example handles application shutdown using the ApplicationStopped event:
This approach is useful for cleaning up resources owned by a plugin, such as closing connections, stopping background tasks, or flushing buffered data.
Provide plugin configuration
The custom header example creates a plugin that appends a predefined header to each response. To make this plugin reusable, define a configuration that lets users specify the header name and value.
Define a configuration class:
class PluginConfiguration { var headerName: String = "Custom-Header-Name" var headerValue: String = "Default value" }Pass the 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) } } }Plugin configuration properties are mutable during plugin installation. If the plugin uses these values in handlers, store them in local variables inside the plugin body.
Install and configure the plugin:
install(CustomHeaderPlugin) { headerName = "X-Custom-Header" headerValue = "Hello, world!" }
Configuration in a file
Ktorcan load plugin settings from a configuration file.
The following example shows how to configure CustomHeaderPlugin from a file.
Add a new group with the plugin settings to your
application.conforapplication.yamlfile: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 this example, the plugin settings are stored in the
http.custom_headergroup.To get access to configuration file properties, pass
ApplicationConfigto the configuration class constructor. ThetryGetString()function returns the value of the specified property:class CustomHeaderConfiguration(config: ApplicationConfig) { var headerName: String = config.tryGetString("header_name") ?: "Custom-Header-Name" var headerValue: String = config.tryGetString("header_value") ?: "Default value" }Assign the
http.custom_headervalue to theconfigurationPathparameter 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
Custom plugins can access application-level settings from the plugin body. This is useful when plugin behavior depends on the server configuration or environment.
Configuration
Use the applicationConfig property to access server configuration. This property returns an ApplicationConfig instance.
The following example reads the host and port used by the server:
Environment
Use the environment property to access the application's environment. For example, you can check whether development mode is enabled:
Miscellaneous
Store plugin state
A plugin can store state by capturing values in the plugin body and using them from handler lambdas.
Because plugins can handle multiple calls concurrently, store shared mutable state in thread-safe structures, such as concurrent collections or atomic types:
Databases
Use suspending database APIs
All custom plugin handlers are suspending functions. This means you can call suspending database APIs directly from a handler.
Remember to release resources that are scoped to a specific call. For example, you can use on(ResponseSent) to clean up resources after a response has been sent.
Use blocking database APIs
Ktor uses coroutines, so blocking database calls should not run on the default coroutine dispatcher. A blocking call can occupy a thread and prevent other coroutines from progressing.
To call a blocking database API, create a separate CoroutineContext for blocking work:
Then wrap each blocking database call in withContext():