0
# Plugin System
1
2
The Ktor HTTP Client features an extensible plugin architecture that allows adding cross-cutting functionality like authentication, caching, logging, retries, and custom request/response processing through a standardized plugin interface.
3
4
## Plugin Interface
5
6
```kotlin { .api }
7
interface HttpClientPlugin<TConfig : Any, TPlugin : Any> {
8
val key: AttributeKey<TPlugin>
9
fun prepare(block: TConfig.() -> Unit = {}): TPlugin
10
fun install(plugin: TPlugin, scope: HttpClient)
11
}
12
13
// Plugin installation
14
fun <T : HttpClientEngineConfig> HttpClientConfig<T>.install(
15
plugin: HttpClientPlugin<*, *>,
16
configure: Any.() -> Unit = {}
17
)
18
```
19
20
## Plugin Architecture
21
22
### Plugin Lifecycle
23
1. **Prepare**: Configure plugin with user-provided settings
24
2. **Install**: Install plugin instance into client scope
25
3. **Execution**: Plugin intercepts requests/responses during HTTP calls
26
27
### Plugin Components
28
- **Configuration**: User-configurable options for plugin behavior
29
- **Plugin Instance**: The actual plugin implementation
30
- **Key**: Unique identifier for plugin instance storage
31
- **Hooks**: Integration points with client request/response pipeline
32
33
## Creating Custom Plugins
34
35
### Simple Plugin Example
36
```kotlin
37
class LoggingPlugin(private val logger: (String) -> Unit) {
38
class Config {
39
var logger: (String) -> Unit = ::println
40
var logHeaders: Boolean = true
41
var logBody: Boolean = false
42
}
43
44
companion object : HttpClientPlugin<Config, LoggingPlugin> {
45
override val key: AttributeKey<LoggingPlugin> = AttributeKey("LoggingPlugin")
46
47
override fun prepare(block: Config.() -> Unit): LoggingPlugin {
48
val config = Config().apply(block)
49
return LoggingPlugin(config.logger)
50
}
51
52
override fun install(plugin: LoggingPlugin, scope: HttpClient) {
53
scope.requestPipeline.intercept(HttpRequestPipeline.Before) {
54
plugin.logger("Request: ${context.method.value} ${context.url}")
55
proceed()
56
}
57
58
scope.responsePipeline.intercept(HttpResponsePipeline.After) {
59
plugin.logger("Response: ${context.response.status}")
60
proceed()
61
}
62
}
63
}
64
}
65
```
66
67
### Using Custom Plugin
68
```kotlin
69
val client = HttpClient {
70
install(LoggingPlugin) {
71
logger = { message ->
72
println("[HTTP] $message")
73
}
74
logHeaders = true
75
logBody = false
76
}
77
}
78
```
79
80
## Plugin Configuration
81
82
### Configuration Classes
83
```kotlin
84
class MyPlugin {
85
class Config {
86
var enabled: Boolean = true
87
var retryCount: Int = 3
88
var timeout: Long = 30000
89
var customHeader: String? = null
90
91
internal val interceptors = mutableListOf<suspend PipelineContext<*, *>.(Any) -> Unit>()
92
93
fun addInterceptor(interceptor: suspend PipelineContext<*, *>.(Any) -> Unit) {
94
interceptors.add(interceptor)
95
}
96
}
97
}
98
```
99
100
### Plugin Installation with Configuration
101
```kotlin
102
val client = HttpClient {
103
install(MyPlugin) {
104
enabled = true
105
retryCount = 5
106
timeout = 60000
107
customHeader = "MyApp/1.0"
108
109
addInterceptor { data ->
110
// Custom logic
111
proceed()
112
}
113
}
114
}
115
```
116
117
## Pipeline Integration
118
119
### Request Pipeline Phases
120
```kotlin { .api }
121
class HttpRequestPipeline : Pipeline<Any, HttpRequestBuilder> {
122
companion object {
123
val Before = PipelinePhase("Before")
124
val State = PipelinePhase("State")
125
val Transform = PipelinePhase("Transform")
126
val Render = PipelinePhase("Render")
127
val Send = PipelinePhase("Send")
128
}
129
}
130
```
131
132
### Response Pipeline Phases
133
```kotlin { .api }
134
class HttpResponsePipeline : Pipeline<HttpResponseContainer, HttpClientCall> {
135
companion object {
136
val Receive = PipelinePhase("Receive")
137
val Parse = PipelinePhase("Parse")
138
val Transform = PipelinePhase("Transform")
139
val State = PipelinePhase("State")
140
val After = PipelinePhase("After")
141
}
142
}
143
```
144
145
### Pipeline Interception
146
```kotlin
147
override fun install(plugin: MyPlugin, scope: HttpClient) {
148
// Request pipeline interception
149
scope.requestPipeline.intercept(HttpRequestPipeline.Before) {
150
// Pre-request processing
151
println("Before request: ${context.url}")
152
proceed()
153
}
154
155
scope.requestPipeline.intercept(HttpRequestPipeline.State) {
156
// Modify request state
157
context.header("X-Custom", "value")
158
proceed()
159
}
160
161
// Response pipeline interception
162
scope.responsePipeline.intercept(HttpResponsePipeline.Receive) {
163
// Post-response processing
164
println("Response received: ${context.response.status}")
165
proceed()
166
}
167
168
// Send pipeline interception (for request modification)
169
scope.sendPipeline.intercept(HttpSendPipeline.Before) {
170
// Final request modifications
171
proceed()
172
}
173
}
174
```
175
176
## Plugin API Builder
177
178
### Modern Plugin API
179
```kotlin { .api }
180
// New plugin API builder
181
fun <TConfig : Any> createClientPlugin(
182
name: String,
183
createConfiguration: () -> TConfig,
184
body: ClientPluginBuilder<TConfig>.() -> Unit
185
): HttpClientPlugin<TConfig, *>
186
187
class ClientPluginBuilder<TConfig : Any> {
188
fun onRequest(block: suspend OnRequestContext<TConfig>.(HttpRequestBuilder) -> Unit)
189
fun onResponse(block: suspend OnResponseContext<TConfig>.(HttpResponse) -> Unit)
190
fun onClose(block: suspend TConfig.() -> Unit)
191
192
fun transformRequestBody(block: suspend TransformRequestBodyContext<TConfig>.(Any) -> Any)
193
fun transformResponseBody(block: suspend TransformResponseBodyContext<TConfig>.(HttpResponse, Any) -> Any)
194
}
195
```
196
197
### Creating Plugin with Builder API
198
```kotlin
199
val CustomPlugin = createClientPlugin("CustomPlugin", ::CustomConfig) {
200
onRequest { request ->
201
if (pluginConfig.addTimestamp) {
202
request.header("X-Timestamp", Clock.System.now().toString())
203
}
204
}
205
206
onResponse { response ->
207
if (pluginConfig.logResponses) {
208
println("Response: ${response.status} for ${response.call.request.url}")
209
}
210
}
211
212
transformRequestBody { body ->
213
if (pluginConfig.wrapRequests && body is String) {
214
"""{"data": $body}"""
215
} else {
216
body
217
}
218
}
219
220
onClose {
221
// Cleanup resources
222
}
223
}
224
225
data class CustomConfig(
226
var addTimestamp: Boolean = true,
227
var logResponses: Boolean = false,
228
var wrapRequests: Boolean = false
229
)
230
```
231
232
## Plugin Hooks
233
234
### Common Hook Types
235
```kotlin { .api }
236
// Request hooks
237
interface OnRequestHook {
238
suspend fun processRequest(context: OnRequestContext, request: HttpRequestBuilder)
239
}
240
241
// Response hooks
242
interface OnResponseHook {
243
suspend fun processResponse(context: OnResponseContext, response: HttpResponse)
244
}
245
246
// Call hooks
247
interface CallHook {
248
suspend fun processCall(context: CallContext, call: HttpClientCall)
249
}
250
251
// Body transformation hooks
252
interface TransformRequestBodyHook {
253
suspend fun transformRequestBody(context: TransformContext, body: Any): Any
254
}
255
256
interface TransformResponseBodyHook {
257
suspend fun transformResponseBody(context: TransformContext, body: Any): Any
258
}
259
```
260
261
### Hook Implementation
262
```kotlin
263
class MetricsPlugin : OnRequestHook, OnResponseHook {
264
override suspend fun processRequest(context: OnRequestContext, request: HttpRequestBuilder) {
265
val startTime = Clock.System.now()
266
request.attributes.put(StartTimeKey, startTime)
267
}
268
269
override suspend fun processResponse(context: OnResponseContext, response: HttpResponse) {
270
val startTime = response.call.request.attributes[StartTimeKey]
271
val duration = Clock.System.now() - startTime
272
recordMetric("http.request.duration", duration.inWholeMilliseconds)
273
}
274
275
companion object {
276
private val StartTimeKey = AttributeKey<Instant>("StartTime")
277
}
278
}
279
```
280
281
## Plugin Dependencies
282
283
### Plugin Dependencies and Ordering
284
```kotlin
285
class DependentPlugin {
286
companion object : HttpClientPlugin<DependentPlugin.Config, DependentPlugin> {
287
override fun install(plugin: DependentPlugin, scope: HttpClient) {
288
// Ensure required plugins are installed
289
val requiredPlugin = scope.pluginOrNull(RequiredPlugin)
290
?: error("DependentPlugin requires RequiredPlugin to be installed")
291
292
// Plugin implementation
293
}
294
}
295
}
296
```
297
298
### Accessing Other Plugins
299
```kotlin
300
override fun install(plugin: MyPlugin, scope: HttpClient) {
301
// Access other installed plugins
302
val cookies = scope.pluginOrNull(HttpCookies)
303
val cache = scope.pluginOrNull(HttpCache)
304
305
if (cookies != null) {
306
// Integrate with cookies plugin
307
}
308
}
309
```
310
311
## Plugin State Management
312
313
### Plugin Attributes
314
```kotlin
315
class StatefulPlugin {
316
companion object {
317
private val StateKey = AttributeKey<PluginState>("StatefulPluginState")
318
}
319
320
override fun install(plugin: StatefulPlugin, scope: HttpClient) {
321
val state = PluginState()
322
scope.attributes.put(StateKey, state)
323
324
scope.requestPipeline.intercept(HttpRequestPipeline.Before) {
325
val pluginState = scope.attributes[StateKey]
326
pluginState.requestCount++
327
proceed()
328
}
329
}
330
331
private class PluginState {
332
var requestCount: Int = 0
333
val cache: MutableMap<String, Any> = mutableMapOf()
334
}
335
}
336
```
337
338
### Thread-Safe State
339
```kotlin
340
class ThreadSafePlugin {
341
private class PluginState {
342
private val _requestCount = AtomicInteger(0)
343
val requestCount: Int get() = _requestCount.get()
344
345
fun incrementRequests() = _requestCount.incrementAndGet()
346
347
private val cache = ConcurrentHashMap<String, Any>()
348
349
fun getFromCache(key: String): Any? = cache[key]
350
fun putInCache(key: String, value: Any) = cache.put(key, value)
351
}
352
}
353
```
354
355
## Error Handling in Plugins
356
357
### Exception Handling
358
```kotlin
359
override fun install(plugin: ResilientPlugin, scope: HttpClient) {
360
scope.requestPipeline.intercept(HttpRequestPipeline.Before) {
361
try {
362
// Plugin logic that might fail
363
doSomethingRisky()
364
proceed()
365
} catch (e: Exception) {
366
// Handle or rethrow
367
logger.error("Plugin error", e)
368
369
if (config.failSilently) {
370
proceed() // Continue with original request
371
} else {
372
throw e // Propagate error
373
}
374
}
375
}
376
}
377
```
378
379
### Response Error Handling
380
```kotlin
381
scope.responsePipeline.intercept(HttpResponsePipeline.After) {
382
val response = context.response
383
384
if (!response.status.isSuccess() && config.handleErrors) {
385
val errorBody = response.bodyAsText()
386
387
when (response.status.value) {
388
401 -> throw AuthenticationException(errorBody)
389
429 -> throw RateLimitException(errorBody)
390
else -> throw HttpException(response.status, errorBody)
391
}
392
}
393
394
proceed()
395
}
396
```
397
398
## Plugin Testing
399
400
### Testing Custom Plugins
401
```kotlin
402
@Test
403
fun testCustomPlugin() = runTest {
404
val mockEngine = MockEngine { request ->
405
respond(
406
content = """{"result": "success"}""",
407
status = HttpStatusCode.OK,
408
headers = headersOf(HttpHeaders.ContentType, "application/json")
409
)
410
}
411
412
val client = HttpClient(mockEngine) {
413
install(CustomPlugin) {
414
enabled = true
415
customValue = "test"
416
}
417
}
418
419
val response = client.get("https://test.com/api")
420
421
// Assert plugin behavior
422
assertEquals(HttpStatusCode.OK, response.status)
423
424
// Verify plugin modifications
425
val requestHistory = mockEngine.requestHistory
426
assertTrue(requestHistory.first().headers.contains("X-Custom-Header"))
427
}
428
```