CtrlK
BlogDocsLog inGet started
Tessl Logo

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

Ktor HTTP client core library - asynchronous framework for creating HTTP clients in Kotlin multiplatform

Pending
Overview
Eval results
Files

plugin-system.mddocs/

Plugin System

Extensible plugin architecture for adding cross-cutting concerns like authentication, caching, content negotiation, and logging to HTTP clients.

Capabilities

HttpClientPlugin Interface

Base interface for all HTTP client plugins defining lifecycle and configuration.

/**
 * Base plugin interface for HTTP client functionality
 */
interface HttpClientPlugin<out TConfig : Any, TPlugin : Any> {
    /** Unique plugin identifier */
    val key: AttributeKey<TPlugin>
    
    /**
     * Prepare plugin instance with configuration
     */
    fun prepare(block: TConfig.() -> Unit = {}): TPlugin
    
    /**
     * Install plugin into HTTP client scope
     */
    fun install(plugin: TPlugin, scope: HttpClient)
}

Usage Examples:

// Example plugin implementation
object CustomLogging : HttpClientPlugin<CustomLogging.Config, CustomLogging> {
    override val key: AttributeKey<CustomLogging> = AttributeKey("CustomLogging")
    
    class Config {
        var logLevel: LogLevel = LogLevel.INFO
        var includeHeaders: Boolean = false
    }
    
    override fun prepare(block: Config.() -> Unit): CustomLogging {
        val config = Config().apply(block)
        return CustomLogging()
    }
    
    override fun install(plugin: CustomLogging, scope: HttpClient) {
        // Install interceptors and setup plugin logic
    }
}

Plugin Installation

Methods for installing and configuring plugins in HTTP client configuration.

/**
 * Install plugin with configuration in HttpClientConfig
 */
fun <TBuilder : Any, TPlugin : Any> HttpClientConfig<*>.install(
    plugin: HttpClientPlugin<TBuilder, TPlugin>,
    configure: TBuilder.() -> Unit = {}
)

/**
 * Install custom interceptor with string key
 */
fun HttpClientConfig<*>.install(key: String, block: HttpClient.() -> Unit)

Usage Examples:

import io.ktor.client.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.serialization.kotlinx.json.*

val client = HttpClient {
    // Install plugin with configuration
    install(Logging) {
        logger = Logger.DEFAULT
        level = LogLevel.INFO
        filter { request ->
            request.url.host.contains("api.example.com")
        }
    }
    
    // Install content negotiation
    install(ContentNegotiation) {
        json(Json {
            prettyPrint = true
            isLenient = true
            ignoreUnknownKeys = true
        })
    }
    
    // Install custom plugin
    install(CustomLogging) {
        logLevel = LogLevel.DEBUG
        includeHeaders = true
    }
    
    // Install custom interceptor
    install("RequestIdGenerator") {
        requestPipeline.intercept(HttpRequestPipeline.Before) {
            context.headers.append("X-Request-ID", generateRequestId())
        }
    }
}

Plugin Access

Functions for accessing installed plugins from client instances.

/**
 * Get installed plugin instance (nullable)
 */
fun <B : Any, F : Any> HttpClient.pluginOrNull(plugin: HttpClientPlugin<B, F>): F?

/**
 * Get installed plugin instance (throws if not found)
 */
fun <B : Any, F : Any> HttpClient.plugin(plugin: HttpClientPlugin<B, F>): F

Usage Examples:

val client = HttpClient {
    install(Logging) {
        level = LogLevel.INFO
    }
}

// Access plugin safely
val loggingPlugin = client.pluginOrNull(Logging)
if (loggingPlugin != null) {
    // Use plugin instance
    println("Logging plugin is installed")
}

// Access plugin (throws if not installed)
try {
    val logging = client.plugin(Logging)
    // Use plugin instance
} catch (e: IllegalStateException) {
    println("Logging plugin not installed")
}

Built-in Core Plugins

Essential plugins that are automatically installed or commonly used.

// Always installed plugins
object HttpSend : HttpClientPlugin<Unit, HttpSend>
object HttpCallValidator : HttpClientPlugin<HttpCallValidator.Config, HttpCallValidator>
object HttpRequestLifecycle : HttpClientPlugin<Unit, HttpRequestLifecycle>
object BodyProgress : HttpClientPlugin<Unit, BodyProgress>

