or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

builtin-plugins.mdcaching.mdcookies.mdengine-configuration.mdforms.mdhttp-client.mdindex.mdplugin-system.mdrequest-building.mdresponse-handling.mdresponse-observation.mdutilities.mdwebsockets.md

caching.mddocs/

0

# HTTP Caching

1

2

The Ktor HTTP Client Core provides comprehensive HTTP response caching functionality through the `HttpCache` plugin. This enables automatic caching of HTTP responses with configurable storage backends, cache control header support, and custom cache validation logic to improve performance and reduce network requests.

3

4

## Core Cache API

5

6

### HttpCache Plugin

7

8

The main plugin for HTTP response caching that automatically handles cache storage and retrieval based on HTTP semantics.

9

10

```kotlin { .api }

11

object HttpCache : HttpClientPlugin<HttpCache.Config, HttpCache> {

12

class Config {

13

var publicStorage: HttpCacheStorage = UnlimitedCacheStorage()

14

var privateStorage: HttpCacheStorage = UnlimitedCacheStorage()

15

var useOldConnection: Boolean = true

16

17

fun publicStorage(storage: HttpCacheStorage)

18

fun privateStorage(storage: HttpCacheStorage)

19

}

20

}

21

```

22

23

### HttpCacheStorage Interface

24

25

Base interface for implementing custom cache storage backends.

26

27

```kotlin { .api }

28

interface HttpCacheStorage {

29

suspend fun find(url: Url, vary: Map<String, String>): HttpCacheEntry?

30

suspend fun findAll(url: Url): Set<HttpCacheEntry>

31

suspend fun store(url: Url, data: HttpCacheEntry)

32

}

33

```

34

35

### HttpCacheEntry

36

37

Represents a cached HTTP response with metadata and content.

38

39

```kotlin { .api }

40

data class HttpCacheEntry(

41

val url: Url,

42

val statusCode: HttpStatusCode,

43

val requestTime: GMTDate,

44

val responseTime: GMTDate,

45

val version: HttpProtocolVersion,

46

val expires: GMTDate,

47

val vary: Map<String, String>,

48

val varyKeys: Set<String>,

49

val body: ByteArray,

50

val headers: Headers

51

) {

52

fun isStale(): Boolean

53

fun age(): Long

54

}

55

```

56

57

## Built-in Storage Implementations

58

59

### UnlimitedCacheStorage

60

61

Default storage implementation that caches all responses in memory without size limits.

62

63

```kotlin { .api }

64

class UnlimitedCacheStorage : HttpCacheStorage {

65

override suspend fun find(url: Url, vary: Map<String, String>): HttpCacheEntry?

66

override suspend fun findAll(url: Url): Set<HttpCacheEntry>

67

override suspend fun store(url: Url, data: HttpCacheEntry)

68

}

69

```

70

71

### DisabledCacheStorage

72

73

No-op storage implementation that disables caching completely.

74

75

```kotlin { .api }

76

object DisabledCacheStorage : HttpCacheStorage {

77

override suspend fun find(url: Url, vary: Map<String, String>): HttpCacheEntry? = null

78

override suspend fun findAll(url: Url): Set<HttpCacheEntry> = emptySet()

79

override suspend fun store(url: Url, data: HttpCacheEntry) = Unit

80

}

81

```

82

83

## Basic Usage

84

85

### Simple HTTP Caching

86

87

```kotlin

88

val client = HttpClient {

89

install(HttpCache)

90

}

91

92

// First request - response is cached

93

val response1 = client.get("https://api.example.com/data")

94

val data1 = response1.bodyAsText()

95

96

// Second request - served from cache if still valid

97

val response2 = client.get("https://api.example.com/data")

98

val data2 = response2.bodyAsText() // Same as data1, served from cache

99

100

client.close()

101

```

102

103

### Custom Storage Configuration

104

105

```kotlin

106

val client = HttpClient {

107

install(HttpCache) {

108

publicStorage = UnlimitedCacheStorage()

109

privateStorage = UnlimitedCacheStorage()

110

}

111

}

112

```

113

114

### Disabling Cache

115

116

```kotlin

117

val client = HttpClient {

118

install(HttpCache) {

119

publicStorage = DisabledCacheStorage

120

privateStorage = DisabledCacheStorage

121

}

122

}

123

```

124

125

## Cache Control Headers

126

127

### Server-Side Cache Control

128

129

The cache respects standard HTTP cache control headers sent by servers:

130

131

- `Cache-Control: max-age=3600` - Cache for 1 hour

132

- `Cache-Control: no-cache` - Revalidate on each request

