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
```