or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

action-caching.mdcore-cache-operations.mdindex.mdnamed-caches.md

action-caching.mddocs/

0

# Action-Level HTTP Caching

1

2

Action-level HTTP caching provides built-in support for caching Play Framework action results with proper HTTP headers (ETag, Expires) for client-side caching coordination. This includes both server-side response caching and client cache validation.

3

4

## Capabilities

5

6

### Cached Helper Class (Scala)

7

8

The Cached class provides a fluent API for creating cacheable actions with various caching strategies.

9

10

```scala { .api }

11

/**

12

* A helper to add caching to an Action

13

*/

14

class Cached @Inject() (cache: AsyncCacheApi)(implicit materializer: Materializer) {

15

/**

16

* Cache an action with custom key and caching function

17

* @param key Compute a key from the request header

18

* @param caching Compute a cache duration from the resource header

19

* @return CachedBuilder for further configuration

20

*/

21

def apply(key: RequestHeader => String, caching: PartialFunction[ResponseHeader, Duration]): CachedBuilder

22

23

/**

24

* Cache an action with custom key (no expiration)

25

* @param key Compute a key from the request header

26

* @return CachedBuilder for further configuration

27

*/

28

def apply(key: RequestHeader => String): CachedBuilder

29

30

/**

31

* Cache an action with fixed key (no expiration)

32

* @param key Cache key

33

* @return CachedBuilder for further configuration

34

*/

35

def apply(key: String): CachedBuilder

36

37

/**

38

* Cache an action with custom key and duration in seconds

39

* @param key Compute a key from the request header

40

* @param duration Cache duration in seconds

41

* @return CachedBuilder for further configuration

42

*/

43

def apply(key: RequestHeader => String, duration: Int): CachedBuilder

44

45

/**

46

* Cache an action with custom key and duration

47

* @param key Compute a key from the request header

48

* @param duration Cache duration

49

* @return CachedBuilder for further configuration

50

*/

51

def apply(key: RequestHeader => String, duration: Duration): CachedBuilder

52

53

/**

54

* A cached instance caching nothing (useful for composition)

55

* @param key Compute a key from the request header

56

* @return CachedBuilder for further configuration

57

*/

58

def empty(key: RequestHeader => String): CachedBuilder

59

60

/**

61

* Caches everything, forever

62

* @param key Compute a key from the request header

63

* @return CachedBuilder for further configuration

64

*/

65

def everything(key: RequestHeader => String): CachedBuilder

66

67

/**

68

* Caches everything for the specified seconds

69

* @param key Compute a key from the request header

70

* @param duration Cache duration in seconds

71

* @return CachedBuilder for further configuration

72

*/

73

def everything(key: RequestHeader => String, duration: Int): CachedBuilder

74

75

/**

76

* Caches everything for the specified duration

77

* @param key Compute a key from the request header

78

* @param duration Cache duration

79

* @return CachedBuilder for further configuration

80

*/

81

def everything(key: RequestHeader => String, duration: Duration): CachedBuilder

82

83

/**

84

* Caches the specified status, for the specified number of seconds

85

* @param key Compute a key from the request header

86

* @param status HTTP status code to cache

87

* @param duration Cache duration in seconds

88

* @return CachedBuilder for further configuration

89

*/

90

def status(key: RequestHeader => String, status: Int, duration: Int): CachedBuilder

91

92

/**

93

* Caches the specified status, for the specified duration

94

* @param key Compute a key from the request header

95

* @param status HTTP status code to cache

96

* @param duration Cache duration

97

* @return CachedBuilder for further configuration

98

*/

99

def status(key: RequestHeader => String, status: Int, duration: Duration): CachedBuilder

100

101

/**

102

* Caches the specified status forever

103

* @param key Compute a key from the request header

104

* @param status HTTP status code to cache

105

* @return CachedBuilder for further configuration

106

*/

107

def status(key: RequestHeader => String, status: Int): CachedBuilder

108

109

/**

110

* A cached instance caching nothing (useful for composition)

111

* @param key Compute a key from the request header

112

* @return CachedBuilder for further configuration

113

*/

114

def empty(key: RequestHeader => String): CachedBuilder

115

116

/**

117

* Caches everything, forever

118

* @param key Compute a key from the request header

119

* @return CachedBuilder for further configuration

120

*/

121

def everything(key: RequestHeader => String): CachedBuilder

122

123

/**

124

* Caches everything for the specified seconds

125

* @param key Compute a key from the request header

126

* @param duration Cache duration in seconds

127

* @return CachedBuilder for further configuration

128

*/

129

def everything(key: RequestHeader => String, duration: Int): CachedBuilder

130

131

/**

132

* Caches everything for the specified duration

133

* @param key Compute a key from the request header

134

* @param duration Cache duration

135

* @return CachedBuilder for further configuration

136

*/

137

def everything(key: RequestHeader => String, duration: Duration): CachedBuilder

138

}

139

```