// Optional core plugins
object HttpPlainText : HttpClientPlugin<HttpPlainText.Config, HttpPlainText>
object DefaultRequest : HttpClientPlugin<DefaultRequest.DefaultRequestBuilder, DefaultRequest>
object UserAgent : HttpClientPlugin<UserAgent.Config, UserAgent>

Usage Examples:

// These plugins are automatically installed:
// - HttpSend: Request sending pipeline
// - HttpCallValidator: Response validation
// - HttpRequestLifecycle: Request lifecycle management
// - BodyProgress: Progress tracking

// Optional plugins you can install:
val client = HttpClient {
    install(HttpPlainText) {
        // Plain text content handling configuration
        charset = Charsets.UTF_8
        sendCharset = Charsets.UTF_8
    }
    
    install(DefaultRequest) {
        // Default request configuration
        host = "api.example.com"
        port = 443
        url {
            protocol = URLProtocol.HTTPS
        }
        headers {
            append(HttpHeaders.UserAgent, "MyApp/1.0")
        }
    }
    
    install(UserAgent) {
        agent = "MyApp/1.0 (Ktor Client)"
    }
}

Timeout Plugin

Comprehensive timeout management for requests, connections, and sockets.

/**
 * HTTP timeout management plugin
 */
object HttpTimeout : HttpClientPlugin<HttpTimeoutCapabilityConfiguration, HttpTimeout> {
    override val key: AttributeKey<HttpTimeout> = AttributeKey("HttpTimeout")
    
    /** Infinite timeout constant */
    const val INFINITE_TIMEOUT_MS: Long = Long.MAX_VALUE
}

/**
 * Timeout configuration class
 */
class HttpTimeoutCapabilityConfiguration {
    /** Request timeout in milliseconds */
    var requestTimeoutMillis: Long? = null
    
    /** Connection timeout in milliseconds */
    var connectTimeoutMillis: Long? = null
    
    /** Socket timeout in milliseconds */
    var socketTimeoutMillis: Long? = null
}

/**
 * Configure timeout for specific request
 */
fun HttpRequestBuilder.timeout(block: HttpTimeoutCapabilityConfiguration.() -> Unit)

Usage Examples:

import io.ktor.client.plugins.*

// Install timeout plugin globally
val client = HttpClient {
    install(HttpTimeout) {
        requestTimeoutMillis = 30_000
        connectTimeoutMillis = 10_000
        socketTimeoutMillis = 15_000
    }
}

// Configure timeout per request
val response = client.get("https://api.example.com/slow-endpoint") {
    timeout {
        requestTimeoutMillis = 60_000 // 1 minute for slow endpoint
        socketTimeoutMillis = HttpTimeout.INFINITE_TIMEOUT_MS
    }
}

// Handle timeout exceptions
try {
    val response = client.get("https://api.example.com/data")
} catch (e: HttpRequestTimeoutException) {
    println("Request timed out: ${e.message}")
} catch (e: ConnectTimeoutException) {
    println("Connection timed out: ${e.message}")
} catch (e: SocketTimeoutException) {
    println("Socket timed out: ${e.message}")
}

Request Retry Plugin

Automatic retry mechanism for failed HTTP requests with configurable retry policies, delays, and request modification.

/**
 * HTTP request retry plugin
 */
object HttpRequestRetry : HttpClientPlugin<HttpRequestRetry.Configuration, HttpRequestRetry> {
    override val key: AttributeKey<HttpRequestRetry> = AttributeKey("HttpRequestRetry")

    /**
     * Retry configuration class
     */
    class Configuration {
        /** Maximum number of retries (default: 3) */
        var maxRetries: Int = 3
        
        /** Delay function for calculating retry delay */
        var delayMillis: DelayContext.(Int) -> Long = { retry -> 1000L * (retry - 1) }
        
        /** Custom delay function (default: kotlinx.coroutines.delay) */
        var delay: suspend (Long) -> Unit = { kotlinx.coroutines.delay(it) }
        
        /** Predicate to determine if response should trigger retry */
        var shouldRetry: ShouldRetryContext.(HttpRequest, HttpResponse) -> Boolean = { _, _ -> false }
        
        /** Predicate to determine if exception should trigger retry */
        var shouldRetryOnException: ShouldRetryContext.(HttpRequestBuilder, Throwable) -> Boolean = { _, _ -> false }
        
