CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-io-ktor--ktor-client-core-tvosarm64

Ktor HTTP Client Core for tvOS ARM64 - multiplatform asynchronous HTTP client library with coroutines support

Pending
Overview
Eval results
Files

plugin-system.mddocs/

Plugin System

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.

Plugin Interface

interface HttpClientPlugin<TConfig : Any, TPlugin : Any> {
    val key: AttributeKey<TPlugin>
    fun prepare(block: TConfig.() -> Unit = {}): TPlugin
    fun install(plugin: TPlugin, scope: HttpClient)
}

// Plugin installation
fun <T : HttpClientEngineConfig> HttpClientConfig<T>.install(
    plugin: HttpClientPlugin<*, *>,
    configure: Any.() -> Unit = {}
)

Plugin Architecture

Plugin Lifecycle

  1. Prepare: Configure plugin with user-provided settings
  2. Install: Install plugin instance into client scope
  3. Execution: Plugin intercepts requests/responses during HTTP calls

Plugin Components

  • Configuration: User-configurable options for plugin behavior
  • Plugin Instance: The actual plugin implementation
  • Key: Unique identifier for plugin instance storage
  • Hooks: Integration points with client request/response pipeline

Creating Custom Plugins

Simple Plugin Example

class LoggingPlugin(private val logger: (String) -> Unit) {
    class Config {
        var logger: (String) -> Unit = ::println
        var logHeaders: Boolean = true
        var logBody: Boolean = false
    }
    
    companion object : HttpClientPlugin<Config, LoggingPlugin> {
        override val key: AttributeKey<LoggingPlugin> = AttributeKey("LoggingPlugin")
        
        override fun prepare(block: Config.() -> Unit): LoggingPlugin {
            val config = Config().apply(block)
            return LoggingPlugin(config.logger)
        }
        
        override fun install(plugin: LoggingPlugin, scope: HttpClient) {
            scope.requestPipeline.intercept(HttpRequestPipeline.Before) {
                plugin.logger("Request: ${context.method.value} ${context.url}")
                proceed()
            }
            
            scope.responsePipeline.intercept(HttpResponsePipeline.After) {
                plugin.logger("Response: ${context.response.status}")
                proceed()
            }
        }
    }
}

Using Custom Plugin

val client = HttpClient {
    install(LoggingPlugin) {
        logger = { message -> 
            println("[HTTP] $message")
        }
        logHeaders = true
        logBody = false
    }
}

Plugin Configuration

Configuration Classes

class MyPlugin {
    class Config {
        var enabled: Boolean = true
        var retryCount: Int = 3
        var timeout: Long = 30000
        var customHeader: String? = null
        
        internal val interceptors = mutableListOf<suspend PipelineContext<*, *>.(Any) -> Unit>()
        
        fun addInterceptor(interceptor: suspend PipelineContext<*, *>.(Any) -> Unit) {
            interceptors.add(interceptor)
        }
    }
}

Plugin Installation with Configuration

val client = HttpClient {
    install(MyPlugin) {
        enabled = true
        retryCount = 5
        timeout = 60000
        customHeader = "MyApp/1.0"
        
        addInterceptor { data ->
            // Custom logic
            proceed()
        }
    }
}

Pipeline Integration

Request Pipeline Phases

class HttpRequestPipeline : Pipeline<Any, HttpRequestBuilder> {
    companion object {
        val Before = PipelinePhase("Before")
        val State = PipelinePhase("State")  
        val Transform = PipelinePhase("Transform")
        val Render = PipelinePhase("Render")
        val Send = PipelinePhase("Send")
    }
}

Response Pipeline Phases

class HttpResponsePipeline : Pipeline<HttpResponseContainer, HttpClientCall> {
    companion object {
        val Receive = PipelinePhase("Receive")
        val Parse = PipelinePhase("Parse")
        val Transform = PipelinePhase("Transform")
        val State = PipelinePhase("State")
        val After = PipelinePhase("After")
    }
}