133

- `Cache-Control: no-store` - Don't cache at all

134

- `Cache-Control: private` - Cache only in private storage

135

- `Cache-Control: public` - Cache in public storage

136

- `Expires` - Absolute expiration date

137

- `ETag` - Entity tag for conditional requests

138

- `Last-Modified` - Last modification date for conditional requests

139

140

```kotlin

141

val client = HttpClient {

142

install(HttpCache)

143

}

144

145

// Server response with cache headers:

146

// Cache-Control: max-age=300, public

147

// ETag: "abc123"

148

val response = client.get("https://api.example.com/data")

149

150

// Subsequent request within 5 minutes will be served from cache

151

val cachedResponse = client.get("https://api.example.com/data")

152

```

153

154

### Client-Side Cache Control

155

156

Control caching behavior on individual requests:

157

158

```kotlin

159

val client = HttpClient {

160

install(HttpCache)

161

}

162

163

// Force fresh request (bypass cache)

164

val freshResponse = client.get("https://api.example.com/data") {

165

header("Cache-Control", "no-cache")

166

}

167

168

// Only use cache if available

169

val cachedOnlyResponse = client.get("https://api.example.com/data") {

170

header("Cache-Control", "only-if-cached")

171

}

172

173

// Set maximum acceptable age

174

val maxStaleResponse = client.get("https://api.example.com/data") {

175

header("Cache-Control", "max-stale=60") // Accept cache up to 1 minute stale

176

}

177

```

178

179

## Conditional Requests

180

181

### ETag Validation

182

183

```kotlin

184

val client = HttpClient {

185

install(HttpCache)

186

}

187

188

// First request stores ETag

189

val response1 = client.get("https://api.example.com/resource")

190

191

// Subsequent request includes If-None-Match header

192

// Server returns 304 Not Modified if unchanged

193

val response2 = client.get("https://api.example.com/resource")

194

// Automatically handled by cache plugin

195

```

196

197

### Last-Modified Validation

198

199

```kotlin

200

val client = HttpClient {

201

install(HttpCache)

202

}

203

204

// First request stores Last-Modified date

205

val response1 = client.get("https://api.example.com/document")

206

207

// Subsequent request includes If-Modified-Since header

208

val response2 = client.get("https://api.example.com/document")

209

// Returns cached version if not modified

210

```

211

212

## Advanced Caching Features

213

214

### Vary Header Handling

215

216

The cache properly handles `Vary` headers to store different versions of responses based on request headers:

217

218

```kotlin

219

val client = HttpClient {

220

install(HttpCache)

221

}

222

223

// Server responds with: Vary: Accept-Language, Accept-Encoding

224

val englishResponse = client.get("https://api.example.com/content") {

225

header("Accept-Language", "en-US")

226

}

227

228

val frenchResponse = client.get("https://api.example.com/content") {

229

header("Accept-Language", "fr-FR")

230

}

231

232

// Different cached versions for different languages

233

```

234

235

### Cache Invalidation

236

237

```kotlin

238

class InvalidatingCacheStorage : HttpCacheStorage {

239

private val storage = UnlimitedCacheStorage()

240

241

override suspend fun find(url: Url, vary: Map<String, String>): HttpCacheEntry? {

242

val entry = storage.find(url, vary)

243

return if (entry?.isStale() == true) {

244

null // Treat stale entries as cache miss

245

} else {

246

entry

247

}

248

}

249

250

override suspend fun findAll(url: Url): Set<HttpCacheEntry> {

251

return storage.findAll(url).filterNot { it.isStale() }.toSet()

252

}

253

254

override suspend fun store(url: Url, data: HttpCacheEntry) {

255

storage.store(url, data)

256

}

257

}

258

```

259

260

## Custom Storage Implementations

261

262

### Size-Limited Cache Storage

263

264