        /** Request modification before retry */
        var modifyRequest: ModifyRequestContext.(HttpRequestBuilder) -> Unit = {}
        
        /** Predefined policy: retry on server errors (5xx) */
        fun retryOnServerErrors(maxRetries: Int = 3)
        
        /** Predefined policy: retry on connection failures */
        fun retryOnConnectionFailure(maxRetries: Int = 3)
        
        /** Predefined delay policy: exponential backoff */
        fun exponentialDelay(
            base: Double = 2.0,
            maxDelayMs: Long = 60000,
            randomizationMs: Long = 1000
        )
        
        /** Predefined delay policy: constant delay */
        fun constantDelay(delayMs: Long = 1000)
        
        /** Custom retry condition based on response */
        fun retryIf(block: ShouldRetryContext.(HttpRequest, HttpResponse) -> Boolean)
        
        /** Custom retry condition based on exception */
        fun retryOnExceptionIf(block: ShouldRetryContext.(HttpRequestBuilder, Throwable) -> Boolean)
        
        /** Custom request modification */
        fun modifyRequest(block: ModifyRequestContext.(HttpRequestBuilder) -> Unit)
    }
    
    /** Context classes */
    class ShouldRetryContext(val retryCount: Int)
    class DelayContext(val request: HttpRequestBuilder, val response: HttpResponse?, val cause: Throwable?)
    class ModifyRequestContext(
        val request: HttpRequestBuilder, 
        val response: HttpResponse?, 
        val cause: Throwable?, 
        val retryCount: Int
    )
    class RetryEventData(
        val request: HttpRequestBuilder, 
        val retryCount: Int, 
        val response: HttpResponse?, 
        val cause: Throwable?
    )
}

/** Event fired when request is being retried */
val HttpRequestRetryEvent: EventDefinition<HttpRequestRetry.RetryEventData>

/** Configure retry for specific request */
fun HttpRequestBuilder.retry(block: HttpRequestRetry.Configuration.() -> Unit)

Usage Examples:

import io.ktor.client.plugins.*

// Basic retry with server errors and exponential backoff
val client = HttpClient {
    install(HttpRequestRetry) {
        retryOnServerErrors(maxRetries = 3)
        exponentialDelay()
    }
}

// Advanced custom retry configuration
val client = HttpClient {
    install(HttpRequestRetry) {
        maxRetries = 5
        
        // Retry on specific status codes
        retryIf { request, response -> 
            response.status.value in 500..599 || response.status.value == 429
        }
        
        // Retry on network errors
        retryOnExceptionIf { request, cause -> 
            cause is ConnectTimeoutException || cause is SocketTimeoutException
        }
        
        // Custom delay with jitter
        delayMillis { retry -> 
            (retry * 1000L) + Random.nextLong(0, 500)
        }
        
        // Modify request before retry (e.g., add retry headers)
        modifyRequest { request ->
            request.headers.append("X-Retry-Count", retryCount.toString())
        }
    }
}

// Per-request retry configuration
val response = client.get("https://api.example.com/data") {
    retry {
        maxRetries = 2
        constantDelay(2000) // 2 second delay between retries
    }
}

// Listen to retry events
client.monitor.subscribe(HttpRequestRetryEvent) { retryData ->
    println("Retrying request ${retryData.retryCount} time(s)")
}

// Handle retry exhaustion
try {
    val response = client.get("https://unreliable-api.example.com/data")
} catch (e: SendCountExceedException) {
    println("Max retries exceeded: ${e.message}")
}

Redirect Plugin

HTTP redirect handling with configurable redirect policies.

/**
 * HTTP redirect handling plugin
 */
object HttpRedirect : HttpClientPlugin<HttpRedirect.Config, HttpRedirect> {
    override val key: AttributeKey<HttpRedirect> = AttributeKey("HttpRedirect")
    
    /** Event fired when redirect occurs */
    val HttpResponseRedirect: EventDefinition<HttpResponse>
    
    /**
     * Redirect configuration
     */
    class Config {
        /** Check HTTP method for redirects (default: true) */
        var checkHttpMethod: Boolean = true
        
        /** Allow HTTPS to HTTP downgrade (default: false) */
        var allowHttpsDowngrade: Boolean = false
    }
}

Usage Examples:

