CtrlK
BlogDocsLog inGet started
Tessl Logo

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

Ktor HTTP Client Core - a multiplatform asynchronous HTTP client library for Kotlin providing comprehensive HTTP request/response handling with plugin architecture.

Pending
Overview
Eval results
Files

http-caching.mddocs/

HTTP Caching

HTTP response caching with configurable storage, cache control handling, and comprehensive caching strategies for improved performance.

Capabilities

HTTP Cache Plugin

Install and configure the HttpCache plugin for automatic response caching.

/**
 * HTTP Cache plugin for response caching
 */
object HttpCache : HttpClientPlugin<HttpCacheConfig, HttpCacheConfig> {
    override val key: AttributeKey<HttpCacheConfig>
    
    /**
     * Cache configuration
     */
    class HttpCacheConfig {
        /** Cache storage implementation */
        var storage: HttpCacheStorage = UnlimitedCacheStorage()
        
        /** Whether to use cache-control headers */
        var useHeaders: Boolean = true
        
        /** Default cache validity period */
        var defaultValidityPeriod: Duration = Duration.INFINITE
    }
}

Usage Examples:

val client = HttpClient {
    install(HttpCache) {
        // Use default unlimited storage
        storage = UnlimitedCacheStorage()
        
        // Respect cache-control headers
        useHeaders = true
        
        // Default cache period if no headers specify
        defaultValidityPeriod = 1.hours
    }
}

// Subsequent identical requests will be served from cache
val response1 = client.get("https://api.example.com/data")
val response2 = client.get("https://api.example.com/data") // Served from cache

// Cache respects HTTP cache headers like Cache-Control, ETag, etc.
val apiResponse = client.get("https://api.example.com/users") {
    header("Cache-Control", "max-age=300") // Cache for 5 minutes
}

Cache Storage Interface

Core interface for HTTP cache storage implementations.

/**
 * HTTP cache storage interface
 */
interface HttpCacheStorage {
    /**
     * Find cached response for URL and vary keys
     * @param url Request URL
     * @param varyKeys Headers used for cache key variation
     * @returns Cached entry or null if not found/expired
     */
    suspend fun find(url: Url, varyKeys: Map<String, String>): HttpCacheEntry?
    
    /**
     * Store response in cache
     * @param url Request URL
     * @param data Cache entry to store
     */
    suspend fun store(url: Url, data: HttpCacheEntry)
    
    /**
     * Find cached response for URL with headers
     * @param url Request URL
     * @param requestHeaders Request headers for vary key calculation
     * @returns Cached entry or null
     */
    suspend fun findByUrl(url: Url, requestHeaders: Headers): HttpCacheEntry? = 
        find(url, calculateVaryKeys(requestHeaders))
        
    /**
     * Clear all cached entries
     */
    suspend fun clear()
    
    /**
     * Clear expired entries
     */
    suspend fun clearExpired()
}

Built-in Storage Implementations

Ready-to-use cache storage implementations for different scenarios.

/**
 * Unlimited memory-based cache storage
 */
class UnlimitedCacheStorage : HttpCacheStorage {
    override suspend fun find(url: Url, varyKeys: Map<String, String>): HttpCacheEntry?
    override suspend fun store(url: Url, data: HttpCacheEntry)
    override suspend fun clear()
    override suspend fun clearExpired()
}

/**
 * Disabled cache storage (no caching)
 */
object DisabledCacheStorage : HttpCacheStorage {
    override suspend fun find(url: Url, varyKeys: Map<String, String>): HttpCacheEntry? = null
    override suspend fun store(url: Url, data: HttpCacheEntry) = Unit
    override suspend fun clear() = Unit
    override suspend fun clearExpired() = Unit
}

/**
 * LRU cache storage with size limits
 */
class LRUCacheStorage(
    private val maxEntries: Int = 1000,
    private val maxSizeBytes: Long = 100 * 1024 * 1024 // 100MB
) : HttpCacheStorage {
    override suspend fun find(url: Url, varyKeys: Map<String, String>): HttpCacheEntry?
    override suspend fun store(url: Url, data: HttpCacheEntry)
    override suspend fun clear()
    override suspend fun clearExpired()
}

Usage Examples:

// Unlimited caching (default)
val clientUnlimited = HttpClient {
    install(HttpCache) {
        storage = UnlimitedCacheStorage()
    }
}

