Ktor HTTP Client Core for tvOS ARM64 - multiplatform asynchronous HTTP client library with coroutines support
—
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.
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 = {}
)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()
}
}
}
}val client = HttpClient {
install(LoggingPlugin) {
logger = { message ->
println("[HTTP] $message")
}
logHeaders = true
logBody = false
}
}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)
}
}
}val client = HttpClient {
install(MyPlugin) {
enabled = true
retryCount = 5
timeout = 60000
customHeader = "MyApp/1.0"
addInterceptor { data ->
// Custom logic
proceed()
}
}
}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")
}
}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")
}
}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()
}
}// 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)
}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
)// 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
}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")
}
}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
}
}
}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
}
}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()
}
}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)
}
}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
}
}
}
}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()
}@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