val client = HttpClient {
    install(HttpRedirect) {
        checkHttpMethod = true // Only redirect GET/HEAD by default
        allowHttpsDowngrade = false // Prevent HTTPS -> HTTP redirects
    }
    
    // Monitor redirect events
    monitor.subscribe(HttpRedirect.HttpResponseRedirect) { response ->
        println("Redirected to: ${response.request.url}")
    }
}

// Client automatically follows redirects
val response = client.get("https://example.com/redirect-me")
println("Final URL: ${response.request.url}")

Content Negotiation Plugin

Automatic serialization and deserialization of request/response bodies.

/**
 * Content negotiation plugin for automatic serialization
 * Note: This is typically provided by ktor-client-content-negotiation artifact
 */
object ContentNegotiation : HttpClientPlugin<ContentNegotiation.Config, ContentNegotiation> {
    class Config {
        fun json(json: Json = Json)
        fun xml()
        fun cbor()
        // Additional serialization formats
    }
}

Usage Examples:

import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.Serializable

@Serializable
data class User(val id: Int, val name: String, val email: String)

val client = HttpClient {
    install(ContentNegotiation) {
        json(Json {
            prettyPrint = true
            ignoreUnknownKeys = true
        })
    }
}

// Automatic serialization
val newUser = User(0, "John Doe", "john@example.com")
val response = client.post("https://api.example.com/users") {
    contentType(ContentType.Application.Json)
    setBody(newUser) // Automatically serialized to JSON
}

// Automatic deserialization
val users: List<User> = client.get("https://api.example.com/users").body()

Custom Plugin Development

Guidelines and patterns for developing custom HTTP client plugins.

/**
 * Example custom plugin structure
 */
object CustomPlugin : HttpClientPlugin<CustomPlugin.Config, CustomPlugin> {
    override val key: AttributeKey<CustomPlugin> = AttributeKey("CustomPlugin")
    
    class Config {
        // Configuration properties
        var enabled: Boolean = true
        var customProperty: String = "default"
    }
    
    override fun prepare(block: Config.() -> Unit): CustomPlugin {
        val config = Config().apply(block)
        return CustomPlugin(config)
    }
    
    override fun install(plugin: CustomPlugin, scope: HttpClient) {
        // Install request interceptors
        scope.requestPipeline.intercept(HttpRequestPipeline.Before) {
            // Modify request
        }
        
        // Install response interceptors
        scope.responsePipeline.intercept(HttpResponsePipeline.Receive) {
            // Process response
        }
        
        // Setup cleanup
        scope.monitor.subscribe(HttpClientEvents.Closed) {
            // Cleanup resources
        }
    }
}

class CustomPlugin(private val config: Config) {
    // Plugin implementation
}

Usage Examples:

// Example: Request/Response logging plugin
object RequestResponseLogger : HttpClientPlugin<RequestResponseLogger.Config, RequestResponseLogger> {
    override val key = AttributeKey<RequestResponseLogger>("RequestResponseLogger")
    
    class Config {
        var logRequests: Boolean = true
        var logResponses: Boolean = true
        var logger: (String) -> Unit = ::println
    }
    
    override fun prepare(block: Config.() -> Unit) = RequestResponseLogger(Config().apply(block))
    
    override fun install(plugin: RequestResponseLogger, scope: HttpClient) {
        if (plugin.config.logRequests) {
            scope.requestPipeline.intercept(HttpRequestPipeline.Before) {
                plugin.config.logger("Request: ${context.method} ${context.url}")
            }
        }
        
        if (plugin.config.logResponses) {
            scope.responsePipeline.intercept(HttpResponsePipeline.Receive) {
                plugin.config.logger("Response: ${context.status}")
            }
        }
    }
}

class RequestResponseLogger(val config: Config)

// Usage
val client = HttpClient {
    install(RequestResponseLogger) {
        logRequests = true
        logResponses = true
        logger = { message -> 
            println("[HTTP] $message")
        }
    }
}

Plugin Pipeline Integration

Understanding how plugins integrate with request/response pipelines.

/**
 * Request pipeline phases where plugins can intercept
 */
object HttpRequestPipeline {
    val Before: PipelinePhase
    val State: PipelinePhase
    val Transform: PipelinePhase
    val Render: PipelinePhase
    val Send: PipelinePhase
}

/**
 * Response pipeline phases where plugins can intercept
 */