// No caching
val clientNoCache = HttpClient {
    install(HttpCache) {
        storage = DisabledCacheStorage
    }
}

// LRU cache with limits
val clientLRU = HttpClient {
    install(HttpCache) {
        storage = LRUCacheStorage(
            maxEntries = 500,
            maxSizeBytes = 50 * 1024 * 1024 // 50MB
        )
    }
}

Cache Entry Representation

Comprehensive cache entry with all necessary metadata and content.

/**
 * HTTP cache entry representation
 */
data class HttpCacheEntry(
    /** 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,
    
    /** Response body content */
    val body: ByteArray,
    
    /** Vary keys for conditional caching */
    val varyKeys: Map<String, String> = emptyMap()
) {
    /**
     * Check if cache entry is expired
     * @param now Current time (default: current system time)
     * @returns True if expired
     */
    fun isExpired(now: GMTDate = GMTDate.now()): Boolean = now > expires
    
    /**
     * Check if entry is still fresh
     * @param now Current time
     * @returns True if fresh
     */
    fun isFresh(now: GMTDate = GMTDate.now()): Boolean = !isExpired(now)
    
    /**
     * Get age of cache entry in seconds
     * @param now Current time
     * @returns Age in seconds
     */
    fun getAge(now: GMTDate = GMTDate.now()): Long = (now.timestamp - responseTime.timestamp) / 1000
    
    /**
     * Create HTTP response from cache entry
     * @param call Associated HTTP call
     * @returns HttpResponse representing cached data
     */
    fun toHttpResponse(call: HttpClientCall): HttpResponse
}

Usage Examples:

val client = HttpClient {
    install(HttpCache)
}

// Make request that will be cached
val response = client.get("https://api.example.com/data")

// Access cache entry details (for debugging/monitoring)
val cacheStorage = client.plugin(HttpCache).storage
val cacheEntry = cacheStorage.find(
    Url("https://api.example.com/data"),
    emptyMap()
)

cacheEntry?.let { entry ->
    println("Cache entry details:")
    println("URL: ${entry.url}")
    println("Status: ${entry.statusCode}")
    println("Cached at: ${entry.responseTime}")
    println("Expires: ${entry.expires}")
    println("Age: ${entry.getAge()} seconds")
    println("Fresh: ${entry.isFresh()}")
    println("Body size: ${entry.body.size} bytes")
    
    // Check cache headers
    entry.headers.forEach { name, values ->
        println("Header $name: ${values.joinToString()}")
    }
}

Cache Control Handling

Functions for working with HTTP cache control headers and policies.

/**
 * Cache control utilities
 */
object CacheControl {
    /**
     * Parse Cache-Control header
     * @param headerValue Cache-Control header value
     * @returns Parsed cache control directives
     */
    fun parse(headerValue: String): CacheControlDirectives
    
    /**
     * Check if response is cacheable
     * @param response HTTP response
     * @returns True if response can be cached
     */
    fun isCacheable(response: HttpResponse): Boolean
    
    /**
     * Calculate cache expiration time
     * @param response HTTP response
     * @param requestTime When request was made
     * @param defaultTtl Default TTL if no cache headers
     * @returns Expiration time
     */
    fun calculateExpiration(
        response: HttpResponse,
        requestTime: GMTDate,
        defaultTtl: Duration = Duration.INFINITE
    ): GMTDate
    
    /**
     * Check if cached entry needs revalidation
     * @param entry Cache entry
     * @param now Current time
     * @returns True if revalidation needed
     */
    fun needsRevalidation(entry: HttpCacheEntry, now: GMTDate = GMTDate.now()): Boolean
}

/**
 * Cache control directives
 */
data class CacheControlDirectives(
    val maxAge: Long? = null,
    val maxStale: Long? = null,
    val minFresh: Long? = null,
    val noCache: Boolean = false,
    val noStore: Boolean = false,
    val noTransform: Boolean = false,
    val onlyIfCached: Boolean = false,
    val mustRevalidate: Boolean = false,
    val public: Boolean = false,
    val private: Boolean = false,
    val proxyRevalidate: Boolean = false,
    val sMaxAge: Long? = null,
    val extensions: Map<String, String?> = emptyMap()
)

Usage Examples:

val client = HttpClient {
    install(HttpCache) {
        // Custom cache validation
        storage = object : HttpCacheStorage by UnlimitedCacheStorage() {
            override suspend fun find(url: Url, varyKeys: Map<String, String>): HttpCacheEntry? {
                val entry = super.find(url, varyKeys)
                return if (entry != null && CacheControl.needsRevalidation(entry)) {
                    null // Force fresh request
                } else {
                    entry
                }
            }
        }
    }
}

// Make request with specific cache control
val response = client.get("https://api.example.com/data") {
    header("Cache-Control", "max-age=300, must-revalidate")
}

// Check if response was served from cache
val cacheControlHeader = response.headers["Cache-Control"]
if (cacheControlHeader != null) {
    val directives = CacheControl.parse(cacheControlHeader)
    println("Max-Age: ${directives.maxAge}")
    println("No-Cache: ${directives.noCache}")
    println("Must-Revalidate: ${directives.mustRevalidate}")
}

// Manual cache control
if (CacheControl.isCacheable(response)) {
    println("Response is cacheable")
    val expiration = CacheControl.calculateExpiration(
        response,
        GMTDate.now(),
        1.hours
    )
    println("Expires at: $expiration")
}

Custom Cache Storage

Create custom cache storage implementations for specific requirements.

/**
 * Example: File-based cache storage
 */
class FileCacheStorage(
    private val cacheDir: File,
    private val maxSizeBytes: Long = 100 * 1024 * 1024
) : HttpCacheStorage {
    
    override suspend fun find(url: Url, varyKeys: Map<String, String>): HttpCacheEntry? {
        val cacheKey = generateCacheKey(url, varyKeys)
        val cacheFile = File(cacheDir, cacheKey)
        
        if (!cacheFile.exists()) return null
        
        return try {
            val entry = deserializeCacheEntry(cacheFile.readBytes())
            if (entry.isExpired()) {
                cacheFile.delete()
                null
            } else {
                entry
            }
        } catch (e: Exception) {
            cacheFile.delete()
            null
        }
    }
    
    override suspend fun store(url: Url, data: HttpCacheEntry) {
        if (data.isExpired()) return
        
        val cacheKey = generateCacheKey(url, data.varyKeys)
        val cacheFile = File(cacheDir, cacheKey)
        
        ensureCacheSize()
        
        try {
            cacheFile.writeBytes(serializeCacheEntry(data))
        } catch (e: Exception) {
            cacheFile.delete()
            throw e
        }
    }
    
    override suspend fun clear() {
        cacheDir.listFiles()?.forEach { it.delete() }
    }
    
    override suspend fun clearExpired() {
        cacheDir.listFiles()?.forEach { file ->
            try {
                val entry = deserializeCacheEntry(file.readBytes())
                if (entry.isExpired()) {
                    file.delete()
                }
            } catch (e: Exception) {
                file.delete()
            }
        }
    }
    
    private fun generateCacheKey(url: Url, varyKeys: Map<String, String>): String {
        // Generate unique cache key
        return "${url.buildString().hashCode()}_${varyKeys.hashCode()}"
    }
    
    private fun serializeCacheEntry(entry: HttpCacheEntry): ByteArray {
        // Serialize cache entry to bytes
        return ByteArray(0) // Placeholder
    }
    
    private fun deserializeCacheEntry(data: ByteArray): HttpCacheEntry {
        // Deserialize cache entry from bytes
        throw NotImplementedError("Placeholder")
    }
    
    private fun ensureCacheSize() {
        // Implement cache size management
    }
}

/**
 * Example: Redis-backed cache storage
 */
