Ktor 2.3.9 Help

Custom plugins - Base API

Ktor exposes API for developing custom plugins that implement common functionalities and can be reused in multiple applications. This API allows you to intercept different pipeline phases to add custom logic to request/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, follow the steps below:

  1. Create a plugin class and declare a companion object that implements one of the following interfaces:

  2. Implement the key and install members of this companion object.

  3. Provide a plugin configuration.

  4. Handle calls by intercepting the required pipeline phases.

  5. Install a plugin.

Create a companion object

A custom plugin's class should have a companion object that implements the BaseApplicationPlugin or BaseRouteScopedPlugin interface. The BaseApplicationPlugin interface accepts three type parameters:

  • A type of pipeline this plugin is compatible with.

  • A configuration object type for this plugin.

  • An instance type of the plugin object.

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

Implement the 'key' and 'install' members

As a descendant of the BaseApplicationPlugin interface, a companion object should implement two members:

  • The key property is used to identify a plugin. Ktor has a map of all attributes, and each plugin adds itself to this map using the specified key.

  • The install function allows you to configure how your plugin works. Here you need to intercept a pipeline and return a plugin instance. We'll take a look at how to intercept a pipeline and handle calls in the next chapter.

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 your 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. So, intercepting a specific pipeline allows you to access different stages of a call, for instance:

  • ApplicationCallPipeline.Monitoring: intercepting this phase can be used for request logging or collecting metrics.

  • ApplicationCallPipeline.Plugins: can be used to modify response parameters, for instance, append custom headers.

  • ApplicationReceivePipeline.Transform and ApplicationSendPipeline.Transform: allow you to obtain and transform data received from the client and transform data before sending it back.

The example below demonstrates how to intercept the ApplicationCallPipeline.Plugins phase and append 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 } } }

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.

Provide plugin configuration

The previous chapter shows 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 inside a plugin's class:

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

Given that plugin configuration fields are mutable, saving them in local variables is recommended:

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

Finally, in the install function, you can get this 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 install function and pass the desired configuration parameters:

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

Examples

The code snippets below demonstrate several examples of custom plugins. You can find the runnable project here: custom-plugin-base-api

Request logging

The example below shows how to create a custom plugin for logging 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

This example demonstrates how to create 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 example below shows how to:

  • transform data received from the client;

  • transform data to be sent to the client.

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).readUTF8Line()?.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 in one or more ordered phases. Each interceptor can perform custom logic before and after processing a request.

ApplicationCallPipeline is a pipeline for executing application calls. This pipeline defines 5 phases:

  • Setup: a phase used for preparing a call and its attributes for processing.

  • Monitoring: a phase for tracing calls. It might be useful for request logging, collecting metrics, error handling, and so on.

  • Plugins: a phase used to handle calls. Most plugins intercept at this phase.

  • Call: a phase used to complete a call.

  • Fallback: a phase for handling unprocessed calls.

Mapping of pipeline phases to new API handlers

Starting with v2.0.0, Ktor provides a new simplified 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 various handlers, such as onCall, onCallReceive, onCallRespond, and so on. The table below shows how pipeline phases map to handlers in a new API.

Base API

New API

before ApplicationCallPipeline.Setup

on(CallFailed)

ApplicationCallPipeline.Setup

on(CallSetup)

ApplicationCallPipeline.Plugins

onCall

ApplicationReceivePipeline.Transform

onCallReceive

ApplicationSendPipeline.Transform

onCallRespond

ApplicationSendPipeline.After

on(ResponseBodyReadyForSend)

ApplicationSendPipeline.Engine

on(ResponseSent)

after Authentication.ChallengePhase

on(AuthenticationChecked)

Last modified: 26 December 2022