object HttpResponsePipeline {
    val Receive: PipelinePhase
    val Parse: PipelinePhase
    val Transform: PipelinePhase
    val State: PipelinePhase
    val After: PipelinePhase
}

Usage Examples:

// Plugin intercepting at different pipeline phases
scope.requestPipeline.intercept(HttpRequestPipeline.Before) {
    // Early request modification
    context.headers.append("X-Early-Header", "value")
}

scope.requestPipeline.intercept(HttpRequestPipeline.State) {
    // Request state management
    context.attributes.put(StateKey, "some-state")
}

scope.responsePipeline.intercept(HttpResponsePipeline.Receive) {
    // Early response processing
    if (context.status == HttpStatusCode.Unauthorized) {
        // Handle auth refresh
    }
}

scope.responsePipeline.intercept(HttpResponsePipeline.After) {
    // Final response processing
    logResponseMetrics(context)
}

Progress Tracking Plugin

Upload and download progress monitoring with customizable progress listeners.

/**
 * Body progress tracking plugin
 */
object BodyProgress : HttpClientPlugin<Unit, BodyProgress> {
    override val key: AttributeKey<BodyProgress> = AttributeKey("BodyProgress")
}

/** Progress listener typealias */
typealias ProgressListener = suspend (bytesSentTotal: Long, contentLength: Long) -> Unit

/** Configure upload progress tracking */
fun HttpRequestBuilder.onUpload(listener: ProgressListener?)

/** Configure download progress tracking */  
fun HttpRequestBuilder.onDownload(listener: ProgressListener?)

Usage Examples:

val client = HttpClient {
    install(BodyProgress)
}

// Track upload progress
val response = client.post("https://api.example.com/upload") {
    setBody(fileContent)
    onUpload { bytesSentTotal, contentLength ->
        val progress = (bytesSentTotal.toDouble() / contentLength * 100).toInt()
        println("Upload progress: $progress% ($bytesSentTotal/$contentLength bytes)")
    }
}

// Track download progress
val response = client.get("https://example.com/large-file.zip") {
    onDownload { bytesReceivedTotal, contentLength ->
        val progress = if (contentLength > 0) {
            (bytesReceivedTotal.toDouble() / contentLength * 100).toInt()
        } else {
            -1 // Unknown content length
        }
        if (progress >= 0) {
            println("Download progress: $progress% ($bytesReceivedTotal/$contentLength bytes)")
        } else {
            println("Downloaded: $bytesReceivedTotal bytes")
        }
    }
}

// File upload with progress
val file = File("document.pdf")
val response = client.post("https://api.example.com/documents") {
    setBody(file.readBytes())
    onUpload { sent, total ->
        println("Uploading ${file.name}: ${(sent * 100 / total)}%")
    }
}

Default Request Plugin

Configures default request parameters applied to all requests made by the client.

/**
 * Default request configuration plugin
 */
object DefaultRequest : HttpClientPlugin<DefaultRequest.DefaultRequestBuilder, DefaultRequest> {
    override val key: AttributeKey<DefaultRequest> = AttributeKey("DefaultRequest")
    
    /**
     * Default request builder for configuring defaults
     */
    class DefaultRequestBuilder : HttpRequestBuilder() {
        fun host(value: String)
        fun port(value: Int)
        fun headers(block: HeadersBuilder.() -> Unit)
        fun cookie(name: String, value: String, encoding: CookieEncoding = CookieEncoding.URI_ENCODING)
    }
}

/** Configure default request settings in client config */
fun HttpClientConfig<*>.defaultRequest(block: DefaultRequest.DefaultRequestBuilder.() -> Unit)

Usage Examples:

val client = HttpClient {
    install(DefaultRequest) {
        // Default host and port
        host = "api.example.com"
        port = 443
        url.protocol = URLProtocol.HTTPS
        
        // Default headers
        headers {
            append("User-Agent", "MyApp/1.0")
            append("Accept", "application/json")
        }
        
        // Default authentication
        bearerAuth("default-token")
        
        // Default parameters
        parameter("version", "v1")
        parameter("format", "json")
    }
}

// All requests will inherit defaults
val response1 = client.get("/users") // GET https://api.example.com:443/users?version=v1&format=json
val response2 = client.post("/users") { // POST with default headers and auth
    contentType(ContentType.Application.Json)
    setBody(newUser)
}