class RedisCacheStorage(
    private val redis: RedisClient,
    private val keyPrefix: String = "ktor_cache:",
    private val defaultTtl: Duration = 1.hours
) : HttpCacheStorage {
    
    override suspend fun find(url: Url, varyKeys: Map<String, String>): HttpCacheEntry? {
        val key = "$keyPrefix${generateKey(url, varyKeys)}"
        val data = redis.get(key) ?: return null
        
        return try {
            deserializeCacheEntry(data)
        } catch (e: Exception) {
            redis.delete(key)
            null
        }
    }
    
    override suspend fun store(url: Url, data: HttpCacheEntry) {
        val key = "$keyPrefix${generateKey(url, data.varyKeys)}"
        val serialized = serializeCacheEntry(data)
        
        val ttl = if (data.isExpired()) {
            return // Don't store expired entries
        } else {
            val remaining = data.expires.timestamp - GMTDate.now().timestamp
            maxOf(remaining / 1000, 1) // At least 1 second
        }
        
        redis.setex(key, ttl.toInt(), serialized)
    }
    
    override suspend fun clear() {
        val keys = redis.keys("$keyPrefix*")
        if (keys.isNotEmpty()) {
            redis.del(*keys.toTypedArray())
        }
    }
    
    override suspend fun clearExpired() {
        // Redis handles expiration automatically
    }
    
    private fun generateKey(url: Url, varyKeys: Map<String, String>): String {
        return "${url.buildString().hashCode()}_${varyKeys.hashCode()}"
    }
    
    private fun serializeCacheEntry(entry: HttpCacheEntry): String {
        // Serialize to JSON or other format
        return "" // Placeholder
    }
    
    private fun deserializeCacheEntry(data: String): HttpCacheEntry {
        // Deserialize from stored format
        throw NotImplementedError("Placeholder")
    }
}

Usage Examples:

// File-based caching
val fileStorage = FileCacheStorage(
    cacheDir = File("./cache"),
    maxSizeBytes = 200 * 1024 * 1024 // 200MB
)

val clientFile = HttpClient {
    install(HttpCache) {
        storage = fileStorage
    }
}

// Redis-based caching
val redisStorage = RedisCacheStorage(
    redis = createRedisClient(),
    keyPrefix = "myapp_cache:",
    defaultTtl = 30.minutes
)

val clientRedis = HttpClient {
    install(HttpCache) {
        storage = redisStorage
    }
}

// Memory cache with custom eviction
class CustomMemoryCache(
    private val maxEntries: Int = 1000
) : HttpCacheStorage {
    private val cache = Collections.synchronizedMap(
        object : LinkedHashMap<String, HttpCacheEntry>(16, 0.75f, true) {
            override fun removeEldestEntry(eldest: Map.Entry<String, HttpCacheEntry>): Boolean {
                return size > maxEntries
            }
        }
    )
    
    override suspend fun find(url: Url, varyKeys: Map<String, String>): HttpCacheEntry? {
        val key = "${url.buildString()}_${varyKeys.hashCode()}"
        val entry = cache[key]
        return if (entry?.isExpired() == true) {
            cache.remove(key)
            null
        } else {
            entry
        }
    }
    
    override suspend fun store(url: Url, data: HttpCacheEntry) {
        if (!data.isExpired()) {
            val key = "${url.buildString()}_${data.varyKeys.hashCode()}"
            cache[key] = data
        }
    }
    
    override suspend fun clear() {
        cache.clear()
    }
    
    override suspend fun clearExpired() {
        val now = GMTDate.now()
        cache.entries.removeAll { it.value.isExpired(now) }
    }
}

Types

Cache Types

/**
 * Duration representation for cache timeouts
 */
data class Duration(
    val nanoseconds: Long
) {
    companion object {
        val ZERO: Duration
        val INFINITE: Duration
        
        fun ofSeconds(seconds: Long): Duration
        fun ofMinutes(minutes: Long): Duration
        fun ofHours(hours: Long): Duration
        fun ofDays(days: Long): Duration
    }
    
    val inWholeSeconds: Long
    val inWholeMinutes: Long
    val inWholeHours: Long
    val inWholeDays: Long
    
    operator fun plus(other: Duration): Duration
    operator fun minus(other: Duration): Duration
    operator fun times(scale: Int): Duration
    operator fun div(scale: Int): Duration
}

/**
 * Cache key calculation utilities
 */
object CacheKeyUtils {
    /**
     * Calculate vary keys from headers
     * @param headers Request headers
     * @param varyHeader Vary header value from response
     * @returns Map of header names to values for cache key
     */
    fun calculateVaryKeys(headers: Headers, varyHeader: String?): Map<String, String>
    
    /**
     * Generate cache key for URL and vary keys
     * @param url Request URL
     * @param varyKeys Vary key headers
     * @returns Unique cache key string
     */
    fun generateCacheKey(url: Url, varyKeys: Map<String, String>): String
}

Install with Tessl CLI

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

docs

client-configuration.md

cookie-management.md

forms-and-uploads.md

http-caching.md

http-requests.md

index.md

plugin-system.md

response-handling.md

server-sent-events.md

websockets.md

tile.json