Files
What you'll learn
- Control how long a response may be reused with Cache-Control, and tell no-cache from no-store
- Tag a representation with an ETag and answer If-None-Match with 304 Not Modified
- Prevent lost updates with If-Match, answering 412 for a stale write and 428 for one that names no version
Caching and conditional requests
The fastest request is the one that is never sent. The next fastest is answered with "you already have it", in a few bytes and no body. HTTP builds both into the protocol, through headers that caches along the way (the browser's own, a company proxy, a CDN) all understand. The same headers solve a problem that has nothing to do with speed: two people editing the same record, where the second save silently erases the first. This lesson is about both.
Freshness: may this be reused?
Cache-Control on a response says how a cache may treat it:
| Directive | Meaning |
|---|---|
max-age=3600 | fresh for an hour: reuse it without asking the server |
no-cache | store it, but check with the server before every reuse |
no-store | do not store it at all (secrets, one-time answers) |
private | only the user's own browser may store it, not a shared cache |
public | any cache may store it, even for a request that carried credentials |
no-cache is the most misread header in HTTP: it does not mean "do not cache". It means "cache, and revalidate", which is exactly right for data that changes whenever someone edits it. A list of breeds that never changes can carry max-age; a cat that anyone may rename should be revalidated.
Validation: is my copy still current?
To revalidate, the client needs a way to name the version it has. The server gives each representation an ETag, an opaque quoted string that changes whenever the representation does:
HTTP/1.1 200 OK
ETag: "3-7"
Cache-Control: no-cache
The tag can be anything that changes with the content: a version number, a hash of the body, a last-updated timestamp. Here it is the cat's id and a version counter. Next time, the client sends the tag back, and the server compares:
GET /cats/3
If-None-Match: "3-7"
HTTP/1.1 304 Not Modified
ETag: "3-7"
304 Not Modified has no body: the client uses the copy it has. It still carries the ETag and Cache-Control headers, so the cache knows the copy is good for another round. If-None-Match may list several tags, "3-6", "3-7", or be *; any match counts. (A tag written W/"3-7" is a weak one, meaning "equivalent", not "byte for byte identical"; this lesson's are strong.)
Conditional writes: the lost update
Two clients read cat 3 at version 7. The first changes its name and saves. The second, still holding version 7, changes the age and saves too, and its PUT replaces the whole cat, erasing the first client's name. Nobody gets an error. This is the lost update, and the fix is to make the write conditional on the version it was based on:
PUT /cats/3
If-Match: "3-7"
The server compares If-Match with the current tag. If they differ, someone else wrote first, and the answer is 412 Precondition Failed: fetch it again, look at what changed, and decide. A server that insists on this answers a write with no If-Match at all with 428 Precondition Required. Here is the shape of the check on owners:
const ifMatch = c.req.header('if-match');
if (ifMatch === undefined) return c.json({ error: 'Say which version you are replacing' }, 428);
if (ifMatch !== currentTagOf(owner)) return c.json({ error: 'Someone changed this owner first' }, 412);
A successful write creates a new version, so it answers with the new tag, which is what the client sends next time.
Your task
GET /breedscarriescache-control: max-age=3600.GET /cats/:idcarries anETagof"<id>-<version>", quotes included, andcache-control: no-cache; whenIf-None-Matchnames that tag (it may list several, or be*), it answers304with no body and the same two headers.PUT /cats/:idanswers428with{ "error": "Send If-Match with the ETag you last saw" }when there is noIf-Match, and412with{ "error": "The cat has changed since you read it" }when it does not name the current tag.- A
PUTthat goes through increases the version by one and answers the cat with its newETag.
When it fails
- The ETag check fails with
1-1against"1-1": an ETag is a quoted string, and the quotes are part of the header's value. - "Revalidate with a list" answers 200: the header was compared whole. Split on commas and trim each tag.
- "Revalidate" is missing its ETag: the header was set after the 304 was returned. A 304 carries the same validators as a 200.
- "The lost update, prevented" is accepted: the version was not bumped, so the old tag still matches.
Remember
max-agesays how long a response is fresh;no-cachemeans revalidate,no-storemeans never keep it.- An ETag names a version of a representation and must change when it does.
If-None-Matchwith the current tag earns a304with no body.If-Matchmakes a write conditional:412when the tag is stale,428when there is none.
Stuck? Show a hint
An ETag is a quoted string, so the header's value includes the quotes: `"1-1"`. If-None-Match and If-Match may list several tags separated by commas, or be *. c.body(null, 304) answers with no body; set the ETag and Cache-Control headers before it, since a 304 carries them too. Bump the version before computing the tag the PUT answers with.
Press Run tests to start the app. Its log appears here.Graded endpoints
max-age=3600: any cache may reuse this answer for an hour without asking again
The cat, its version as an ETag, and no-cache: keep it, but check before reusing it
The client sends back the tag it has; it is still current, so 304 and no body
If-None-Match may list several tags; one of them matching is enough
A tag only matches the resource it came from: Luna is sent in full
428 Precondition Required: a write must say which version it is replacing
412 Precondition Failed: the version the client read is not the current one
Both refused writes left Tom and his version alone
The client read version 1 and version 1 is current: the write goes through and the tag moves on
A second client also read version 1 and now tries to write: its change would erase the first, so 412
The client's cached tag is out of date, so the new version comes in full
And the new tag revalidates
404, unchanged
There is nothing to compare a tag with