Ktor HTTP Client Core - a multiplatform asynchronous HTTP client library for Kotlin providing comprehensive HTTP request/response handling with plugin architecture.
—
HTTP response caching with configurable storage, cache control handling, and comprehensive caching strategies for improved performance.
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
}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()
}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
)
}
}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()}")
}
}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")
}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) }
}
}/**
* 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