Cache
lib/cache.march — Cache
HTTP cache headers, response caching, and fragment caching for Bastion. All storage is backed by Vault (in-memory, per-node, TTL-aware).
── HTTP ETags ────────────────────────────────────────────────────────────────
Use Cache.etag_from/2 in handlers after the response body is known:
fn show(conn) do
let html = render_page()
conn
|> Cache.cache_control("public, max-age=600")
|> Cache.etag_from(html) -- halts with 304 if client cache is fresh
|> HttpServer.html(200, html)
end── Response caching ──────────────────────────────────────────────────────────
Cache the full response for expensive routes:
fn index(conn) do
Cache.cached(conn, "posts:index", 60, fn conn2 ->
let posts = Blog.list_posts()
HttpServer.html(conn2, 200, render_posts(posts))
end)
end── Fragment caching ──────────────────────────────────────────────────────────
Cache expensive rendered fragments:
fn sidebar_html() do
Cache.fragment("sidebar", 300, fn () -> render_sidebar() end)
endFunctions
Set the Cache-Control response header.
Common values:
"public, max-age=3600" — cache for 1 hour, shareable
"private, max-age=0" — client-only, no store
"no-store" — never cache (e.g. sensitive data)
"public, max-age=31536000" — 1 year (use with content-hash URLs)
conn |> Cache.cache_control("public, max-age=600")Set an explicit ETag value without computing it from content. Use this when you have a known version identifier (e.g. a record's updated_at).
let tag = "v" ++ int_to_string(post.version)
conn |> Cache.etag(tag)Set an ETag from known content and short-circuit to 304 if the client already has a fresh copy.
- Computes SHA-256 of the content string and wraps it in quotes: "abc123".
- Sets the ETag response header.
- Reads If-None-Match from the request.
- If the ETags match, halts the conn with a 304 Not Modified response
(empty body, no content-type). Subsequent calls like HttpServer.html are
no-ops when the conn is halted.
Call this AFTER building the response body but BEFORE sending it:
let html = render_page(data)
conn
|> Cache.etag_from(html)
|> HttpServer.html(200, html)Cache a rendered string fragment. On hit, returns the cached string. On miss, calls generator(), stores the result for ttl_secs, and returns it.
key — cache key, e.g. "sidebar" or "user:42:avatar"
ttl_secs — how long to keep the fragment
generator — fn() -> String that renders the fragment
let html = Cache.fragment("global_stats", 300, fn () -> render_stats() end)Invalidate a cached response by key.
Cache.invalidate("posts:index")Invalidate a cached fragment by key.
Cache.invalidate_fragment("global_stats")Invalidate all cached responses whose keys start with prefix. Useful for invalidating a group of related routes.
Cache.invalidate_prefix("posts:")