140

141

**Usage Examples:**

142

143

```scala

144

import play.api.cache.Cached

145

import play.api.mvc._

146

import javax.inject.Inject

147

import scala.concurrent.duration._

148

149

class ProductController @Inject()(

150

cc: ControllerComponents,

151

cached: Cached

152

) extends AbstractController(cc) {

153

154

// Cache with request-based key

155

def getProduct(id: String) = cached(req => s"product:$id", 1.hour) {

156

Action { implicit request =>

157

val product = loadProduct(id)

158

Ok(Json.toJson(product))

159

}

160

}

161

162

// Cache everything for 30 minutes

163

def listProducts = cached.everything(req => "products", 30.minutes) {

164

Action { implicit request =>

165

val products = loadAllProducts()

166

Ok(Json.toJson(products))

167

}

168

}

169

170

// Cache only successful responses (200 status)

171

def getProductDetails(id: String) = cached.status(req => s"product-details:$id", 200, 1.hour) {

172

Action { implicit request =>

173

loadProduct(id) match {

174

case Some(product) => Ok(Json.toJson(product))

175

case None => NotFound("Product not found")

176

}

177

}

178

}

179

180

// Conditional caching based on response headers

181

def getProductWithConditionalCache(id: String) = cached(

182

key = req => s"product-conditional:$id",

183

caching = {

184

case header if header.status == 200 => 1.hour

185

case header if header.status == 404 => 5.minutes

186

}

187

) {

188

Action { implicit request =>

189

loadProduct(id) match {

190

case Some(product) => Ok(Json.toJson(product))

191

case None => NotFound("Product not found")

192

}

193

}

194

}

195

}

196

```

197

198

### CachedBuilder Class (Scala)

199

200

The CachedBuilder provides additional configuration options for cached actions.

201

202

```scala { .api }

203

/**

204

* Builds an action with caching behavior

205

* Uses both server and client caches:

206

* - Adds an Expires header to the response, so clients can cache response content

207

* - Adds an Etag header to the response, so clients can cache response content and ask the server for freshness

208

* - Cache the result on the server, so the underlying action is not computed at each call

209

*/

210

final class CachedBuilder(

211

cache: AsyncCacheApi,

212

key: RequestHeader => String,

213

caching: PartialFunction[ResponseHeader, Duration]

214

)(implicit materializer: Materializer) {

215

216

/**

217

* Compose the cache with an action

218

* @param action The action to cache

219

* @return Cached EssentialAction

220

*/

221

def apply(action: EssentialAction): EssentialAction

222

223

/**

224

* Compose the cache with an action

225

* @param action The action to cache

226

* @return Cached EssentialAction

227

*/

228

def build(action: EssentialAction): EssentialAction

229

230

/**

231

* Whether this cache should cache the specified response if the status code match

232

* This method will cache the result forever

233

* @param status HTTP status code to cache

234

* @return New CachedBuilder with status-based caching

235

*/

236

def includeStatus(status: Int): CachedBuilder

237

238

/**

239

* Whether this cache should cache the specified response if the status code match

240

* This method will cache the result for duration seconds

241

* @param status HTTP status code to cache

242

* @param duration Cache duration in seconds

243

* @return New CachedBuilder with status-based caching

244

*/

245

def includeStatus(status: Int, duration: Int): CachedBuilder

246

247

/**

248

* Whether this cache should cache the specified response if the status code match

249

* This method will cache the result for duration

250

* @param status HTTP status code to cache

251

* @param duration Cache duration

252

* @return New CachedBuilder with status-based caching

253

*/

254

def includeStatus(status: Int, duration: Duration): CachedBuilder

255

256

/**

257

* The returned cache will store all responses whatever they may contain

258

* @param duration Cache duration

259

* @return New CachedBuilder with default caching

260

*/

261

def default(duration: Duration): CachedBuilder

262

263

/**

264

* The returned cache will store all responses whatever they may contain

265

* @param duration Cache duration in seconds

266

* @return New CachedBuilder with default caching

267

*/

268

def default(duration: Int): CachedBuilder

269

270

/**

271

* Whether this cache should cache the specified response if the status code match

272

* This method will cache the result forever

273

* @param status HTTP status code to cache

274

* @return New CachedBuilder with status-based caching

275

*/

276

def includeStatus(status: Int): CachedBuilder

277

278

/**

279

* Compose the cache with new caching function

280

* @param alternative A closure getting the response header and returning the duration we should cache for

281

* @return New CachedBuilder with composed caching logic

282

*/

283

def compose(alternative: PartialFunction[ResponseHeader, Duration]): CachedBuilder

284

}

285

```

