Ktor 3.6.0 Help

Custom plugins - Base API

Ktor provides a base API for developing custom plugins that implement reusable functionality across multiple applications.

The base API lets you intercept different pipeline phases and add custom logic to request and response processing. For example, you can intercept the Monitoring phase to log incoming requests or collect metrics.

Create a plugin

To create a custom plugin with the base API:

  1. Create a plugin class and declare a companion object that implements a plugin interface.

  2. Implement the key property and the install() function in the companion object.

  3. Provide a plugin configuration.

  4. Handle calls by intercepting the required pipeline phases.

  5. Install the plugin.

Create a companion object

A custom plugin's class must have a companion object that implements one of the following interfaces:

The BaseApplicationPlugin interface accepts the following type parameters:

  • The pipeline type that the plugin supports.

  • The configuration type for the plugin.

  • The plugin instance type.

class CustomHeader() { companion object Plugin : BaseApplicationPlugin<ApplicationCallPipeline, Configuration, CustomHeader> { // ... } }

Implement the 'key' property and 'install()' function

A companion object that implements BaseApplicationPlugin must define the following:

  • The key property identifies the plugin. Ktor stores plugin instances in the application's attributes and uses this key to access the plugin instance.

  • The install() function configures the plugin. In this function, intercept the required pipeline phases and return the plugin instance. The Handle calls section shows how to intercept a pipeline phase.

class CustomHeader() { companion object Plugin : BaseApplicationPlugin<ApplicationCallPipeline, Configuration, CustomHeader> { override val key = AttributeKey<CustomHeader>("CustomHeader") override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): CustomHeader { val plugin = CustomHeader() // Intercept a pipeline ... return plugin } } }

Handle calls

In a custom plugin, you can handle requests and responses by intercepting existing pipeline phases or newly defined ones. For example, the Authentication plugin adds the Authenticate and Challenge custom phases to the default pipeline.

Intercepting a specific phase gives you access to a specific stage of call processing:

  • ApplicationCallPipeline.Monitoring: use this phase for request logging, metrics, tracing, and similar monitoring tasks.

  • ApplicationCallPipeline.Plugins: use this phase to handle calls or modify response parameters, such as appending custom headers.

  • ApplicationReceivePipeline.Transform and ApplicationSendPipeline.Transform: use these phases to access and transform data received from the client or sent to the client.

The following example intercepts the ApplicationCallPipeline.Plugins phase and appends a custom header to each response:

class CustomHeader() { companion object Plugin : BaseApplicationPlugin<ApplicationCallPipeline, Configuration, CustomHeader> { override val key = AttributeKey<CustomHeader>("CustomHeader") override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): CustomHeader { val plugin = CustomHeader() pipeline.intercept(ApplicationCallPipeline.Plugins) { call.response.header("X-Custom-Header", "Hello, world!") } return plugin } } }

In this example, the header name and value are hardcoded. To make the plugin reusable, provide a configuration that lets users specify the header name and value.

Provide plugin configuration

The previous section shows how to create a plugin that appends a predefined custom header to each response. To make this plugin reusable, define a configuration that lets users specify the header name and value.

First, define a configuration class inside the plugin class:

class Configuration { var headerName = "Custom-Header-Name" var headerValue = "Default value" }

You can update plugin configuration properties during plugin installation. If the plugin uses these values in interceptors, store them in local variables inside the install() function:

class CustomHeader(configuration: Configuration) { private val name = configuration.headerName private val value = configuration.headerValue class Configuration { var headerName = "Custom-Header-Name" var headerValue = "Default value" } }

Then, in the install() function, read the configuration and use its properties:

class CustomHeader(configuration: Configuration) { private val name = configuration.headerName private val value = configuration.headerValue class Configuration { var headerName = "Custom-Header-Name" var headerValue = "Default value" } companion object Plugin : BaseApplicationPlugin<ApplicationCallPipeline, Configuration, CustomHeader> { override val key = AttributeKey<CustomHeader>("CustomHeader") override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): CustomHeader { val configuration = Configuration().apply(configure) val plugin = CustomHeader(configuration) pipeline.intercept(ApplicationCallPipeline.Plugins) { call.response.header(plugin.name, plugin.value) } return plugin } } }

Install a plugin

To install a custom plugin to your application, call the Application.install() function and pass the required configuration parameters:

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

Examples

The following examples show several custom plugins built with the base API.

Request logging

The following example creates a custom plugin that logs incoming requests:

package com.example.plugins import io.ktor.serialization.* import io.ktor.server.application.* import io.ktor.server.plugins.* import io.ktor.util.* class RequestLogging { companion object Plugin : BaseApplicationPlugin<ApplicationCallPipeline, Configuration, RequestLogging> { override val key = AttributeKey<RequestLogging>("RequestLogging") override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): RequestLogging { val plugin = RequestLogging() pipeline.intercept(ApplicationCallPipeline.Monitoring) { call.request.origin.apply { println("Request URL: $scheme://$localHost:$localPort$uri") } } return plugin } } }

Custom header

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

package com.example.plugins import io.ktor.server.application.* import io.ktor.server.response.* import io.ktor.util.* class CustomHeader(configuration: Configuration) { private val name = configuration.headerName private val value = configuration.headerValue class Configuration { var headerName = "Custom-Header-Name" var headerValue = "Default value" } companion object Plugin : BaseApplicationPlugin<ApplicationCallPipeline, Configuration, CustomHeader> { override val key = AttributeKey<CustomHeader>("CustomHeader") override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): CustomHeader { val configuration = Configuration().apply(configure) val plugin = CustomHeader(configuration) pipeline.intercept(ApplicationCallPipeline.Plugins) { call.response.header(plugin.name, plugin.value) } return plugin } } }

