Ktor HTTP client core library - asynchronous framework for creating HTTP clients in Kotlin multiplatform
—
Extensible plugin architecture for adding cross-cutting concerns like authentication, caching, content negotiation, and logging to HTTP clients.
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
}
}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())
}
}
}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>): FUsage 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")
}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)"
}
}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}")
}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}")
}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}")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()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")
}
}
}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)
}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)}%")
}
}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")
}
}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>): FUsage 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