Skip to content

Latest commit

 

History

History
291 lines (197 loc) · 13.7 KB

custom_plugins.md

File metadata and controls

291 lines (197 loc) · 13.7 KB

Starting with v2.0.0, Ktor provides a new 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 the onCall, onCallReceive, and onCallRespond handlers.

The API described in this topic is in effect for v2.0.0 and higher. For the old versions, you can use the base API.

Create and install your first plugin {id="first-plugin"}

In this section, we'll demonstrate how to create and install your first plugin. You can use ktor-get-started-sample as a starting project.

  1. To create a plugin, call the createApplicationPlugin function and pass a plugin name:

    {src="snippets/custom-plugin/src/main/kotlin/com/example/plugins/SimplePlugin.kt" lines="3-7"}

    This function returns the ApplicationPlugin instance that will be used in the next step to install a plugin.

    There is also the createRouteScopedPlugin function allowing you to create plugins that can be installed to a specific route.

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

    {src="snippets/custom-plugin/src/main/kotlin/com/example/Application.kt" lines="11-12,32"}

  3. Finally, run your application to see the plugin's greeting 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

You can find the full example here: SimplePlugin.kt. In the following sections, we'll look at how to handle calls on different stages and provide a plugin configuration.

Handle calls {id="call-handling"}

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 get request/response information, modify response parameters (for instance, append custom headers), and so on.
  • onCallReceive allows you to obtain and transform data received from the client.
  • onCallRespond allows you to transform data before sending it to the client.
  • on(...) allows you to invoke specific hooks that might be useful to handle other stages of a call or exceptions that happened during a call.
  • If required, you can share a call state between different handlers using call.attributes.

onCall {id="on-call"}

The onCall handler accepts the ApplicationCall as a lambda argument. This allows you to access request/response information and modify response parameters (for instance, append custom headers). If you need to transform a request/response body, use onCallReceive/onCallRespond.

Example 1: Request logging {id="request-logging"}

The example below shows how to use onCall to create a custom plugin for logging incoming requests:

{src="snippets/custom-plugin/src/main/kotlin/com/example/plugins/RequestLoggingPlugin.kt" lines="6-12"}

If you install this plugin, the application will show requested URLs in a console, for example:

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

Example 2: Custom header {id="custom-header"}

This example demonstrates how to create a plugin that appends a custom header to each response:

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

As a result, a custom header will be added to all responses:

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

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.

onCallReceive {id="on-call-receive"}

The onCallReceive handler provides the transformBody function and allows you to transform data received from the client. Suppose the client makes a sample POST request that contains 10 as text/plain in its body:

{src="snippets/custom-plugin/post.http"}

To receive this body as an integer value, you need create a route handler for POST requests and call call.receive with the Int parameter:

{src="snippets/custom-plugin/src/main/kotlin/com/example/Application.kt" lines="27-28,30"}

Now let's create a plugin that receives a body as an integer value and adds 1 to it. To do this, we need to handle transformBody inside onCallReceive as follows:

{src="snippets/custom-plugin/src/main/kotlin/com/example/plugins/DataTransformationPlugin.kt" lines="6-16,27"}

transformBody in the code snippet above works as follows:

  1. TransformContext is a lambda receiver that contains type information about the current request. In the example above, the TransformContext.requestedType property is used to check the requested data type.
  2. data is a lambda argument that allows you to receive a request body as ByteReadChannel and convert it to the required type. In the example above, ByteReadChannel.readUTF8Line is used to read a request body.
  3. Finally, you need to transform and return data. In our example, 1 is added to the received integer value.

You can find the full example here: DataTransformationPlugin.kt.

onCallRespond {id="on-call-respond"}

onCallRespond also provides the transformBody handler and allows you to transform data to be sent to the client. This handler is executed when the call.respond function is invoked in a route handler. Let's continue with the example from onCallReceive where an integer value is received in a POST request handler:

{src="snippets/custom-plugin/src/main/kotlin/com/example/Application.kt" lines="27-30"}