Pipeline Interception

override fun install(plugin: MyPlugin, scope: HttpClient) {
    // Request pipeline interception
    scope.requestPipeline.intercept(HttpRequestPipeline.Before) {
        // Pre-request processing
        println("Before request: ${context.url}")
        proceed()
    }
    
    scope.requestPipeline.intercept(HttpRequestPipeline.State) {
        // Modify request state
        context.header("X-Custom", "value")
        proceed()
    }
    
    // Response pipeline interception
    scope.responsePipeline.intercept(HttpResponsePipeline.Receive) {
        // Post-response processing
        println("Response received: ${context.response.status}")
        proceed()
    }
    
    // Send pipeline interception (for request modification)
    scope.sendPipeline.intercept(HttpSendPipeline.Before) {
        // Final request modifications
        proceed()
    }
}

Plugin API Builder

Modern Plugin API

// New plugin API builder
fun <TConfig : Any> createClientPlugin(
    name: String,
    createConfiguration: () -> TConfig,
    body: ClientPluginBuilder<TConfig>.() -> Unit
): HttpClientPlugin<TConfig, *>

class ClientPluginBuilder<TConfig : Any> {
    fun onRequest(block: suspend OnRequestContext<TConfig>.(HttpRequestBuilder) -> Unit)
    fun onResponse(block: suspend OnResponseContext<TConfig>.(HttpResponse) -> Unit)  
    fun onClose(block: suspend TConfig.() -> Unit)
    
    fun transformRequestBody(block: suspend TransformRequestBodyContext<TConfig>.(Any) -> Any)
    fun transformResponseBody(block: suspend TransformResponseBodyContext<TConfig>.(HttpResponse, Any) -> Any)
}

Creating Plugin with Builder API

val CustomPlugin = createClientPlugin("CustomPlugin", ::CustomConfig) {
    onRequest { request ->
        if (pluginConfig.addTimestamp) {
            request.header("X-Timestamp", Clock.System.now().toString())
        }
    }
    
    onResponse { response ->
        if (pluginConfig.logResponses) {
            println("Response: ${response.status} for ${response.call.request.url}")
        }
    }
    
    transformRequestBody { body ->
        if (pluginConfig.wrapRequests && body is String) {
            """{"data": $body}"""
        } else {
            body
        }
    }
    
    onClose {
        // Cleanup resources
    }
}

data class CustomConfig(
    var addTimestamp: Boolean = true,
    var logResponses: Boolean = false,
    var wrapRequests: Boolean = false
)

Plugin Hooks

Common Hook Types

// Request hooks
interface OnRequestHook {
    suspend fun processRequest(context: OnRequestContext, request: HttpRequestBuilder)
}

// Response hooks  
interface OnResponseHook {
    suspend fun processResponse(context: OnResponseContext, response: HttpResponse)
}

// Call hooks
interface CallHook {
    suspend fun processCall(context: CallContext, call: HttpClientCall)
}

// Body transformation hooks
interface TransformRequestBodyHook {
    suspend fun transformRequestBody(context: TransformContext, body: Any): Any
}

interface TransformResponseBodyHook {
    suspend fun transformResponseBody(context: TransformContext, body: Any): Any
}

Hook Implementation

class MetricsPlugin : OnRequestHook, OnResponseHook {
    override suspend fun processRequest(context: OnRequestContext, request: HttpRequestBuilder) {
        val startTime = Clock.System.now()
        request.attributes.put(StartTimeKey, startTime)
    }
    
    override suspend fun processResponse(context: OnResponseContext, response: HttpResponse) {
        val startTime = response.call.request.attributes[StartTimeKey]
        val duration = Clock.System.now() - startTime
        recordMetric("http.request.duration", duration.inWholeMilliseconds)
    }
    
    companion object {
        private val StartTimeKey = AttributeKey<Instant>("StartTime")
    }
}

Plugin Dependencies

Plugin Dependencies and Ordering

