Response Caching
This guide covers HTTP response caching in Armature, including Cache-Control headers, in-memory caching, and cache invalidation.
Table of Contents
- Overview
- Features
- Cache-Control Headers
- In-Memory Response Cache
- Cache Keys
- Request Extensions
- Response Extensions
- Best Practices
- Examples
- Summary
Overview
Armature provides comprehensive HTTP response caching support:
- Cache-Control headers - Parse and generate Cache-Control directives
- In-memory cache - Store and retrieve responses with TTL
- Cache keys - Generate unique keys with Vary header support
- Extensions - Convenient methods on HttpRequest and HttpResponse
Features
- โ Full Cache-Control header parsing and generation
- โ All standard cache directives supported
- โ In-memory response cache with configurable TTL
- โ Vary header support for content negotiation
- โ Automatic cache eviction and stale entry purging
- โ Cache statistics
- โ Preset configurations for common scenarios
Cache-Control Headers
Parsing Cache-Control
use armature_framework::prelude::*;
let cc = CacheControl::parse("public, max-age=3600, must-revalidate");
assert!(cc.is_public());
assert_eq!(cc.get_max_age(), Some(3600));
assert!(cc.is_must_revalidate());
assert!(cc.is_cacheable());
Building Cache-Control
use armature_framework::prelude::*;
use std::time::Duration;
let cc = CacheControl::new()
.public()
.max_age(Duration::from_secs(3600))
.must_revalidate();
assert_eq!(cc.to_header_value(), "public, max-age=3600, must-revalidate");
Preset Configurations
use armature_framework::prelude::*;
use std::time::Duration;
// Never cache (no-store, no-cache)
let never = CacheControl::never();
// Public cache with max-age
let public = CacheControl::public_max_age(Duration::from_secs(3600));
// Private cache with max-age
let private = CacheControl::private_max_age(Duration::from_secs(300));
// Immutable assets (versioned files)
let immutable = CacheControl::immutable_asset(Duration::from_secs(31536000)); // 1 year
// Must revalidate after TTL
let revalidate = CacheControl::revalidate(Duration::from_secs(60));
Cache Directives
| Directive | Description |
|---|---|
public |
Response can be cached by any cache |
private |
Response is for single user, not shared caches |
no-store |
Response must not be stored |
no-cache |
Response can be stored but must be revalidated |
max-age=N |
Response is fresh for N seconds |
s-maxage=N |
Shared cache max-age (overrides max-age) |
must-revalidate |
Stale responses must be revalidated |
proxy-revalidate |
Shared caches must revalidate |
immutable |
Response will never change |
no-transform |
Response must not be transformed |
In-Memory Response Cache
Basic Usage
use armature_framework::prelude::*;
use std::time::Duration;
#[controller("/api")]
struct ApiController {
cache: ResponseCache,
}
#[controller]
impl ApiController {
#[get("/data")]
async fn get_data(&self, request: HttpRequest) -> Result<HttpResponse, Error> {
// Check cache first
if let Some(cached) = self.cache.get(&request).await {
return Ok(cached);
}
// Generate response
let data = expensive_operation();
let response = HttpResponse::ok()
.cache_public(Duration::from_secs(300))
.with_json(&data)?;
// Store in cache
self.cache.store(&request, &response).await;
Ok(response)
}
}
Custom Configuration
use armature_framework::prelude::*;
use std::time::Duration;
let cache = ResponseCache::with_config(ResponseCacheConfig {
max_entries: 10000,
default_ttl: Duration::from_secs(600),
max_body_size: 5 * 1024 * 1024, // 5MB
cacheable_status_codes: vec![200, 203, 204, 301],
cacheable_methods: vec!["GET".into(), "HEAD".into()],
});
Cache Operations
use armature_framework::prelude::*;
async fn cache_operations(cache: &ResponseCache, request: &HttpRequest, response: &HttpResponse) {
// Store with default TTL
cache.store(request, response).await;
// Store with custom TTL
cache.store_with_ttl(request, response, Duration::from_secs(60)).await;
// Get cached response
if let Some(cached) = cache.get(request).await {
// Use cached response
}
// Invalidate specific entry
cache.invalidate(request).await;
// Invalidate by path prefix
cache.invalidate_prefix("/api/users").await;
// Clear all entries
cache.clear().await;
// Remove stale entries
cache.purge_stale().await;
// Get statistics
let stats = cache.stats().await;
println!("Entries: {}/{}", stats.total_entries, stats.max_entries);
println!("Fresh: {}, Stale: {}", stats.fresh_entries, stats.stale_entries);
}
Cache Keys
Automatic Key Generation
use armature_framework::prelude::*;
let request = HttpRequest::new("GET".into(), "/api/users".into());
let key = request.cache_key();
// Key includes: method, path, sorted query params
println!("Key: {}", key); // GET:/api/users
Keys with Vary Headers
use armature_framework::prelude::*;
let mut request = HttpRequest::new("GET".into(), "/api/users".into());
request.headers.insert("Accept".into(), "application/json".into());
request.headers.insert("Accept-Language".into(), "en-US".into());
// Include Vary headers in key
let key = request.cache_key_with_vary(&["Accept", "Accept-Language"]);
// Different Accept values = different cache entries
Cache with Vary Support
use armature_framework::prelude::*;
async fn get_with_vary(cache: &ResponseCache, request: &HttpRequest) -> Option<HttpResponse> {
// Get with specific Vary headers
cache.get_with_vary(request, &["Accept", "Accept-Encoding"]).await
}
Request Extensions
use armature_framework::prelude::*;
fn analyze_request(request: &HttpRequest) {
// Get Cache-Control from request
if let Some(cc) = request.cache_control() {
if cc.is_no_cache() {
// Client wants fresh response
}
}
// Check if request allows cached responses
if request.allows_cached() {
// Can serve from cache
}
// Get max-stale tolerance
if let Some(max_stale) = request.max_stale() {
// Client accepts stale responses up to max_stale seconds
}
// Generate cache key
let key = request.cache_key();
}
Response Extensions
use armature_framework::prelude::*;
use std::time::Duration;
fn build_cached_response() -> HttpResponse {
HttpResponse::ok()
// Set Cache-Control with builder
.with_cache_control(
CacheControl::new()
.public()
.max_age(Duration::from_secs(3600))
)
// Or use convenience methods
.cache_public(Duration::from_secs(3600))
// Set Vary header
.with_vary(&["Accept", "Accept-Encoding"])
}
fn cache_control_shortcuts() {
// No caching
let _ = HttpResponse::ok().no_cache();
// Public cache
let _ = HttpResponse::ok().cache_public(Duration::from_secs(3600));
// Private cache
let _ = HttpResponse::ok().cache_private(Duration::from_secs(300));
// Immutable assets
let _ = HttpResponse::ok().cache_immutable(Duration::from_secs(31536000));
}
fn check_response_cacheability(response: &HttpResponse) {
if let Some(cc) = response.get_cache_control() {
println!("Cacheable: {}", cc.is_cacheable());
println!("Max-Age: {:?}", cc.get_max_age());
}
if response.is_cacheable() {
// Response can be cached
}
}
Best Practices
1. Choose the Right Cache-Control
use armature_framework::prelude::*;
use std::time::Duration;
// Static assets - cache for a long time with immutable
fn static_asset_response() -> HttpResponse {
HttpResponse::ok()
.cache_immutable(Duration::from_secs(31536000)) // 1 year
}
// API data - short cache, must revalidate
fn api_response() -> HttpResponse {
HttpResponse::ok()
.with_cache_control(
CacheControl::new()
.public()
.max_age(Duration::from_secs(60))
.must_revalidate()
)
}
// User-specific data - private cache
fn user_data_response() -> HttpResponse {
HttpResponse::ok()
.cache_private(Duration::from_secs(300))
}
// Sensitive data - never cache
fn sensitive_response() -> HttpResponse {
HttpResponse::ok().no_cache()
}
2. Use Vary Headers Correctly
use armature_framework::prelude::*;
fn content_negotiated_response() -> HttpResponse {
HttpResponse::ok()
.cache_public(Duration::from_secs(3600))
.with_vary(&["Accept", "Accept-Encoding", "Accept-Language"])
}
3. Invalidate on Mutations
use armature_framework::prelude::*;
#[put("/users/:id")]
async fn update_user(
&self,
request: HttpRequest,
#[param("id")] id: u64,
) -> Result<HttpResponse, Error> {
// Update user
let user = update_user_in_db(id)?;
// Invalidate cache
self.cache.invalidate_prefix(&format!("/users/{}", id)).await;
HttpResponse::ok().with_json(&user)
}
4. Configure Cache Appropriately
use armature_framework::prelude::*;
use std::time::Duration;
// Small API responses
let api_cache = ResponseCache::with_config(
ResponseCacheConfig::new()
.max_entries(10000)
.default_ttl(Duration::from_secs(60))
.max_body_size(100 * 1024) // 100KB
);
// Large file responses
let file_cache = ResponseCache::with_config(
ResponseCacheConfig::new()
.max_entries(100)
.default_ttl(Duration::from_secs(3600))
.max_body_size(10 * 1024 * 1024) // 10MB
);
Examples
Complete Cached API Endpoint
use armature_framework::prelude::*;
use std::time::Duration;
use std::sync::Arc;
#[controller("/api")]
struct ProductController {
cache: Arc<ResponseCache>,
}
#[controller]
impl ProductController {
#[get("/products")]
async fn list_products(&self, request: HttpRequest) -> Result<HttpResponse, Error> {
// Check if client allows cached response
if !request.allows_cached() {
return self.fetch_fresh_products().await;
}
// Try cache
if let Some(cached) = self.cache.get_with_vary(
&request,
&["Accept", "Accept-Language"]
).await {
return Ok(cached);
}
// Fetch fresh data
let products = load_products_from_db()?;
// Build cacheable response
let response = HttpResponse::ok()
.with_json(&products)?
.cache_public(Duration::from_secs(300))
.with_vary(&["Accept", "Accept-Language"]);
// Store in cache
self.cache.store(&request, &response).await;
Ok(response)
}
#[post("/products")]
async fn create_product(
&self,
#[body] product: CreateProduct,
) -> Result<HttpResponse, Error> {
let created = save_product_to_db(&product)?;
// Invalidate product list cache
self.cache.invalidate_prefix("/api/products").await;
HttpResponse::created()
.no_cache()
.with_json(&created)
}
async fn fetch_fresh_products(&self) -> Result<HttpResponse, Error> {
let products = load_products_from_db()?;
HttpResponse::ok()
.no_cache()
.with_json(&products)
}
}
Cache Middleware Pattern
use armature_framework::prelude::*;
use std::sync::Arc;
struct CachingMiddleware {
cache: Arc<ResponseCache>,
ttl: Duration,
}
impl CachingMiddleware {
async fn handle(
&self,
request: HttpRequest,
next: impl FnOnce(HttpRequest) -> Result<HttpResponse, Error>,
) -> Result<HttpResponse, Error> {
// Only cache GET requests
if request.method != "GET" {
return next(request);
}
// Check cache
if let Some(cached) = self.cache.get(&request).await {
return Ok(cached);
}
// Call handler
let response = next(request.clone())?;
// Cache if cacheable
if response.is_cacheable() {
self.cache.store_with_ttl(&request, &response, self.ttl).await;
}
Ok(response)
}
}
Versioned Static Assets
use armature_framework::prelude::*;
use std::time::Duration;
#[controller("/assets")]
struct AssetController;
#[controller]
impl AssetController {
#[get("/:version/:filename")]
async fn get_asset(
&self,
#[param("version")] version: String,
#[param("filename")] filename: String,
) -> Result<HttpResponse, Error> {
let content = load_asset(&filename)?;
let content_type = guess_content_type(&filename);
Ok(HttpResponse::ok()
.with_header("Content-Type".into(), content_type)
.with_body(content)
// Version in URL = can cache forever
.cache_immutable(Duration::from_secs(31536000)))
}
}
Common Pitfalls
โ Caching user-specific data with public
โ Forgetting Vary headers with content negotiation
โ Not invalidating cache after mutations
โ Caching responses with no-store directive
โ Use
privatefor user-specific dataโ Include all negotiated headers in Vary
โ Invalidate affected cache entries on write operations
โ Check
is_cacheable()before storing
Summary
| Component | Purpose |
|---|---|
CacheControl |
Parse/build Cache-Control headers |
ResponseCache |
In-memory response caching |
CacheKey |
Generate unique cache keys |
ResponseCacheConfig |
Configure cache behavior |
Key Points:
- Cache-Control - Use appropriate directives for your content type
- Vary headers - Include all headers that affect response content
- Invalidation - Invalidate cache on mutations
- TTL - Set reasonable expiration times
- Private data - Use
privateorno-storefor sensitive content
Caching QUERY Requests
The IETF QUERY method (#[query(...)]) is a safe, idempotent read whose parameters
travel in the request body. Because the body is what distinguishes one query from another,
the response cache includes a hash of the request body in the cache key for QUERY
requests (it does not for other methods). QUERY is a cacheable method by default, so two
QUERY requests to the same path with different bodies are cached and served as distinct
entries, exactly as two GETs with different query strings would be.