Ktor HTTP client core library - asynchronous framework for creating HTTP clients in Kotlin multiplatform
—
Response caching with configurable storage backends, cache control support, and HTTP-compliant caching behavior for improved performance.
Core plugin for HTTP response caching with public and private storage options.
/**
* HTTP response caching plugin
*/
class HttpCache internal constructor(
private val publicStorage: CacheStorage,
private val privateStorage: CacheStorage,
private val isSharedClient: Boolean
) {
/**
* Cache configuration
*/
class Config {
/** Whether client is shared between multiple users */
var isShared: Boolean = false
/**
* Configure public cache storage
*/
fun publicStorage(storage: CacheStorage)
/**
* Configure private cache storage
*/
fun privateStorage(storage: CacheStorage)
}
companion object : HttpClientPlugin<Config, HttpCache> {
override val key: AttributeKey<HttpCache> = AttributeKey("HttpCache")
/** Event fired when response is served from cache */
val HttpResponseFromCache: EventDefinition<HttpResponse>
}
}Usage Examples:
import io.ktor.client.plugins.cache.*
val client = HttpClient {
install(HttpCache) {
// Configure as shared client
isShared = false
// Use unlimited cache storage
publicStorage(CacheStorage.Unlimited())
privateStorage(CacheStorage.Unlimited())
}
// Monitor cache hits
monitor.subscribe(HttpCache.HttpResponseFromCache) { response ->
println("Cache hit for: ${response.request.url}")
}
}
// First request - fetches from server and caches
val response1 = client.get("https://api.example.com/data")
println("First request: ${response1.status}")
// Second request - served from cache if cacheable
val response2 = client.get("https://api.example.com/data")
println("Second request: ${response2.status}")Modern cache storage interface for storing and retrieving cached responses.
/**
* Cache storage interface for HTTP responses
*/
interface CacheStorage {
/**
* Store cached response data
*/
suspend fun store(url: Url, data: CachedResponseData)
/**
* Find cached response with vary key matching
*/
suspend fun find(url: Url, varyKeys: Map<String, String>): CachedResponseData?
/**
* Find all cached responses for URL
*/
suspend fun findAll(url: Url): Set<CachedResponseData>
companion object {
/** Create unlimited in-memory cache storage */
fun Unlimited(): CacheStorage
/** Disabled cache storage (no-op) */
val Disabled: CacheStorage
}
}Pre-built storage implementations for different caching strategies.
/**
* Unlimited in-memory cache storage
*/
class UnlimitedStorage : CacheStorage {
override suspend fun store(url: Url, data: CachedResponseData)
override suspend fun find(url: Url, varyKeys: Map<String, String>): CachedResponseData?
override suspend fun findAll(url: Url): Set<CachedResponseData>
}
/**
* Disabled cache storage (no-op implementation)
*/
object DisabledStorage : CacheStorage {
override suspend fun store(url: Url, data: CachedResponseData) {}
override suspend fun find(url: Url, varyKeys: Map<String, String>): CachedResponseData? = null
override suspend fun findAll(url: Url): Set<CachedResponseData> = emptySet()
}Usage Examples:
// Unlimited caching
val unlimitedClient = HttpClient {
install(HttpCache) {
publicStorage(CacheStorage.Unlimited())
privateStorage(CacheStorage.Unlimited())
}
}
// Disabled caching
val noCacheClient = HttpClient {
install(HttpCache) {
publicStorage(CacheStorage.Disabled)
privateStorage(CacheStorage.Disabled)
}
}
// Custom cache storage with size limit
class LimitedCacheStorage(private val maxEntries: Int) : CacheStorage {
private val cache = LinkedHashMap<String, CachedResponseData>()
override suspend fun store(url: Url, data: CachedResponseData) {
val key = url.toString()
if (cache.size >= maxEntries && key !in cache) {
// Remove oldest entry
cache.remove(cache.keys.first())
}
cache[key] = data
}
override suspend fun find(url: Url, varyKeys: Map<String, String>): CachedResponseData? {
return cache[url.toString()]
}
override suspend fun findAll(url: Url): Set<CachedResponseData> {
return cache[url.toString()]?.let { setOf(it) } ?: emptySet()
}
}
val limitedClient = HttpClient {
install(HttpCache) {
publicStorage(LimitedCacheStorage(maxEntries = 100))
privateStorage(LimitedCacheStorage(maxEntries = 50))
}
}Data class representing cached HTTP response with metadata.
/**
* Cached response data with HTTP metadata
*/
data class CachedResponseData(
/** Original request URL */
val url: Url,
/** Response status code */
val statusCode: HttpStatusCode,
/** Request timestamp */
val requestTime: GMTDate,
/** Response timestamp */
val responseTime: GMTDate,
/** HTTP protocol version */
val version: HttpProtocolVersion,
/** Cache expiration time */
val expires: GMTDate,
/** Response headers */
val headers: Headers,
/** Vary header matching keys */
val varyKeys: Map<String, String>,
/** Response body content */
val body: ByteArray
)Usage Examples:
// Create cached response data manually
val cachedData = CachedResponseData(
url = Url("https://api.example.com/data"),
statusCode = HttpStatusCode.OK,
requestTime = GMTDate(),
responseTime = GMTDate(),
version = HttpProtocolVersion.HTTP_1_1,
expires = GMTDate() + 3600_000, // 1 hour from now
headers = headersOf(
HttpHeaders.ContentType, "application/json",
HttpHeaders.CacheControl, "public, max-age=3600"
),
varyKeys = mapOf(
"Accept-Language" to "en-US",
"Accept-Encoding" to "gzip"
),
body = """{"message": "cached data"}""".toByteArray()
)
// Store in cache manually
val storage = CacheStorage.Unlimited()
storage.store(cachedData.url, cachedData)
// Retrieve from cache
val retrieved = storage.find(
url = Url("https://api.example.com/data"),
varyKeys = mapOf(
"Accept-Language" to "en-US",
"Accept-Encoding" to "gzip"
)
)
if (retrieved != null) {
println("Found cached data: ${String(retrieved.body)}")
println("Expires: ${retrieved.expires}")
}Support for HTTP cache control directives and validation.
/**
* Cache storage extensions for response handling
*/
suspend fun CacheStorage.store(response: HttpResponse): CachedResponseData
suspend fun CacheStorage.store(
response: HttpResponse,
varyKeys: Map<String, String>
): CachedResponseData
suspend fun CacheStorage.store(
response: HttpResponse,
varyKeys: Map<String, String>,
isShared: Boolean
): CachedResponseDataUsage Examples:
val client = HttpClient {
install(HttpCache) {
publicStorage(CacheStorage.Unlimited())
}
}
// Responses with cache headers are automatically cached
val response1 = client.get("https://api.example.com/public-data") {
headers {
// Request headers that might affect caching
append(HttpHeaders.AcceptLanguage, "en-US")
append(HttpHeaders.AcceptEncoding, "gzip")
}
}
// Check if response was cached
val cacheControl = response1.headers[HttpHeaders.CacheControl]
println("Cache-Control: $cacheControl")
// Conditional requests with cache validation
val response2 = client.get("https://api.example.com/data") {
headers {
// Add conditional headers for cache validation
append(HttpHeaders.IfModifiedSince, lastModified)
append(HttpHeaders.IfNoneMatch, etag)
}
}
// Handle 304 Not Modified responses
if (response2.status == HttpStatusCode.NotModified) {
println("Content not modified, using cached version")
}Events for monitoring cache behavior and performance.
/**
* Event fired when response is served from cache
*/
val HttpCache.HttpResponseFromCache: EventDefinition<HttpResponse>Usage Examples:
val client = HttpClient {
install(HttpCache) {
publicStorage(CacheStorage.Unlimited())
}
// Monitor cache performance
monitor.subscribe(HttpCache.HttpResponseFromCache) { response ->
println("Cache HIT: ${response.request.url}")
println("Status: ${response.status}")
println("Cache headers: ${response.headers[HttpHeaders.CacheControl]}")
}
}
// Track cache statistics
var cacheHits = 0
var totalRequests = 0
client.monitor.subscribe(HttpRequestCreated) { request ->
totalRequests++
}
client.monitor.subscribe(HttpCache.HttpResponseFromCache) { response ->
cacheHits++
val hitRate = (cacheHits.toFloat() / totalRequests) * 100
println("Cache hit rate: ${String.format("%.1f%%", hitRate)}")
}
// Make requests to see cache behavior
repeat(5) {
val response = client.get("https://api.example.com/static-data")
println("Request $it: ${response.status}")
}Exception handling for cache-related errors.
/**
* Exception thrown when cache is in invalid state
*/
class InvalidCacheStateException(message: String) : IllegalStateException(message)Usage Examples:
try {
val client = HttpClient {
install(HttpCache) {
publicStorage(CustomCacheStorage())
}
}
val response = client.get("https://api.example.com/data")
} catch (e: InvalidCacheStateException) {
println("Cache error: ${e.message}")
// Handle cache corruption or invalid state
} catch (e: Exception) {
println("Other error: ${e.message}")
}Advanced usage patterns for complex caching scenarios.
// Custom cache storage with persistence
class PersistentCacheStorage(private val cacheDir: File) : CacheStorage {
override suspend fun store(url: Url, data: CachedResponseData) {
// Store to filesystem or database
}
override suspend fun find(url: Url, varyKeys: Map<String, String>): CachedResponseData? {
// Load from persistent storage
return null
}
override suspend fun findAll(url: Url): Set<CachedResponseData> {
// Load all variants from storage
return emptySet()
}
}Usage Examples:
// Multi-tier caching with memory + disk
class TieredCacheStorage(
private val memoryCache: CacheStorage,
private val diskCache: CacheStorage
) : CacheStorage {
override suspend fun store(url: Url, data: CachedResponseData) {
// Store in both memory and disk
memoryCache.store(url, data)
diskCache.store(url, data)
}
override suspend fun find(url: Url, varyKeys: Map<String, String>): CachedResponseData? {
// Try memory first, then disk
return memoryCache.find(url, varyKeys)
?: diskCache.find(url, varyKeys)?.also {
// Promote to memory cache
memoryCache.store(url, it)
}
}
override suspend fun findAll(url: Url): Set<CachedResponseData> {
val memory = memoryCache.findAll(url)
val disk = diskCache.findAll(url)
return memory + disk
}
}
val client = HttpClient {
install(HttpCache) {
publicStorage(TieredCacheStorage(
memoryCache = CacheStorage.Unlimited(),
diskCache = PersistentCacheStorage(File("cache"))
))
}
}Install with Tessl CLI
npx tessl i tessl/maven-io-ktor--ktor-client-core-iosx64