286

287

**Usage Examples:**

288

289

```scala

290

import play.api.cache.Cached

291

import play.api.mvc._

292

import javax.inject.Inject

293

import scala.concurrent.duration._

294

295

class ApiController @Inject()(

296

cc: ControllerComponents,

297

cached: Cached

298

) extends AbstractController(cc) {

299

300

// Complex caching configuration with builder methods

301

def getApiData(category: String) = cached

302

.empty(req => s"api:$category")

303

.includeStatus(200, 1.hour) // Cache successful responses for 1 hour

304

.includeStatus(404, 5.minutes) // Cache not found responses for 5 minutes

305

.default(30.seconds) { // Default cache for other responses

306

Action { implicit request =>

307

loadApiData(category) match {

308

case Some(data) => Ok(Json.toJson(data))

309

case None => NotFound("Category not found")

310

}

311

}

312

}

313

314

// Composing multiple caching strategies

315

def getDataWithComposition(id: String) = cached

316

.status(req => s"data:$id", 200, 2.hours)

317

.compose {

318

case header if header.headers.contains("X-Cache-Control") => 10.minutes

319

case header if header.status == 500 => Duration.Zero // Don't cache errors

320

} {

321

Action { implicit request =>

322

// Action implementation

323

Ok("data")

324

}

325

}

326

}

327

```

328

329

### @Cached Annotation (Java)

330

331

The @Cached annotation provides a simple way to cache action results in Java controllers.

332

333

```java { .api }

334

/**

335

* Mark an action to be cached on server side

336

*/

337

@With(CachedAction.class)

338

@Target({ElementType.TYPE, ElementType.METHOD})

339

@Retention(RetentionPolicy.RUNTIME)

340

public @interface Cached {

341

/**

342

* The cache key to store the result in

343

* @return the cache key

344

*/

345

String key();

346

347

/**

348

* The duration the action should be cached for. Defaults to 0.

349

* @return the duration in seconds (0 = no expiration)

350

*/

351

int duration() default 0;

352

}

353

```

354

355

### CachedAction Class (Java)

356

357

The CachedAction class implements the caching behavior for @Cached annotated methods.

358

359

```java { .api }

360

/**

361

* Cache another action

362

*/

363

public class CachedAction extends Action<Cached> {

364

@Inject

365

public CachedAction(AsyncCacheApi cacheApi);

366

367

/**

368

* Execute the cached action

369

* @param req HTTP request

370

* @return CompletionStage containing the cached or computed result

371

*/

372

public CompletionStage<Result> call(Request req);

373

}

374

```

375

376

**Usage Examples:**

377

378

```java

379

import play.cache.Cached;

380

import play.mvc.*;

381

import javax.inject.Inject;

382

import java.util.concurrent.CompletionStage;

383

384

public class ProductController extends Controller {

385

386

@Inject

387

private ProductService productService;

388

389

// Cache with fixed key and 1-hour expiration

390

@Cached(key = "products.all", duration = 3600)

391

public CompletionStage<Result> listProducts() {

392

return productService.getAllProducts()

393

.thenApply(products -> ok(Json.toJson(products)));

394

}

395

396

// Cache with dynamic key based on request parameter

397

@Cached(key = "product.{id}", duration = 1800) // 30 minutes

398

public CompletionStage<Result> getProduct(String id) {

399

return productService.getProduct(id)

400

.thenApply(product -> {

401

if (product.isPresent()) {

402

return ok(Json.toJson(product.get()));

403

} else {

404

return notFound("Product not found");

405

}

406

});

407

}

408

409

// Cache without expiration (permanent until manually evicted)

410

@Cached(key = "product.categories")

411

public CompletionStage<Result> getCategories() {

412

return productService.getCategories()

413

.thenApply(categories -> ok(Json.toJson(categories)));

414

}

415

416

// Cache at class level - applies to all methods

417

@Cached(key = "admin.stats", duration = 300) // 5 minutes

418

public static class AdminController extends Controller {

419

420

public CompletionStage<Result> getStats() {

421

return CompletableFuture.supplyAsync(() ->

422

ok(Json.toJson(generateStats())));

423

}

424

}

425

}

426

```

