Cookbook / Requests and responses
HTTP caching and ETags
Answer 304 Not Modified, set Cache-Control, and skip the expensive work when the client already has the data.
Caching headers save bandwidth and, done in the handler, save the work too. The ConditionalGet middleware gives every successful GET an ETag and answers If-None-Match with 304. For expensive endpoints, check freshness yourself, with a cheap version number, before doing the work.
use axeon::cache::{CacheControl, ConditionalGet, ETag};
use axeon::{Json, Response, Server, ServerError};
use axeon::json;
use std::time::Duration;
fn main() {
let mut app = Server::new();
// Weak ETags from response bodies, plus a default Cache-Control.
app.middleware(ConditionalGet::new().cache_control(CacheControl::revalidate()));
// Cheap and automatic: the handler runs, but a matching request gets 304.
app.get("/v1/categories", |_req| async { Json(vec!["lighting", "furniture"]) });
// Skip the work: compare a version number before building the response.
app.get("/v1/products/:id", |req| async move {
let id: i64 = req.param("id")?;
let version = 42; // e.g. SELECT version FROM products WHERE id = $1
let etag = ETag::strong(format!("p{}-v{}", id, version));
if req.is_fresh(Some(&etag), None) {
return Ok(Response::not_modified(&etag));
}
let body = json!({ "id": id, "version": version }); // the expensive part
Ok::<_, ServerError>(
Response::json(&body)
.with_etag(&etag)
.with_cache_control(CacheControl::new().private().max_age(Duration::from_secs(60))),
)
});
app.listen("127.0.0.1:3000").expect("server failed to start");
}
curl -si http://127.0.0.1:3000/v1/categories # ETag: W/"..."
curl -si http://127.0.0.1:3000/v1/categories -H 'if-none-match: W/"<value from above>"' # 304, no body
curl -si http://127.0.0.1:3000/v1/products/7 # ETag: "p7-v42"
curl -si http://127.0.0.1:3000/v1/products/7 -H 'if-none-match: "p7-v42"' # 304
Notes#
- Use
CacheControl::revalidate()(no-cache) for data that must never be stale: caches keep a copy but check it with the ETag before every use. Use.public().max_age(..)for data every user may share, and.private()for personal data. UseCacheControl::never()(no-store) for sensitive responses. - A strong ETag promises identical bytes; a weak one only equivalent content. Body hashes from
ConditionalGetare weak, because compression may change the bytes. - Add
ConditionalGetafterCompressionso tags describe the uncompressed body. If-None-Matchwins overIf-Modified-Since, as the spec says. For the date form, set.with_last_modified(time)and pass the time toreq.is_fresh.- Handlers that set their own
ETagorCache-Controlkeep them: the middleware only fills in what is missing. Streamed responses are left alone.