or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

client-configuration.mdcookie-management.mdforms-and-uploads.mdhttp-caching.mdhttp-requests.mdindex.mdplugin-system.mdresponse-handling.mdserver-sent-events.mdwebsockets.md

http-caching.mddocs/

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

```