Ktor 3.6.0 Help

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.

  1. 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 ApplicationPlugin instance that you can install in your application.

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

    fun Application.module() { install(SimplePlugin) }

  3. 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:

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

When you install this plugin, it prints requested URLs to the console:

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

Example 2: Add a custom header

The following example creates a plugin that adds a custom header to each response:

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

The resulting response includes the custom header:

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

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:

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>() }

The following plugin receives a body as an integer value and adds 1 to it:

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

In the above example:

  • TransformBodyContext is the lambda receiver. Its requestedType property contains information about the type requested by call.receive().

  • The data argument contains the current request body. In this case, it is a ByteReadChannel and ByteReadChannel.readLine() reads its contents.

  • If the requested type is Int, the plugin converts the received value to an integer, adds 1, 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:

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.

Inside onCallRespond(), use transformBody() to transform the response body. The following example adds 1 to an integer response and converts it to a string:

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

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:

val UserValidationPlugin = createRouteScopedPlugin("UserValidationPlugin") { onCallValidators { call -> val principal = call.principal<UserIdPrincipal>() if (principal != null) { call.application.log.info("Validating request for ${principal.name}") } } }

Install the plugin inside the authenticated route to run it after authentication:

routing { authenticate("auth") { install(UserValidationPlugin) get("/api") { call.respondText("OK") } } }

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:

  • CallSetup is invoked at the beginning of call processing.

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

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

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

  • AuthenticationChecked is 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:

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

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:

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}") } }

When you send a POST request, the plugin prints the delay to the console:

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

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:

  • ApplicationStarting

  • ApplicationStarted

  • ApplicationStopPreparing

  • ApplicationStopping

  • ApplicationStopped

The following example handles application shutdown using the ApplicationStopped event:

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 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.

  1. Define a configuration class:

    class PluginConfiguration { var headerName: String = "Custom-Header-Name" var headerValue: String = "Default value" }

  2. 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.

  3. 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.

  1. Add a new group with the plugin settings to your 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 this 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 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" }

  3. 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

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:

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

Environment

Use the environment property to access the application's environment. For example, you can check whether 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

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:

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

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:

val databaseContext = Dispatchers.IO

Then wrap each blocking database call in withContext():

onCall { withContext(databaseContext) { database.access(...) // A call to your database } }
17 September 2026