Calling call.respond invokes the onCallRespond, which is in turn allows you to transform data to be sent to the client. For example, the code snippet below shows how to add 1 to the initial value:

{src="snippets/custom-plugin/src/main/kotlin/com/example/plugins/DataTransformationPlugin.kt" lines="18-26"}

You can find the full example here: DataTransformationPlugin.kt.

Other useful handlers {id="other"}

Apart from the onCall, onCallReceive, and onCallRespond handlers, Ktor provides a set of specific hooks that might be useful to handle other stages of a call. You can handle these hooks using the on handler that accepts a Hook as a parameter. These hooks include:

  • CallSetup is invoked as a first step in processing a call.
  • ResponseBodyReadyForSend is invoked when a response body comes through all transformations and is ready to be sent.
  • ResponseSent is invoked when a response is successfully sent to a client.
  • CallFailed is invoked when a call fails with an exception.

The example below shows how to handle CallSetup:

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

There is also the MonitoringEvent hook that allows you to handle application events, such as application startup or shutdown.

Share call state {id="call-state"}

Custom plugins allow you to share any value related to a call, so you can access this value inside any handler processing this call. This value is stored as an attribute with a unique key in the call.attributes collection. The example below demonstrates how use attributes to calculate a time between receiving a request and reading a body:

{src="snippets/custom-plugin/src/main/kotlin/com/example/plugins/DataTransformationBenchmarkPlugin.kt" lines="6-18"}

If you make a POST request, the plugin prints a delay in a console:

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

You can find the full example here: DataTransformationBenchmarkPlugin.kt.

You can also access call attributes in a route handler.

Handle application events {id="handle-app-events"}

The on handler provides the ability to use the MonitoringEvent hook to handle events related to an application's lifecycle. For example, you can pass the following predefined events to the on handler:

  • ApplicationStarting
  • ApplicationStarted
  • ApplicationStopPreparing
  • ApplicationStopping
  • ApplicationStopped

The code snippet below shows how to handle application shutdown using ApplicationStopped:

on(MonitoringEvent(ApplicationStopped)) {
    println("Server is stopped")
}

This might be useful to release application resources.

Provide plugin configuration {id="plugin-configuration"}

The Custom header example demonstrates 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:

{src="snippets/custom-plugin/src/main/kotlin/com/example/plugins/CustomHeaderPlugin.kt" lines="18-21"}

To use this configuration in a plugin, pass a configuration class reference to createApplicationPlugin:

{src="snippets/custom-plugin/src/main/kotlin/com/example/plugins/CustomHeaderPlugin.kt" lines="5-16"}

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

Finally, you can install and configure a plugin as follows:

{src="snippets/custom-plugin/src/main/kotlin/com/example/Application.kt" lines="15-18"}

You can find the full example here: CustomHeaderPlugin.kt.

Access application settings {id="app-settings"}

Configuration {id="config"}

You can access your server configuration using the applicationConfig property, which returns the ApplicationConfig instance. The example below shows how to get a 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 {id="environment"}

To access the application's environment, use the environment property. For example, this property allows you to determine whether the 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 {id="misc"}

Store plugin state {id="plugin-state"}

To store a plugin's state, you can capture any value from handler lambda. Note that it is recommended to make all state values thread safe by using concurrent data structures and atomic data types:

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

Databases {id="databases"}

  • Can I use a custom plugin with suspendable databases?

    Yes. All the handlers are suspending functions, so you can perform any suspendable database operations inside your plugin. But don't forget to deallocate resources for specific calls (for example, by using on(ResponseSent)).

  • How to use a custom plugin with blocking databases?

    As Ktor uses coroutines and suspending functions, making a request to a blocking database can be dangerous because a coroutine that performs a blocking call can be blocked and then suspended forever. To prevent this, you need to create a separate CoroutineContext:

    val databaseContext = newSingleThreadContext("DatabaseThread")

    Then, once your context is created, wrap each call to your database into withContext call:

    onCall {
        withContext(databaseContext) {
            database.access(...) // some call to your database
        }
    }