howdy/conditional
Conditional requests: answer 304 Not Modified when the client already
has the current version of a response, and 412 Precondition Failed
when a write was based on a version that is no longer current.
Describe the version of what a handler is about to send with
validators, then let when decide whether to send it:
import howdy/conditional
fn show(ctx: Context) {
use id <- param.int(ctx, "id")
case notes.find(repo, id) {
Error(error) -> service.error_response(ctx, error)
Ok(note) -> {
use <- conditional.when(
ctx,
conditional.etag(["note", int.to_string(id), int.to_string(note.version)])
|> conditional.last_modified(note.updated_at),
)
controller.json(ctx, note_to_json(note))
}
}
}
A browser that saw this note before sends its etag back in
if-none-match. While the note is unchanged it gets an empty 304 and
the rest of the handler never runs: the JSON is not built and the body
is not sent. The database read still happens, since that is what tells
the version; cache it with howdy_cache if it is the expensive part.
The response is cache-control: private, no-cache unless you say
otherwise with cache_control. Browsers keep it, but ask every time
whether it is still current, which is what makes the 304s happen.
Shared caches never store it. A page that differs per user must put
the user in the etag too, so that one user’s version is never confirmed
to another.
Static files from howdy/static use the same rules.
Types
Who may store a response, and for how long it counts as current.
Start from private, public or no_store.
pub opaque type CacheControl
What identifies the version of a response: an entity tag, a
modification time, or both, and the cache-control to send with it.
pub opaque type Validators
Values
pub fn auto_etag(
ctx: context.Context(Nil),
next: fn(context.Context(Nil)) -> response.Response(
content.Content,
),
) -> response.Response(content.Content)
Middleware that gives every 200 text or bytes response to a GET or
HEAD a weak etag hashed from its body, and answers 304 when the
client already has that body.
howdy.new() |> howdy.middleware(conditional.auto_etag)
The handler still runs in full, so this saves bandwidth, not work: use
when where building the response is the cost. Responses that already
have an etag, say cache-control: no-store, set a cookie, or stream
their body (files, server-sent events, chunked bodies) are left alone.
pub fn cache_control(
validators: Validators,
policy: CacheControl,
) -> Validators
Send policy as the cache-control of the response and of its 304,
instead of private, no-cache.
pub fn etag(parts: List(String)) -> Validators
A weak entity tag hashed from parts. List whatever changes when the
response would change: a record’s id and version or update time, the
signed-in user for a per-user page, the language for a translated one.
Weak means the tag vouches for the data, not for the exact bytes, which
is right for a response rendered from data: the same note still has the
same tag after a gzip middleware compresses it. Use strong_etag to
protect writes with if-match.
pub fn http_date(seconds: Int) -> String
Seconds since the Unix epoch as an HTTP date, such as
Sun, 06 Nov 1994 08:49:37 GMT.
pub fn immutable(policy: CacheControl) -> CacheControl
The response at this URL never changes, so browsers need not ask even
when the user reloads. For fingerprinted assets, with a long max_age.
pub fn is_fresh(
ctx: context.Context(guarded),
validators: Validators,
) -> Bool
Whether the client’s copy is current: what when uses to decide on a
304, for handlers that build their own response. Always False for
methods other than GET and HEAD.
pub fn last_modified(
validators: Validators,
seconds: Int,
) -> Validators
Add a modification time to validators that have an etag. Clients that
send if-none-match are judged by the etag alone; older clients and
some proxies use the time.
pub fn max_age(
policy: CacheControl,
seconds: Int,
) -> CacheControl
Use a stored copy for seconds without asking, instead of asking every
time.
pub fn modified_at(seconds: Int) -> Validators
Validators with only a modification time, in seconds since the Unix epoch. HTTP dates carry whole seconds, so two changes within the same second look like one; prefer an etag when that matters.
pub fn must_revalidate(policy: CacheControl) -> CacheControl
Never use the stored copy once max_age has run out, even when the
server cannot be reached.
pub fn no_store() -> CacheControl
Nobody may store the response: no-store. For secrets, and anything
that must never be shown from a cache, such as a page after sign-out.
The other settings are ignored.
pub fn parse_http_date(date: String) -> Result(Int, Nil)
An HTTP date back to seconds since the Unix epoch. Accepts the
preferred format and the two obsolete ones HTTP still allows,
Sunday, 06-Nov-94 08:49:37 GMT and Sun Nov 6 08:49:37 1994.
pub fn private() -> CacheControl
Only the user’s browser may store the response, and it asks whether
its copy is current before each use: private, no-cache. The default
for when, and right for anything that differs per user.
pub fn public() -> CacheControl
Shared caches such as a CDN may store the response too: public, no-cache. Only for responses that are the same for everyone.
pub fn quoted_etag(tag: String) -> Validators
An entity tag exactly as given, already quoted, such as "\"v42\"" or
"W/\"v42\"". For a version the application already stores, such as a
content hash; etag and strong_etag hash parts for you.
pub fn set_cache_control(
response: response.Response(content.Content),
policy: CacheControl,
) -> response.Response(content.Content)
Set the cache-control header of a response, for handlers that do not
go through when.
pub fn shared_max_age(
policy: CacheControl,
seconds: Int,
) -> CacheControl
A different max_age for shared caches: s-maxage. Lets a CDN keep a
page for minutes while browsers ask every time.
pub fn stale_while_revalidate(
policy: CacheControl,
seconds: Int,
) -> CacheControl
After max_age runs out, keep serving the stored copy for up to
seconds more while fetching a new one in the background.
pub fn strong_etag(parts: List(String)) -> Validators
A strong entity tag hashed from parts: a promise that responses with
the same tag are byte-for-byte the same. if-match compares only strong
tags, so a handler that guards writes with when needs one.
pub fn vary(
response: response.Response(content.Content),
headers: List(String),
) -> response.Response(content.Content)
Add header names to the response’s vary, keeping any already there.
A cache stores one copy per combination of these request headers, so
list every header that changes the response: accept for content
negotiation, accept-language for translations.
pub fn when(
ctx: context.Context(guarded),
validators: Validators,
respond: fn() -> response.Response(content.Content),
) -> response.Response(content.Content)
Answer from the validators when the request’s conditions allow it,
otherwise run respond and stamp the validators on its response.
GETandHEADget a304with no body whenif-none-matchnames the current etag, or, withoutif-none-match, whenif-modified-sinceis no earlier than the modification time.- Any method gets a
412whenif-matchnames no current strong etag, or, withoutif-match, when the resource changed afterif-unmodified-since. Other methods also get a412whenif-none-matchmatches. This is how aPUTis refused when the client edited an old copy; describe the version as it is before the write.
Only a 2xx response to GET or HEAD gets the validators. A
response to a write describes the new version, which these validators
do not, and an error is not a version at all. Headers the handler set
itself are kept.
pub fn without_cache_control(
validators: Validators,
) -> Validators
Leave cache-control alone: the response keeps whatever the handler
set, and a 304 carries none.