Introduction
In the Ktor framework, there are many plugins that allow developers to solve almost any tasks faced by client-server application developers. For example, authentication, cookies management, establishing WebSocket connections. What if you have a function that is not implemented in Ktor plugins? Will you have to give up the convenience of Ktor plugins? Of course not! You can write your custom plugin!
Creating and installation
To create your plugin, use the createApplicationPlugin function:
val LocalizationPlugin = createApplicationPlugin(name = "LocalizationPlugin") {
println("LocalizationPlugin is installed!")
}This function requires two parameters: a String name that identifies the plugin and a PluginBuilder<Unit>.() -> Unit body that defines the plugin's behavior. We pass the body as a lambda because it's really convenient. In the code above, we passed the println function that outputs a message to the console when the plugin is installed.
Ktor also provides the createRouteScopedPlugin function, enabling you to create plugins that can be specifically assigned to a particular route.
Now let's install the plugin just as we did before with any other plugin:
fun Application.module() {
install(LocalizationPlugin)
configureRouting()
}Our plugin doesn't have much functionality yet, so we're not passing any additional parameters during installation. However, later in this topic, we will explore how to add configuration parameters to our plugin.
Let's run our server and see what we have in the console:
Here, we see the string printed by our plugin. We've created our first plugin! Now, let's add something more to it than just installation.
Handle calls
To add functionality to our plugin, we need to have the ability to handle client requests and server responses. For this purpose, we will use a set of handlers, which are functions that are called at specific points during the processing of a request and provide access to different stages of a call. The main handlers are the following:
onCall: This handler provides anApplicationCallinstance that allows you to access both request and response objects. You can use this handler to inspect the request details, modify the response parameters.onCallReceive: This handler is triggered when the server is ready to read the incoming request body. It provides you with a powerful way to intercept and transform the data received from the client before it reaches your route handlers. For example, you could use this handler to decrypt encrypted data, deserialize complex data structures, or validate incoming data.onCallRespond: This handler comes into play when the server is about to send the response body back to the client. It provides a final opportunity to transform or modify the data before it's sent over the network. This can be used for several purposes, such as serializing your data into the desired format, encrypting sensitive data, or even adding additional data to the response.
In our plugin, we don't intend to process data received from the client or transform data to be sent to the client, so we will use the onCall handler:
onCall { call ->
val preferredLanguage = call.request.queryParameters["lang"] ?: "en"
val localizedGreeting = fetchLocalizedGreeting(preferredLanguage)
call.attributes.put(AttributeKey<String>("LocalizationPlugin"), localizedGreeting)
}Here is our fetchLocalizedGreeting function:
fun fetchLocalizedGreeting(language: String): String {
return when (language) {
"es" -> "¡Hola, bienvenido a nuestra aplicación!"
"ru" -> "Здравствуйте, добро пожаловать в наше приложение!"
"uk" -> "Привіт! Ласкаво просимо до нашої програми!"
else -> "Hello, welcome to our application!"
}
}Now, let's add a simple routing where we will test our plugin:
fun Application.configureRouting() {
routing {
get("/international-greetings") {
call.respondText(call.attributes[AttributeKey<String>("LocalizationPlugin")])
}
}
}As you may have noticed, we used call.attributes to pass the greeting string from the plugin to the route. Attributes are a way to share data between different parts of your application and are often used to propagate plugin state information.
Please note that this example is kept simple for educational purposes and focuses on the core concepts of creating a custom plugin. In a real-world scenario, you might want to consider more advanced features, such as using external resource files for translations, supporting additional languages, and handling more complex use cases.
Now let's set up Postman to conveniently add different query parameters. To do this, simply add a few parameters to the request:
As you can see in the image above, our plugin correctly identified the language and greeted the user in the intended language.
Configuring
To make your plugin more flexible and adaptable to different needs, Ktor provides a way to configure your plugin using a configuration class. The configuration class is a simple Kotlin class that contains properties for each of the configurable aspects of your plugin.
To use a configuration class in your plugin, you need to pass a reference to it to the createApplicationPlugin function (this function has three signatures) through the createConfiguration argument. This argument is a function that creates and returns an instance of your configuration class.
Here's an example of a configuration class for a plugin that sets a default language:
class PluginConfiguration {
var defaultLanguage: String = "en"
}This class is then used in the createApplicationPlugin function as follows:
val LocalizationPlugin = createApplicationPlugin(name = "LocalizationPlugin", createConfiguration = ::PluginConfiguration) {
println("LocalizationPlugin is installed!")
onCall { call ->
// now we use pluginConfig to be able to set default language more flexible
val defaultLanguage = pluginConfig.defaultLanguage
val preferredLanguage = call.request.queryParameters["lang"] ?: defaultLanguage
val localizedGreeting = fetchLocalizedGreeting(preferredLanguage)
call.attributes.put(AttributeKey<String>("LocalizationPlugin"), localizedGreeting)
}
}In the code above, ::PluginConfiguration is a reference to the constructor of the PluginConfiguration class. When Ktor initializes the plugin, it will call this function to create a new instance of the configuration class.
Inside the plugin's code block, you can access the properties of the configuration instance using the pluginConfig property.
Once you've defined a configuration for your plugin, you can set the configuration properties when you install the plugin. You can do this by passing a lambda to the install function where you set the properties of the configuration:
install(LocalizationPlugin) {
defaultLanguage = "uk" // set the default language to Ukranian
}Now, let's try to omit query parameters and send the request:
And here are our Ukrainian greetings!
In this way, you can customize the behavior of the plugin when you install it in your application, making the plugin more flexible and reusable. The configuration is created when the plugin is installed and then used for the lifetime of the plugin.
Under the hood
When creating custom plugins in Ktor, it's crucial to understand the underlying mechanism that makes it all possible, the pipelines. These pipelines are at the core of Ktor's request processing, providing a deeper insight into how your plugins interact with the framework.
The ApplicationCallPipeline is a sequence of stages, or phases, that every request goes through. Each phase represents a specific stage in the processing of a request. The main phases are: Setup, Monitoring, Plugins, Call, and Fallback.
In addition to the ApplicationCallPipeline, Ktor also uses specialized pipelines to handle data request and sending: the ApplicationReceivePipeline and the ApplicationSendPipeline.
The ApplicationReceivePipeline is responsible for processing incoming data from the client. It is invoked when the server is ready to read the incoming request body. This pipeline allows you to transform the received data before it reaches your route handlers.
The ApplicationSendPipeline, on the other hand, is used when the server is about to send the response body back to the client. It provides a final opportunity to transform or modify the data before it's sent over the network.
Both ApplicationReceivePipeline and ApplicationSendPipeline are sub-pipelines of the ApplicationCallPipeline. They are invoked during specific phases of the ApplicationCallPipeline. That means that when you're working with handlers like onCallReceive or onCallRespond, you're actually interacting with these specialized pipelines within the context of the main ApplicationCallPipeline.
Each of the handlers and hooks in a Ktor plugin corresponds to one of these pipelines phases:
The
on(CallFailed)hook is invoked before theApplicationCallPipeline.Setupphase.The
on(CallSetup)hook corresponds to theApplicationCallPipeline.Setupphase.The
onCallhandler intercepts theApplicationCallPipeline.Pluginsphase.The
onCallReceivehandler is invoked during theApplicationReceivePipeline.Transformphase, which is a part of the separateApplicationReceivePipeline.The
onCallRespondhandler corresponds to theApplicationSendPipeline.Transformphase, which is a part of theApplicationSendPipeline.The
on(ResponseBodyReadyForSend)hook is invoked during theApplicationSendPipeline.Afterphase.The
on(ResponseSent)hook is invoked during theApplicationSendPipeline.Enginephase.The
on(AuthenticationChecked)hook is invoked after theAuthentication.ChallengePhase.
As you may have noticed in the list above, new hooks and handlers have appeared that we didn't mention before.
These handlers and hooks give you the ability to interact with and modify the processing of a call at various stages. Understanding which phase each handler corresponds to allows you to better predict and control how your plugin will interact with the call pipeline.
To learn more about the ApplicationCallPipeline, its stages and how they are intercepted, see the intercepting routes topic. If you want to know more about using handlers, you can read official documentation.
Conclusion
Creating custom plugins in Ktor is a powerful way to add functionality to your application or modify the behavior of the framework. By understanding how to create, install, and configure plugins, as well as how they work under the hood, you can leverage this feature to make your Ktor applications more flexible and maintainable.