0
# HTTP Caching
1
2
HTTP response caching with configurable storage, cache control handling, and comprehensive caching strategies for improved performance.
3
4
## Capabilities
5
6
### HTTP Cache Plugin
7
8
Install and configure the HttpCache plugin for automatic response caching.
9
10
```kotlin { .api }
11
/**
12
* HTTP Cache plugin for response caching
13
*/
14
object HttpCache : HttpClientPlugin<HttpCacheConfig, HttpCacheConfig> {
15
override val key: AttributeKey<HttpCacheConfig>
16
17
/**
18
* Cache configuration
19
*/
20
class HttpCacheConfig {
21
/** Cache storage implementation */
22
var storage: HttpCacheStorage = UnlimitedCacheStorage()
23
24
/** Whether to use cache-control headers */
25
var useHeaders: Boolean = true
26
27
/** Default cache validity period */
28
var defaultValidityPeriod: Duration = Duration.INFINITE
29
}
30
}
31
```
32
33
**Usage Examples:**
34
35
```kotlin
36
val client = HttpClient {
37
install(HttpCache) {
38
// Use default unlimited storage
39
storage = UnlimitedCacheStorage()
40
41
// Respect cache-control headers
42
useHeaders = true
43
44
// Default cache period if no headers specify
45
defaultValidityPeriod = 1.hours
46
}
47
}
48
49
// Subsequent identical requests will be served from cache
50
val response1 = client.get("https://api.example.com/data")
51
val response2 = client.get("https://api.example.com/data") // Served from cache
52
53
// Cache respects HTTP cache headers like Cache-Control, ETag, etc.
54
val apiResponse = client.get("https://api.example.com/users") {
55
header("Cache-Control", "max-age=300") // Cache for 5 minutes
56
}
57
```
58
59
### Cache Storage Interface
60
61
Core interface for HTTP cache storage implementations.
62
63
```kotlin { .api }
64
/**
65
* HTTP cache storage interface
66
*/
67
interface HttpCacheStorage {
68
/**
69
* Find cached response for URL and vary keys
70
* @param url Request URL
71
* @param varyKeys Headers used for cache key variation
72
* @returns Cached entry or null if not found/expired
73
*/
74
suspend fun find(url: Url, varyKeys: Map<String, String>): HttpCacheEntry?
75
76
/**
77
* Store response in cache
78
* @param url Request URL
79
* @param data Cache entry to store
80
*/
81
suspend fun store(url: Url, data: HttpCacheEntry)
82
83
/**
84
* Find cached response for URL with headers
85
* @param url Request URL
86
* @param requestHeaders Request headers for vary key calculation
87
* @returns Cached entry or null
88
*/
89
suspend fun findByUrl(url: Url, requestHeaders: Headers): HttpCacheEntry? =
90
find(url, calculateVaryKeys(requestHeaders))
91
92
/**
93
* Clear all cached entries
94
*/
95
suspend fun clear()
96
97
/**
98
* Clear expired entries
99
*/
100
suspend fun clearExpired()
101
}
102
```
103
104
### Built-in Storage Implementations
105
106
Ready-to-use cache storage implementations for different scenarios.
107
108
```kotlin { .api }
109
/**
110
* Unlimited memory-based cache storage
111
*/
112
class UnlimitedCacheStorage : HttpCacheStorage {
113
override suspend fun find(url: Url, varyKeys: Map<String, String>): HttpCacheEntry?
114
override suspend fun store(url: Url, data: HttpCacheEntry)
115
override suspend fun clear()
116
override suspend fun clearExpired()
117
}
118
119
/**
120
* Disabled cache storage (no caching)
121
*/
122
object DisabledCacheStorage : HttpCacheStorage {
123
override suspend fun find(url: Url, varyKeys: Map<String, String>): HttpCacheEntry? = null
124
override suspend fun store(url: Url, data: HttpCacheEntry) = Unit
125
override suspend fun clear() = Unit
126
override suspend fun clearExpired() = Unit
127
}
128
129
/**
130
* LRU cache storage with size limits
131
*/
132
class LRUCacheStorage(
133
private val maxEntries: Int = 1000,
134
private val maxSizeBytes: Long = 100 * 1024 * 1024 // 100MB
135
) : HttpCacheStorage {
136
override suspend fun find(url: Url, varyKeys: Map<String, String>): HttpCacheEntry?
137
override suspend fun store(url: Url, data: HttpCacheEntry)
138
override suspend fun clear()
139
override suspend fun clearExpired()
140
}
141
```
142
143
**Usage Examples:**
144
145
```kotlin
146
// Unlimited caching (default)
147
val clientUnlimited = HttpClient {
148
install(HttpCache) {
149
storage = UnlimitedCacheStorage()
150
}
151
}
152
153
// No caching
154
val clientNoCache = HttpClient {
155
install(HttpCache) {
156
storage = DisabledCacheStorage
157
}
158
}
159
160
// LRU cache with limits
161
val clientLRU = HttpClient {
162
install(HttpCache) {
163
storage = LRUCacheStorage(
164
maxEntries = 500,
165
maxSizeBytes = 50 * 1024 * 1024 // 50MB
166
)
167
}
168
}
169
```
170
171
### Cache Entry Representation
172
173
Comprehensive cache entry with all necessary metadata and content.
174
175
```kotlin { .api }
176
/**
177
* HTTP cache entry representation
178
*/
179
data class HttpCacheEntry(
180
/** Original request URL */
181
val url: Url,
182
183
/** Response status code */
184
val statusCode: HttpStatusCode,
185
186
/** Request timestamp */
187
val requestTime: GMTDate,
188
189
/** Response timestamp */
190
val responseTime: GMTDate,
191
192
/** HTTP protocol version */
193
val version: HttpProtocolVersion,
194
195
/** Cache expiration time */
196
val expires: GMTDate,
197
198
/** Response headers */
199
val headers: Headers,
200
201
/** Response body content */
202
val body: ByteArray,
203
204
/** Vary keys for conditional caching */
205
val varyKeys: Map<String, String> = emptyMap()
206
) {
207
/**
208
* Check if cache entry is expired
209
* @param now Current time (default: current system time)
210
* @returns True if expired
211
*/
212
fun isExpired(now: GMTDate = GMTDate.now()): Boolean = now > expires
213
214
/**
215
* Check if entry is still fresh
216
* @param now Current time
217
* @returns True if fresh
218
*/
219
fun isFresh(now: GMTDate = GMTDate.now()): Boolean = !isExpired(now)
220
221
/**
222
* Get age of cache entry in seconds
223
* @param now Current time
224
* @returns Age in seconds
225
*/
226
fun getAge(now: GMTDate = GMTDate.now()): Long = (now.timestamp - responseTime.timestamp) / 1000
227
228
/**
229
* Create HTTP response from cache entry
230
* @param call Associated HTTP call
231
* @returns HttpResponse representing cached data
232
*/
233
fun toHttpResponse(call: HttpClientCall): HttpResponse
234
}
235
```
236
237
**Usage Examples:**
238
239
```kotlin
240
val client = HttpClient {
241
install(HttpCache)
242
}
243
244
// Make request that will be cached
245
val response = client.get("https://api.example.com/data")
246
247
// Access cache entry details (for debugging/monitoring)
248
val cacheStorage = client.plugin(HttpCache).storage
249
val cacheEntry = cacheStorage.find(
250
Url("https://api.example.com/data"),
251
emptyMap()
252
)
253
254
cacheEntry?.let { entry ->
255
println("Cache entry details:")
256
println("URL: ${entry.url}")
257
println("Status: ${entry.statusCode}")
258
println("Cached at: ${entry.responseTime}")
259
println("Expires: ${entry.expires}")
260
println("Age: ${entry.getAge()} seconds")
261
println("Fresh: ${entry.isFresh()}")
262
println("Body size: ${entry.body.size} bytes")
263
264
// Check cache headers
265
entry.headers.forEach { name, values ->
266
println("Header $name: ${values.joinToString()}")
267
}
268
}
269
```
270
271
### Cache Control Handling
272
273
Functions for working with HTTP cache control headers and policies.
274
275
```kotlin { .api }
276
/**
277
* Cache control utilities
278
*/
279
object CacheControl {
280
/**
281
* Parse Cache-Control header
282
* @param headerValue Cache-Control header value
283
* @returns Parsed cache control directives
284
*/
285
fun parse(headerValue: String): CacheControlDirectives
286
287
/**
288
* Check if response is cacheable
289
* @param response HTTP response
290
* @returns True if response can be cached
291
*/
292
fun isCacheable(response: HttpResponse): Boolean
293
294
/**
295
* Calculate cache expiration time
296
* @param response HTTP response
297
* @param requestTime When request was made
298
* @param defaultTtl Default TTL if no cache headers
299
* @returns Expiration time
300
*/
301
fun calculateExpiration(
302
response: HttpResponse,
303
requestTime: GMTDate,
304
defaultTtl: Duration = Duration.INFINITE
305
): GMTDate
306
307
/**
308
* Check if cached entry needs revalidation
309
* @param entry Cache entry
310
* @param now Current time
311
* @returns True if revalidation needed
312
*/
313
fun needsRevalidation(entry: HttpCacheEntry, now: GMTDate = GMTDate.now()): Boolean
314
}
315
316
/**
317
* Cache control directives
318
*/
319
data class CacheControlDirectives(
320
val maxAge: Long? = null,
321
val maxStale: Long? = null,
322
val minFresh: Long? = null,
323
val noCache: Boolean = false,
324
val noStore: Boolean = false,
325
val noTransform: Boolean = false,
326
val onlyIfCached: Boolean = false,
327
val mustRevalidate: Boolean = false,
328
val public: Boolean = false,
329
val private: Boolean = false,
330
val proxyRevalidate: Boolean = false,
331
val sMaxAge: Long? = null,
332
val extensions: Map<String, String?> = emptyMap()
333
)
334
```
335
336
**Usage Examples:**
337
338
```kotlin
339
val client = HttpClient {
340
install(HttpCache) {
341
// Custom cache validation
342
storage = object : HttpCacheStorage by UnlimitedCacheStorage() {
343
override suspend fun find(url: Url, varyKeys: Map<String, String>): HttpCacheEntry? {
344
val entry = super.find(url, varyKeys)
345
return if (entry != null && CacheControl.needsRevalidation(entry)) {
346
null // Force fresh request
347
} else {
348
entry
349
}
350
}
351
}
352
}
353
}
354
355
// Make request with specific cache control
356
val response = client.get("https://api.example.com/data") {
357
header("Cache-Control", "max-age=300, must-revalidate")
358
}
359
360
// Check if response was served from cache
361
val cacheControlHeader = response.headers["Cache-Control"]
362
if (cacheControlHeader != null) {
363
val directives = CacheControl.parse(cacheControlHeader)
364
println("Max-Age: ${directives.maxAge}")
365
println("No-Cache: ${directives.noCache}")
366
println("Must-Revalidate: ${directives.mustRevalidate}")
367
}
368
369
// Manual cache control
370
if (CacheControl.isCacheable(response)) {
371
println("Response is cacheable")
372
val expiration = CacheControl.calculateExpiration(
373
response,
374
GMTDate.now(),
375
1.hours
376
)
377
println("Expires at: $expiration")
378
}
379
```
380
381
### Custom Cache Storage
382
383
Create custom cache storage implementations for specific requirements.
384
385
```kotlin { .api }
386
/**
387
* Example: File-based cache storage
388
*/
389
class FileCacheStorage(
390
private val cacheDir: File,
391
private val maxSizeBytes: Long = 100 * 1024 * 1024
392
) : HttpCacheStorage {
393
394
override suspend fun find(url: Url, varyKeys: Map<String, String>): HttpCacheEntry? {
395
val cacheKey = generateCacheKey(url, varyKeys)
396
val cacheFile = File(cacheDir, cacheKey)
397
398
if (!cacheFile.exists()) return null
399
400
return try {
401
val entry = deserializeCacheEntry(cacheFile.readBytes())
402
if (entry.isExpired()) {
403
cacheFile.delete()
404
null
405
} else {
406
entry
407
}
408
} catch (e: Exception) {
409
cacheFile.delete()
410
null
411
}
412
}
413
414
override suspend fun store(url: Url, data: HttpCacheEntry) {
415
if (data.isExpired()) return
416
417
val cacheKey = generateCacheKey(url, data.varyKeys)
418
val cacheFile = File(cacheDir, cacheKey)
419
420
ensureCacheSize()
421
422
try {
423
cacheFile.writeBytes(serializeCacheEntry(data))
424
} catch (e: Exception) {
425
cacheFile.delete()
426
throw e
427
}
428
}
429
430
override suspend fun clear() {
431
cacheDir.listFiles()?.forEach { it.delete() }
432
}
433
434
override suspend fun clearExpired() {
435
cacheDir.listFiles()?.forEach { file ->
436
try {
437
val entry = deserializeCacheEntry(file.readBytes())
438
if (entry.isExpired()) {
439
file.delete()
440
}
441
} catch (e: Exception) {
442
file.delete()
443
}
444
}
445
}
446
447
private fun generateCacheKey(url: Url, varyKeys: Map<String, String>): String {
448
// Generate unique cache key
449
return "${url.buildString().hashCode()}_${varyKeys.hashCode()}"
450
}
451
452
private fun serializeCacheEntry(entry: HttpCacheEntry): ByteArray {
453
// Serialize cache entry to bytes
454
return ByteArray(0) // Placeholder
455
}
456
457
private fun deserializeCacheEntry(data: ByteArray): HttpCacheEntry {
458
// Deserialize cache entry from bytes
459
throw NotImplementedError("Placeholder")
460
}
461
462
private fun ensureCacheSize() {
463
// Implement cache size management
464
}
465
}
466
467
/**
468
* Example: Redis-backed cache storage
469
*/
470
class RedisCacheStorage(
471
private val redis: RedisClient,
472
private val keyPrefix: String = "ktor_cache:",
473
private val defaultTtl: Duration = 1.hours
474
) : HttpCacheStorage {
475
476
override suspend fun find(url: Url, varyKeys: Map<String, String>): HttpCacheEntry? {
477
val key = "$keyPrefix${generateKey(url, varyKeys)}"
478
val data = redis.get(key) ?: return null
479
480
return try {
481
deserializeCacheEntry(data)
482
} catch (e: Exception) {
483
redis.delete(key)
484
null
485
}
486
}
487
488
override suspend fun store(url: Url, data: HttpCacheEntry) {
489
val key = "$keyPrefix${generateKey(url, data.varyKeys)}"
490
val serialized = serializeCacheEntry(data)
491
492
val ttl = if (data.isExpired()) {
493
return // Don't store expired entries
494
} else {
495
val remaining = data.expires.timestamp - GMTDate.now().timestamp
496
maxOf(remaining / 1000, 1) // At least 1 second
497
}
498
499
redis.setex(key, ttl.toInt(), serialized)
500
}
501
502
override suspend fun clear() {
503
val keys = redis.keys("$keyPrefix*")
504
if (keys.isNotEmpty()) {
505
redis.del(*keys.toTypedArray())
506
}
507
}
508
509
override suspend fun clearExpired() {
510
// Redis handles expiration automatically
511
}
512
513
private fun generateKey(url: Url, varyKeys: Map<String, String>): String {
514
return "${url.buildString().hashCode()}_${varyKeys.hashCode()}"
515
}
516
517
private fun serializeCacheEntry(entry: HttpCacheEntry): String {
518
// Serialize to JSON or other format
519
return "" // Placeholder
520
}
521
522
private fun deserializeCacheEntry(data: String): HttpCacheEntry {
523
// Deserialize from stored format
524
throw NotImplementedError("Placeholder")
525
}
526
}
527
```
528
529
**Usage Examples:**
530
531
```kotlin
532
// File-based caching
533
val fileStorage = FileCacheStorage(
534
cacheDir = File("./cache"),
535
maxSizeBytes = 200 * 1024 * 1024 // 200MB
536
)
537
538
val clientFile = HttpClient {
539
install(HttpCache) {
540
storage = fileStorage
541
}
542
}
543
544
// Redis-based caching
545
val redisStorage = RedisCacheStorage(
546
redis = createRedisClient(),
547
keyPrefix = "myapp_cache:",
548
defaultTtl = 30.minutes
549
)
550
551
val clientRedis = HttpClient {
552
install(HttpCache) {
553
storage = redisStorage
554
}
555
}
556
557
// Memory cache with custom eviction
558
class CustomMemoryCache(
559
private val maxEntries: Int = 1000
560
) : HttpCacheStorage {
561
private val cache = Collections.synchronizedMap(
562
object : LinkedHashMap<String, HttpCacheEntry>(16, 0.75f, true) {
563
override fun removeEldestEntry(eldest: Map.Entry<String, HttpCacheEntry>): Boolean {
564
return size > maxEntries
565
}
566
}
567
)
568
569
override suspend fun find(url: Url, varyKeys: Map<String, String>): HttpCacheEntry? {
570
val key = "${url.buildString()}_${varyKeys.hashCode()}"
571
val entry = cache[key]
572
return if (entry?.isExpired() == true) {
573
cache.remove(key)
574
null
575
} else {
576
entry
577
}
578
}
579
580
override suspend fun store(url: Url, data: HttpCacheEntry) {
581
if (!data.isExpired()) {
582
val key = "${url.buildString()}_${data.varyKeys.hashCode()}"
583
cache[key] = data
584
}
585
}
586
587
override suspend fun clear() {
588
cache.clear()
589
}
590
591
override suspend fun clearExpired() {
592
val now = GMTDate.now()
593
cache.entries.removeAll { it.value.isExpired(now) }
594
}
595
}
596
```
597
598
## Types
599
600
### Cache Types
601
602
```kotlin { .api }
603
/**
604
* Duration representation for cache timeouts
605
*/
606
data class Duration(
607
val nanoseconds: Long
608
) {
609
companion object {
610
val ZERO: Duration
611
val INFINITE: Duration
612
613
fun ofSeconds(seconds: Long): Duration
614
fun ofMinutes(minutes: Long): Duration
615
fun ofHours(hours: Long): Duration
616
fun ofDays(days: Long): Duration
617
}
618
619
val inWholeSeconds: Long
620
val inWholeMinutes: Long
621
val inWholeHours: Long
622
val inWholeDays: Long
623
624
operator fun plus(other: Duration): Duration
625
operator fun minus(other: Duration): Duration
626
operator fun times(scale: Int): Duration
627
operator fun div(scale: Int): Duration
628
}
629
630
/**
631
* Cache key calculation utilities
632
*/
633
object CacheKeyUtils {
634
/**
635
* Calculate vary keys from headers
636
* @param headers Request headers
637
* @param varyHeader Vary header value from response
638
* @returns Map of header names to values for cache key
639
*/
640
fun calculateVaryKeys(headers: Headers, varyHeader: String?): Map<String, String>
641
642
/**
643
* Generate cache key for URL and vary keys
644
* @param url Request URL
645
* @param varyKeys Vary key headers
646
* @returns Unique cache key string
647
*/
648
fun generateCacheKey(url: Url, varyKeys: Map<String, String>): String
649
}
650
```