Body transformation

The following example creates a plugin that transforms request and response bodies:

package com.example.plugins import io.ktor.serialization.* import io.ktor.server.application.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.util.* import io.ktor.utils.io.* class DataTransformation { companion object Plugin : BaseApplicationPlugin<ApplicationCallPipeline, Configuration, DataTransformation> { override val key = AttributeKey<DataTransformation>("DataTransformation") override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): DataTransformation { val plugin = DataTransformation() pipeline.receivePipeline.intercept(ApplicationReceivePipeline.Transform) { data -> val newValue = (data as ByteReadChannel).readLine()?.toInt()?.plus(1) if (newValue != null) { proceedWith(newValue) } } pipeline.sendPipeline.intercept(ApplicationSendPipeline.Transform) { data -> if (subject is Int) { val newValue = data.toString().toInt() + 1 proceedWith(newValue.toString()) } } return plugin } } }

Pipelines

A Pipeline in Ktor is a collection of interceptors grouped into one or more ordered phases. Each interceptor can run custom logic before and after request processing continues.

ApplicationCallPipeline executes application calls. It defines the following phases:

  • Setup: prepares a call and its attributes for processing.

  • Monitoring: traces calls. Use this phase for request logging, metrics, error handling, and similar tasks.

  • Plugins: handles calls. Most plugins intercept this phase.

  • Call: completes a call.

  • Fallback: handles calls that were not processed by earlier phases.

Mapping of pipeline phases to new API handlers

You can use the simplified custom plugins API to create custom plugins. In most cases, this API does not require direct knowledge of internal Ktor concepts, such as pipelines and phases. Instead, it provides handlers such as onCall(), onCallReceive(), and onCallRespond() for different stages of request and response handling.

The following table shows how base API pipeline phases map to simplified API handlers:

Base API

New API

before ApplicationCallPipeline.Setup

on(CallFailed)

ApplicationCallPipeline.Setup

on(CallSetup)

ApplicationCallPipeline.Plugins

onCall()

ApplicationCallPipeline.Call

onCallValidators()

ApplicationReceivePipeline.Transform

onCallReceive()

ApplicationSendPipeline.Transform

onCallRespond()

ApplicationSendPipeline.After

on(ResponseBodyReadyForSend)

ApplicationSendPipeline.Engine

on(ResponseSent)

after Authentication.ChallengePhase

on(AuthenticationChecked)

01 September 2026