// Override defaults per request
val response3 = client.get("https://other-api.com/data") {
    // This overrides the default host
    bearerAuth("different-token") // Override default auth
}

// Alternative configuration using defaultRequest extension
val client2 = HttpClient {
    defaultRequest {
        url("https://jsonplaceholder.typicode.com/")
        header("X-Custom", "value")
    }
}

Plugin Creation APIs

Modern plugin creation utilities for simplified plugin development with DSL support.

/**
 * Creates a client plugin with configuration support
 */
fun <PluginConfigT> createClientPlugin(
    name: String,
    createConfiguration: () -> PluginConfigT,
    body: ClientPluginBuilder<PluginConfigT>.() -> Unit
): ClientPlugin<PluginConfigT>

/**
 * Creates a client plugin without configuration
 */
fun createClientPlugin(
    name: String,
    body: ClientPluginBuilder<Unit>.() -> Unit
): ClientPlugin<Unit>

/**
 * Simplified plugin interface
 */
interface ClientPlugin<PluginConfig : Any> {
    val key: AttributeKey<*>
    fun prepare(block: PluginConfig.() -> Unit = {}): Any
    fun install(plugin: Any, scope: HttpClient)
}

/**
 * Plugin builder DSL for simplified plugin creation
 */
class ClientPluginBuilder<PluginConfig : Any> {
    /** Plugin configuration hook */
    fun onRequest(block: suspend OnRequestContext.(request: HttpRequestBuilder, config: PluginConfig) -> Unit)
    
    /** Response processing hook */
    fun onResponse(block: suspend OnResponseContext.(call: HttpClientCall, config: PluginConfig) -> Unit)
    
    /** Request body transformation hook */
    fun transformRequestBody(block: suspend TransformRequestBodyContext.(request: HttpRequestBuilder, config: PluginConfig) -> Unit)
    
    /** Response body transformation hook */
    fun transformResponseBody(block: suspend TransformResponseBodyContext.(call: HttpClientCall, config: PluginConfig) -> Unit)
    
    /** Plugin installation hook */
    fun onInstall(block: (HttpClient, PluginConfig) -> Unit)
    
    /** Plugin close hook */
    fun onClose(block: (PluginConfig) -> Unit)
}

/** Context classes for plugin hooks */
class OnRequestContext(val client: HttpClient)
class OnResponseContext(val client: HttpClient)
class TransformRequestBodyContext(val client: HttpClient)
class TransformResponseBodyContext(val client: HttpClient)

/** Plugin access functions */
fun <B : Any, F : Any> HttpClient.pluginOrNull(plugin: HttpClientPlugin<B, F>): F?
fun <B : Any, F : Any> HttpClient.plugin(plugin: HttpClientPlugin<B, F>): F

Usage Examples:

import io.ktor.client.plugins.api.*

// Simple plugin without configuration
val RequestLogging = createClientPlugin("RequestLogging") {
    onRequest { request, _ ->
        println("Making request to: ${request.url}")
    }
    
    onResponse { call, _ ->
        println("Received response: ${call.response.status}")
    }
}

// Plugin with configuration
val CustomHeaders = createClientPlugin(
    name = "CustomHeaders",
    createConfiguration = { CustomHeadersConfig() }
) {
    val headers = mutableMapOf<String, String>()
    
    onInstall { client, config ->
        headers.putAll(config.headers)
    }
    
    onRequest { request, config ->
        headers.forEach { (name, value) ->
            request.headers[name] = value
        }
    }
}

data class CustomHeadersConfig(
    val headers: MutableMap<String, String> = mutableMapOf()
) {
    fun header(name: String, value: String) {
        headers[name] = value
    }
}

// Install and use plugins
val client = HttpClient {
    install(RequestLogging)
    
    install(CustomHeaders) {
        header("X-Custom-Client", "MyApp/1.0")
        header("X-Request-ID", UUID.randomUUID().toString())
    }
}

// Access plugin instance
val customHeaders = client.pluginOrNull(CustomHeaders)
if (customHeaders != null) {
    println("CustomHeaders plugin is installed")
}

Install with Tessl CLI

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

docs

client-configuration.md

cookie-management.md

engine-configuration.md

form-handling.md

http-caching.md

index.md

plugin-system.md

request-building.md

response-handling.md

websocket-support.md

tile.json