0
# Plugin System and Extensions
1
2
Plugin architecture for extending Ktor functionality with application-level and route-scoped plugins, providing lifecycle management, configuration systems, and extensible middleware patterns.
3
4
## Capabilities
5
6
### Plugin Architecture
7
8
Core plugin interfaces and implementation patterns for creating reusable extensions to Ktor applications.
9
10
```kotlin { .api }
11
/**
12
* Base plugin interface for all Ktor plugins
13
*/
14
interface Plugin<
15
in TPipeline : Pipeline<*, PipelineCall>,
16
out TConfiguration : Any,
17
TPlugin : Any
18
> {
19
/** Unique key for plugin identification */
20
val key: AttributeKey<TPlugin>
21
/** Install plugin into pipeline */
22
fun install(pipeline: TPipeline, configure: TConfiguration.() -> Unit): TPlugin
23
}
24
25
/**
26
* Interface for application-level plugins
27
*/
28
interface BaseApplicationPlugin<
29
TPipeline : ApplicationCallPipeline,
30
TConfiguration : Any,
31
TPlugin : Any
32
> : Plugin<TPipeline, TConfiguration, TPlugin>
33
34
/**
35
* Interface for simple application plugins
36
*/
37
interface ApplicationPlugin<TConfiguration : Any> :
38
BaseApplicationPlugin<ApplicationCallPipeline, TConfiguration, PluginInstance>
39
40
/**
41
* Interface for route-scoped plugins
42
*/
43
interface BaseRouteScopedPlugin<TConfiguration : Any, TPlugin : Any> :
44
Plugin<ApplicationCallPipeline, TConfiguration, TPlugin>
45
46
/**
47
* Interface for simple route-scoped plugins
48
*/
49
interface RouteScopedPlugin<TConfiguration : Any> :
50
BaseRouteScopedPlugin<TConfiguration, PluginInstance>
51
52
/**
53
* Builder for creating plugin configurations
54
*/
55
class PluginBuilder<TConfiguration : Any> {
56
/** Plugin configuration instance */
57
var pluginConfig: TConfiguration? = null
58
/** Called during call processing */
59
fun onCall(block: suspend PipelineContext<Unit, ApplicationCall>.(ApplicationCall) -> Unit)
60
/** Called when receiving request content */
61
fun onCallReceive(block: suspend PipelineContext<ApplicationReceiveRequest, ApplicationCall>.(ApplicationReceiveRequest) -> Unit)
62
/** Called when sending response content */
63
fun onCallRespond(block: suspend PipelineContext<Any, ApplicationCall>.(Any) -> Unit)
64
}
65
66
/**
67
* Builder for creating route-scoped plugin configurations
68
*/
69
class RouteScopedPluginBuilder<TConfiguration : Any> {
70
/** Plugin configuration instance */
71
var pluginConfig: TConfiguration? = null
72
/** Called during call processing */
73
fun onCall(block: suspend PipelineContext<Unit, ApplicationCall>.(ApplicationCall) -> Unit)
74
/** Called when receiving request content */
75
fun onCallReceive(block: suspend PipelineContext<ApplicationReceiveRequest, ApplicationCall>.(ApplicationReceiveRequest) -> Unit)
76
/** Called when sending response content */
77
fun onCallRespond(block: suspend PipelineContext<Any, ApplicationCall>.(Any) -> Unit)
78
}
79
```
80
81
### Plugin Creation
82
83
Factory functions for creating custom application and route-scoped plugins with configuration support.
84
85
```kotlin { .api }
86
/**
87
* Creates an application plugin with configuration
88
* @param name - Plugin name for identification
89
* @param createConfiguration - Function to create default configuration
90
* @param body - Plugin builder configuration
91
*/
92
fun <PluginConfigT : Any> createApplicationPlugin(
93
name: String,
94
createConfiguration: () -> PluginConfigT,
95
body: PluginBuilder<PluginConfigT>.() -> Unit
96
): ApplicationPlugin<PluginConfigT>
97
98
/**
99
* Creates an application plugin with configuration path
100
* @param name - Plugin name for identification
101
* @param configurationPath - Path in configuration file to plugin config
102
* @param createConfiguration - Function to create configuration from ApplicationConfig
103
* @param body - Plugin builder configuration
104
*/
105
fun <PluginConfigT : Any> createApplicationPlugin(
106
name: String,
107
configurationPath: String,
108
createConfiguration: (config: ApplicationConfig) -> PluginConfigT,
109
body: PluginBuilder<PluginConfigT>.() -> Unit
110
): ApplicationPlugin<PluginConfigT>
111
112
/**
113
* Creates a simple application plugin without configuration
114
* @param name - Plugin name
115
* @param body - Plugin installation logic
116
*/
117
fun createApplicationPlugin(
118
name: String,
119
body: PluginBuilder<Unit>.() -> Unit
120
): ApplicationPlugin<Unit>
121
122
/**
123
* Creates a route-scoped plugin with configuration
124
* @param name - Plugin name for identification
125
* @param createConfiguration - Function to create default configuration
126
* @param body - Plugin builder configuration
127
*/
128
fun <PluginConfigT : Any> createRouteScopedPlugin(
129
name: String,
130
createConfiguration: () -> PluginConfigT,
131
body: RouteScopedPluginBuilder<PluginConfigT>.() -> Unit
132
): RouteScopedPlugin<PluginConfigT>
133
134
/**
135
* Creates a simple route-scoped plugin without configuration
136
* @param name - Plugin name
137
* @param body - Plugin installation logic
138
*/
139
fun createRouteScopedPlugin(
140
name: String,
141
body: RouteScopedPluginBuilder<Unit>.() -> Unit
142
): RouteScopedPlugin<Unit>
143
```
144
145
### Plugin Installation
146
147
Functions for installing plugins into applications and routes with configuration support.
148
149
```kotlin { .api }
150
/**
151
* Install application plugin with configuration
152
* @param plugin - Plugin to install
153
* @param configure - Configuration block
154
*/
155
fun <TConfiguration : Any> Application.install(
156
plugin: ApplicationPlugin<TConfiguration>,
157
configure: TConfiguration.() -> Unit = {}
158
): PluginInstance
159
160
/**
161
* Install base application plugin with configuration
162
* @param plugin - Plugin to install
163
* @param configure - Configuration block
164
*/
165
fun <TConfiguration : Any, TPlugin : Any> ApplicationCallPipeline.install(
166
plugin: BaseApplicationPlugin<*, TConfiguration, TPlugin>,
167
configure: TConfiguration.() -> Unit = {}
168
): TPlugin
169
170
/**
171
* Get installed plugin instance
172
* @param plugin - Plugin to get
173
* @return Plugin instance or null if not installed
174
*/
175
fun <TPlugin : Any> ApplicationCallPipeline.pluginOrNull(
176
plugin: Plugin<*, *, TPlugin>
177
): TPlugin?
178
179
/**
180
* Get installed plugin instance
181
* @param plugin - Plugin to get
182
* @return Plugin instance
183
* @throws PluginNotInstalledException if plugin not installed
184
*/
185
fun <TPlugin : Any> ApplicationCallPipeline.plugin(
186
plugin: Plugin<*, *, TPlugin>
187
): TPlugin
188
189
/**
190
* Install route-scoped plugin with configuration
191
* @param plugin - Plugin to install
192
* @param configure - Configuration block
193
*/
194
fun <TConfiguration : Any> Route.install(
195
plugin: RouteScopedPlugin<TConfiguration, *>,
196
configure: TConfiguration.() -> Unit = {}
197
)
198
199
/**
200
* Get installed plugin instance
201
* @param plugin - Plugin key to retrieve
202
* @return Plugin instance
203
*/
204
fun <A : Any, B : Any> Application.plugin(plugin: Plugin<A, B, *>): B
205
206
/**
207
* Check if plugin is installed
208
* @param plugin - Plugin to check
209
* @return True if plugin is installed
210
*/
211
fun Application.pluginOrNull(plugin: Plugin<*, *, *>): Any?
212
```
213
214
### Built-in Plugin Exceptions
215
216
Standard exception types for plugin error handling and request validation.
217
218
```kotlin { .api }
219
/**
220
* Exception for bad request errors (400)
221
*/
222
class BadRequestException(message: String, cause: Throwable? = null) : Exception(message, cause)
223
224
/**
225
* Exception for not found errors (404)
226
*/
227
class NotFoundException(message: String? = null, cause: Throwable? = null) : Exception(message, cause)
228
229
/**
230
* Exception for unsupported media type errors (415)
231
*/
232
class UnsupportedMediaTypeException(contentType: ContentType) : Exception("Content type $contentType is not supported")
233
234
/**
235
* Exception for missing request parameters
236
*/
237
class MissingRequestParameterException(parameterName: String) : BadRequestException("Request parameter $parameterName is missing")
238
239
/**
240
* Exception for parameter conversion errors
241
*/
242
class ParameterConversionException(
243
parameterName: String,
244
type: String,
245
cause: Throwable? = null
246
) : BadRequestException("Value for parameter $parameterName cannot be converted to $type", cause)
247
248
/**
249
* Exception for configuration errors
250
*/
251
class ConfigurationException(message: String, cause: Throwable? = null) : Exception(message, cause)
252
```
253
254
### Plugin Configuration
255
256
Base classes and patterns for plugin configuration with validation and type safety.
257
258
```kotlin { .api }
259
/**
260
* Base interface for plugin configurations
261
*/
262
interface PluginConfiguration
263
264
/**
265
* Configuration builder with validation
266
*/
267
abstract class ConfigurationBuilder {
268
/** Validate configuration before installation */
269
abstract fun validate()
270
/** Build final configuration */
271
abstract fun build(): PluginConfiguration
272
}
273
274
/**
275
* Attribute key for storing plugin instances
276
*/
277
data class AttributeKey<T>(val name: String) {
278
companion object {
279
/** Create new attribute key */
280
fun <T> create(name: String): AttributeKey<T> = AttributeKey(name)
281
}
282
}
283
```
284
285
### Plugin Lifecycle Hooks
286
287
Hooks for plugin lifecycle management and integration with application events.
288
289
```kotlin { .api }
290
/**
291
* Plugin lifecycle events
292
*/
293
interface PluginLifecycle {
294
/** Called after plugin installation */
295
fun onInstall() {}
296
/** Called when application starts */
297
fun onStart() {}
298
/** Called when application stops */
299
fun onStop() {}
300
/** Called when application configuration changes */
301
fun onConfigurationChange() {}
302
}
303
304
/**
305
* Hook definition for plugin events
306
*/
307
class PluginHook<T : Function<Unit>> {
308
/** Install event handler */
309
fun install(handler: T)
310
/** Uninstall event handler */
311
fun uninstall(handler: T)
312
}
313
```
314
315
**Usage Examples:**
316
317
```kotlin
318
import io.ktor.server.application.*
319
import io.ktor.server.plugins.*
320
import io.ktor.server.response.*
321
import io.ktor.server.routing.*
322
323
// Creating a simple application plugin
324
val RequestLoggingPlugin = createApplicationPlugin("RequestLogging") {
325
onInstall { pipeline ->
326
pipeline.intercept(ApplicationCallPipeline.Setup) {
327
val start = System.currentTimeMillis()
328
329
proceed()
330
331
val duration = System.currentTimeMillis() - start
332
application.log.info("${call.request.httpMethod.value} ${call.request.uri} - ${duration}ms")
333
}
334
}
335
}
336
337
// Creating a configurable application plugin
338
class RateLimitConfig {
339
var requestsPerMinute: Int = 60
340
var keyProvider: (ApplicationCall) -> String = { it.request.local.remoteHost }
341
}
342
343
val RateLimitPlugin = createApplicationPlugin(
344
name = "RateLimit",
345
createConfiguration = ::RateLimitConfig
346
) {
347
val config = pluginConfig as RateLimitConfig
348
val requestCounts = mutableMapOf<String, MutableList<Long>>()
349
350
onInstall { pipeline ->
351
pipeline.intercept(ApplicationCallPipeline.Plugins) {
352
val key = config.keyProvider(call)
353
val now = System.currentTimeMillis()
354
val windowStart = now - 60_000 // 1 minute window
355
356
// Clean old requests
357
requestCounts[key]?.removeAll { it < windowStart }
358
359
// Check rate limit
360
val requests = requestCounts.getOrPut(key) { mutableListOf() }
361
if (requests.size >= config.requestsPerMinute) {
362
call.respond(HttpStatusCode.TooManyRequests, "Rate limit exceeded")
363
return@intercept finish()
364
}
365
366
// Add current request
367
requests.add(now)
368
proceed()
369
}
370
}
371
}
372
373
// Creating a route-scoped plugin
374
class AuthConfig {
375
var validate: suspend (String) -> Boolean = { false }
376
var challenge: suspend ApplicationCall.() -> Unit = {
377
respond(HttpStatusCode.Unauthorized, "Authentication required")
378
}
379
}
380
381
val BasicAuthPlugin = createRouteScopedPlugin(
382
name = "BasicAuth",
383
createConfiguration = ::AuthConfig
384
) {
385
val config = pluginConfig as AuthConfig
386
387
onInstall { pipeline ->
388
pipeline.intercept(ApplicationCallPipeline.Plugins) {
389
val authHeader = call.request.headers["Authorization"]
390
391
if (authHeader?.startsWith("Basic ") != true) {
392
config.challenge(call)
393
return@intercept finish()
394
}
395
396
val token = authHeader.removePrefix("Basic ")
397
if (!config.validate(token)) {
398
config.challenge(call)
399
return@intercept finish()
400
}
401
402
proceed()
403
}
404
}
405
}
406
407
// Installing and using plugins
408
fun Application.configurePlugins() {
409
// Install simple plugin
410
install(RequestLoggingPlugin)
411
412
// Install configurable plugin
413
install(RateLimitPlugin) {
414
requestsPerMinute = 100
415
keyProvider = { call ->
416
call.request.headers["X-API-Key"] ?: call.request.local.remoteHost
417
}
418
}
419
}
420
421
fun Application.configureRouting() {
422
routing {
423
// Public routes
424
get("/") {
425
call.respondText("Welcome!")
426
}
427
428
get("/health") {
429
call.respondText("OK")
430
}
431
432
// Protected admin routes
433
route("/admin") {
434
install(BasicAuthPlugin) {
435
validate = { token ->
436
// Decode and validate Basic auth token
437
val decoded = Base64.getDecoder().decode(token).toString(Charsets.UTF_8)
438
val (username, password) = decoded.split(":", limit = 2)
439
username == "admin" && password == "secret"
440
}
441
challenge = {
442
response.headers.append("WWW-Authenticate", "Basic realm=\"Admin Area\"")
443
respond(HttpStatusCode.Unauthorized, "Admin access required")
444
}
445
}
446
447
get("/dashboard") {
448
call.respondText("Admin Dashboard")
449
}
450
451
get("/users") {
452
call.respondText("User Management")
453
}
454
}
455
456
// API routes with specific rate limiting
457
route("/api") {
458
install(RateLimitPlugin) {
459
requestsPerMinute = 30 // Lower limit for API
460
}
461
462
get("/data") {
463
call.respond(mapOf("data" to "API response"))
464
}
465
}
466
}
467
}
468
469
// Advanced plugin with lifecycle management
470
class DatabaseConnectionPlugin {
471
lateinit var connection: DatabaseConnection
472
473
companion object : ApplicationPlugin<DatabaseConfig, DatabaseConnectionPlugin, DatabaseConnectionPlugin> {
474
override val key = AttributeKey<DatabaseConnectionPlugin>("DatabaseConnection")
475
476
override fun install(
477
pipeline: ApplicationCallPipeline,
478
configure: DatabaseConfig.() -> Unit
479
): DatabaseConnectionPlugin {
480
val config = DatabaseConfig().apply(configure)
481
val plugin = DatabaseConnectionPlugin()
482
483
plugin.connection = createConnection(config)
484
485
// Add connection to call attributes
486
pipeline.intercept(ApplicationCallPipeline.Setup) {
487
call.attributes.put(DatabaseConnectionKey, plugin.connection)
488
}
489
490
return plugin
491
}
492
}
493
}
494
495
data class DatabaseConfig(
496
var url: String = "jdbc:h2:mem:test",
497
var driver: String = "org.h2.Driver",
498
var user: String = "sa",
499
var password: String = ""
500
)
501
502
val DatabaseConnectionKey = AttributeKey<DatabaseConnection>("DatabaseConnection")
503
504
// Usage with lifecycle
505
fun Application.configureDatabasePlugin() {
506
install(DatabaseConnectionPlugin) {
507
url = "jdbc:postgresql://localhost/mydb"
508
user = "dbuser"
509
password = "dbpass"
510
}
511
512
// Access database in routes
513
routing {
514
get("/users") {
515
val db = call.attributes[DatabaseConnectionKey]
516
val users = db.query("SELECT * FROM users")
517
call.respond(users)
518
}
519
}
520
}
521
```