```kotlin

265

class LimitedSizeCacheStorage(

266

private val maxSizeBytes: Long

267

) : HttpCacheStorage {

268

private val cache = mutableMapOf<String, HttpCacheEntry>()

269

private var currentSize = 0L

270

271

override suspend fun find(url: Url, vary: Map<String, String>): HttpCacheEntry? {

272

val key = generateKey(url, vary)

273

return cache[key]?.takeUnless { it.isStale() }

274

}

275

276

override suspend fun findAll(url: Url): Set<HttpCacheEntry> {

277

return cache.values.filter {

278

it.url.toString().startsWith(url.toString()) && !it.isStale()

279

}.toSet()

280

}

281

282

override suspend fun store(url: Url, data: HttpCacheEntry) {

283

val key = generateKey(url, data.vary)

284

val entrySize = data.body.size.toLong()

285

286

// Evict entries if necessary

287

while (currentSize + entrySize > maxSizeBytes && cache.isNotEmpty()) {

288

evictLeastRecentlyUsed()

289

}

290

291

if (entrySize <= maxSizeBytes) {

292

cache[key] = data

293

currentSize += entrySize

294

}

295

}

296

297

private fun generateKey(url: Url, vary: Map<String, String>): String {

298

return "${url}:${vary.entries.sortedBy { it.key }.joinToString(",") { "${it.key}=${it.value}" }}"

299

}

300

301

private fun evictLeastRecentlyUsed() {

302

// Implementation for LRU eviction

303

val oldestEntry = cache.values.minByOrNull { it.responseTime }

304

if (oldestEntry != null) {

305

cache.entries.removeAll { it.value == oldestEntry }

306

currentSize -= oldestEntry.body.size

307

}

308

}

309

}

310

```

311

312

### Persistent Cache Storage

313

314

```kotlin

315

class FileCacheStorage(

316

private val cacheDirectory: File

317

) : HttpCacheStorage {

318

319

init {

320

cacheDirectory.mkdirs()

321

}

322

323

override suspend fun find(url: Url, vary: Map<String, String>): HttpCacheEntry? {

324

val cacheFile = getCacheFile(url, vary)

325

return if (cacheFile.exists()) {

326

try {

327

deserializeCacheEntry(cacheFile.readBytes())

328

} catch (e: Exception) {

329

null // Corrupted cache entry

330

}

331

} else {

332

null

333

}

334

}

335

336

override suspend fun findAll(url: Url): Set<HttpCacheEntry> {

337

return cacheDirectory.listFiles()

338

?.mapNotNull { file ->

339

try {

340

val entry = deserializeCacheEntry(file.readBytes())

341

if (entry.url.toString().startsWith(url.toString())) entry else null

342

} catch (e: Exception) {

343

null

344

}

345

}?.toSet() ?: emptySet()

346

}

347

348

override suspend fun store(url: Url, data: HttpCacheEntry) {

349

val cacheFile = getCacheFile(url, data.vary)

350

val serializedData = serializeCacheEntry(data)

351

cacheFile.writeBytes(serializedData)

352

}

353

354

private fun getCacheFile(url: Url, vary: Map<String, String>): File {

355

val filename = generateCacheFileName(url, vary)

356

return File(cacheDirectory, filename)

357

}

358

359

private fun generateCacheFileName(url: Url, vary: Map<String, String>): String {

360

// Generate unique filename based on URL and vary parameters

361

val urlHash = url.toString().hashCode()

362

val varyHash = vary.hashCode()

363

return "${urlHash}_${varyHash}.cache"

364

}

365

366

private fun serializeCacheEntry(entry: HttpCacheEntry): ByteArray {

367

// Implement serialization (JSON, protobuf, etc.)

368

TODO("Implement serialization")

369

}

370

371

private fun deserializeCacheEntry(data: ByteArray): HttpCacheEntry {

372

// Implement deserialization

373

TODO("Implement deserialization")

374

}

375

}

376

```

377

378

### Redis Cache Storage

379

380

```kotlin

381

class RedisCacheStorage(

382

private val redisClient: RedisClient,

383

private val keyPrefix: String = "ktor-cache:"

384

) : HttpCacheStorage {

385

386

override suspend fun find(url: Url, vary: Map<String, String>): HttpCacheEntry? {

387

val key = generateRedisKey(url, vary)

388

val serializedEntry = redisClient.get(key) ?: return null

389

390

return try {

391

deserializeCacheEntry(serializedEntry)

392

} catch (e: Exception) {

393

null

394

}

395

}

396

397

override suspend fun findAll(url: Url): Set<HttpCacheEntry> {

398

val pattern = "$keyPrefix${url.encodedPath}*"

399

val keys = redisClient.keys(pattern)

400

401

return keys.mapNotNull { key ->

402

try {

403

redisClient.get(key)?.let { deserializeCacheEntry(it) }

404

} catch (e: Exception) {

405

null

406

}

407

}.toSet()

408

}

409

410

override suspend fun store(url: Url, data: HttpCacheEntry) {

411

val key = generateRedisKey(url, data.vary)

412

val serializedEntry = serializeCacheEntry(data)

413

414

// Set with TTL based on cache entry expiration

415

val ttlSeconds = (data.expires.timestamp - Clock.System.now().epochSeconds).coerceAtLeast(0)

416

redisClient.setex(key, ttlSeconds.toInt(), serializedEntry)

417

}

418

419

private fun generateRedisKey(url: Url, vary: Map<String, String>): String {

420

val varyString = vary.entries.sortedBy { it.key }.joinToString(",") { "${it.key}=${it.value}" }

421

return "$keyPrefix${url}:$varyString"

422

}

423

424

private fun serializeCacheEntry(entry: HttpCacheEntry): String {

425

// Implement JSON or other serialization

426

TODO("Implement serialization")

427

}

428

429

private fun deserializeCacheEntry(data: String): HttpCacheEntry {

430

// Implement deserialization

431

TODO("Implement deserialization")

432

}

433

}

434

```