class DependentPlugin {
    companion object : HttpClientPlugin<DependentPlugin.Config, DependentPlugin> {
        override fun install(plugin: DependentPlugin, scope: HttpClient) {
            // Ensure required plugins are installed
            val requiredPlugin = scope.pluginOrNull(RequiredPlugin)
                ?: error("DependentPlugin requires RequiredPlugin to be installed")
            
            // Plugin implementation
        }
    }
}

Accessing Other Plugins

override fun install(plugin: MyPlugin, scope: HttpClient) {
    // Access other installed plugins
    val cookies = scope.pluginOrNull(HttpCookies)
    val cache = scope.pluginOrNull(HttpCache)
    
    if (cookies != null) {
        // Integrate with cookies plugin
    }
}

Plugin State Management

Plugin Attributes

class StatefulPlugin {
    companion object {
        private val StateKey = AttributeKey<PluginState>("StatefulPluginState")
    }
    
    override fun install(plugin: StatefulPlugin, scope: HttpClient) {
        val state = PluginState()
        scope.attributes.put(StateKey, state)
        
        scope.requestPipeline.intercept(HttpRequestPipeline.Before) {
            val pluginState = scope.attributes[StateKey]
            pluginState.requestCount++
            proceed()
        }
    }
    
    private class PluginState {
        var requestCount: Int = 0
        val cache: MutableMap<String, Any> = mutableMapOf()
    }
}

Thread-Safe State

class ThreadSafePlugin {
    private class PluginState {
        private val _requestCount = AtomicInteger(0)
        val requestCount: Int get() = _requestCount.get()
        
        fun incrementRequests() = _requestCount.incrementAndGet()
        
        private val cache = ConcurrentHashMap<String, Any>()
        
        fun getFromCache(key: String): Any? = cache[key]
        fun putInCache(key: String, value: Any) = cache.put(key, value)
    }
}

Error Handling in Plugins

Exception Handling

override fun install(plugin: ResilientPlugin, scope: HttpClient) {
    scope.requestPipeline.intercept(HttpRequestPipeline.Before) {
        try {
            // Plugin logic that might fail
            doSomethingRisky()
            proceed()
        } catch (e: Exception) {
            // Handle or rethrow
            logger.error("Plugin error", e)
            
            if (config.failSilently) {
                proceed() // Continue with original request
            } else {
                throw e // Propagate error
            }
        }
    }
}

Response Error Handling

scope.responsePipeline.intercept(HttpResponsePipeline.After) {
    val response = context.response
    
    if (!response.status.isSuccess() && config.handleErrors) {
        val errorBody = response.bodyAsText()
        
        when (response.status.value) {
            401 -> throw AuthenticationException(errorBody)
            429 -> throw RateLimitException(errorBody)
            else -> throw HttpException(response.status, errorBody)
        }
    }
    
    proceed()
}

Plugin Testing

Testing Custom Plugins

@Test
fun testCustomPlugin() = runTest {
    val mockEngine = MockEngine { request ->
        respond(
            content = """{"result": "success"}""",
            status = HttpStatusCode.OK,
            headers = headersOf(HttpHeaders.ContentType, "application/json")
        )
    }
    
    val client = HttpClient(mockEngine) {
        install(CustomPlugin) {
            enabled = true
            customValue = "test"
        }
    }
    
    val response = client.get("https://test.com/api")
    
    // Assert plugin behavior
    assertEquals(HttpStatusCode.OK, response.status)
    
    // Verify plugin modifications
    val requestHistory = mockEngine.requestHistory
    assertTrue(requestHistory.first().headers.contains("X-Custom-Header"))
}

Install with Tessl CLI

npx tessl i tessl/maven-io-ktor--ktor-client-core-tvosarm64

docs

builtin-plugins.md

caching.md

cookies.md

engine-configuration.md

forms.md

http-client.md

index.md

plugin-system.md

request-building.md

response-handling.md

response-observation.md

utilities.md

websockets.md

tile.json