427

428

## HTTP Caching Features

429

430

### ETag and Client Cache Validation

431

432

The caching system automatically generates ETags and handles client cache validation:

433

434

```scala

435

// Automatic ETag generation based on content and expiration

436

// Client sends: If-None-Match: "etag-value"

437

// Server responds: 304 Not Modified if content unchanged

438

```

439

440

### Cache Headers

441

442

Cached responses automatically include appropriate HTTP headers:

443

444

```http

445

HTTP/1.1 200 OK

446

ETag: "a1b2c3d4e5f6"

447

Expires: Wed, 09 Sep 2025 21:30:00 GMT

448

Cache-Control: public, max-age=3600

449

```

450

451

### SerializableResult

452

453

Internal class for serializing Play Results for caching:

454

455

```scala { .api }

456

/**

457

* Wraps a Result to make it Serializable

458

* Only strict entities can be cached, streamed entities cannot be cached

459

*/

460

private[play] final class SerializableResult(constructorResult: Result) extends Externalizable {

461

def this()

462

def result: Result

463

override def readExternal(in: ObjectInput): Unit

464

override def writeExternal(out: ObjectOutput): Unit

465

}

466

467

private[play] object SerializableResult {

468

val encodingVersion: Byte = 2

469

}

470

```

471

472

## Caching Strategies

473

474

### Cache Key Strategies

475

476

```scala

477

// Static key

478

cached("homepage")

479

480

// Request-based key

481

cached(req => s"user:${req.session.get("userId").getOrElse("anonymous")}")

482

483

// Path-based key

484

cached(req => s"api:${req.path}")

485

486

// Parameter-based key

487

cached(req => s"product:${req.getQueryString("id").getOrElse("all")}")

488

```

489

490

### Duration Strategies

491

492

```scala

493

import scala.concurrent.duration._

494

495

// Fixed duration

496

cached(key, 1.hour)

497

498

// Conditional duration based on response

499

cached(key, {

500

case header if header.status == 200 => 1.hour

501

case header if header.status == 404 => 5.minutes

502

case _ => Duration.Zero // Don't cache other responses

503

})

504

505

// Status-specific caching

506

cached.status(key, 200, 2.hours) // Only cache 200 responses

507

```

508

509

### Builder Pattern Combinations

510

511

```scala

512

cached.empty(key)

513

.includeStatus(200, 1.hour) // Cache successful responses

514

.includeStatus(404, 5.minutes) // Cache not found briefly

515

.compose { // Add conditional logic

516

case header if header.headers.contains("X-No-Cache") => Duration.Zero

517

case _ => 30.seconds // Default fallback

518

}

519

```

520

521

## Error Handling

522

523

### UnsupportedOperationException

524

525

Some cache implementations may not support `removeAll()`:

526

527

```scala

528

cache.removeAll().recover {

529

case _: UnsupportedOperationException =>

530

// Handle unsupported clear operation

531

Done

532

}

533

```

534

535

### Serialization Errors

536

537

Only strict HTTP entities can be cached:

538

539

```scala

540

// This will work - strict entity

541

Ok("Hello World")

542

543

// This will fail - streamed entity

544

Ok.chunked(source) // Throws IllegalStateException during caching

545

```

546

547

### Cache Miss Handling

548

549

```scala

550

// Handle cache misses gracefully

551

cached.getOrElseUpdate("key", 1.hour) {

552

computeExpensiveValue().recover {

553

case ex: Exception =>

554

logger.error("Failed to compute value", ex)

555

defaultValue

556

}

557

}

558

```