435

436

## Cache Debugging and Monitoring

437

438

### Cache Hit/Miss Logging

439

440

```kotlin

441

class LoggingCacheStorage(

442

private val delegate: HttpCacheStorage,

443

private val logger: Logger

444

) : HttpCacheStorage by delegate {

445

446

override suspend fun find(url: Url, vary: Map<String, String>): HttpCacheEntry? {

447

val entry = delegate.find(url, vary)

448

if (entry != null) {

449

logger.info("Cache HIT for $url")

450

} else {

451

logger.info("Cache MISS for $url")

452

}

453

return entry

454

}

455

456

override suspend fun store(url: Url, data: HttpCacheEntry) {

457

logger.info("Caching response for $url (size: ${data.body.size} bytes)")

458

delegate.store(url, data)

459

}

460

}

461

```

462

463

### Cache Statistics

464

465

```kotlin

466

class StatisticsCacheStorage(

467

private val delegate: HttpCacheStorage

468

) : HttpCacheStorage by delegate {

469

private var hitCount = AtomicLong(0)

470

private var missCount = AtomicLong(0)

471

private var storeCount = AtomicLong(0)

472

473

val hitRatio: Double get() = hitCount.get().toDouble() / (hitCount.get() + missCount.get())

474

475

override suspend fun find(url: Url, vary: Map<String, String>): HttpCacheEntry? {

476

val entry = delegate.find(url, vary)

477

if (entry != null) {

478

hitCount.incrementAndGet()

479

} else {

480

missCount.incrementAndGet()

481

}

482

return entry

483

}

484

485

override suspend fun store(url: Url, data: HttpCacheEntry) {

486

storeCount.incrementAndGet()

487

delegate.store(url, data)

488

}

489

490

fun getStatistics(): CacheStatistics {

491

return CacheStatistics(

492

hits = hitCount.get(),

493

misses = missCount.get(),

494

stores = storeCount.get(),

495

hitRatio = hitRatio

496

)

497

}

498

}

499

500

data class CacheStatistics(

501

val hits: Long,

502

val misses: Long,

503

val stores: Long,

504

val hitRatio: Double

505

)

506

```

507

508

## Cache Configuration Best Practices

509

510

### Production Cache Setup

511

512

```kotlin

513

val client = HttpClient {

514

install(HttpCache) {

515

// Use separate storages for public and private content

516

publicStorage = LimitedSizeCacheStorage(maxSizeBytes = 50 * 1024 * 1024) // 50MB

517

privateStorage = FileCacheStorage(File("cache/private"))

518

}

519

}

520

```

521

522

### Development Cache Setup

523

524

```kotlin

525

val client = HttpClient {

526

install(HttpCache) {

527

if (developmentMode) {

528

// Disable caching in development

529

publicStorage = DisabledCacheStorage

530

privateStorage = DisabledCacheStorage

531

} else {

532

publicStorage = UnlimitedCacheStorage()

533

privateStorage = UnlimitedCacheStorage()

534

}

535

}

536

}

537

```

538

539

## Best Practices

540

541

1. **Choose appropriate storage**: Use memory storage for short-lived applications, persistent storage for long-running ones

542

2. **Set size limits**: Implement size limits to prevent memory issues

543

3. **Respect cache headers**: Always honor server-sent cache control headers

544

4. **Handle stale data**: Implement proper handling of stale cache entries

545

5. **Monitor cache performance**: Track hit/miss ratios to optimize cache effectiveness

546

6. **Secure cached data**: Be careful with sensitive data in shared cache storage

547

7. **Clean up expired entries**: Implement cleanup mechanisms for persistent storage

548

8. **Handle cache invalidation**: Provide mechanisms to invalidate specific cache entries when needed

549

9. **Test cache behavior**: Test your application with and without caching enabled

550

10. **Consider network conditions**: Adjust cache strategies based on network reliability