or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

builtin-plugins.mdcaching.mdcookies.mdengine-configuration.mdforms.mdhttp-client.mdindex.mdplugin-system.mdrequest-building.mdresponse-handling.mdresponse-observation.mdutilities.mdwebsockets.md

plugin-system.mddocs/

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

```