Caching and Conditional Requests · practice
Tag Every Representation
make_etag can name any body, and no reply uses it yet. Run the helper’s etag scenario to see what a client receives today:
python pl_http.py etag
It sends GET /books/1, then a PUT /books/1 with the sample writer token and a new title, then GET /books/1 again, and at the end it shows the etag header of the first and the last reply:
--- the two etag lines ---
before the PUT: None
after the PUT: None
Neither reply carries a tag, and neither says how long it may be reused. This lesson adds both to every reply that should have them: an etag and cache-control: max-age=60.
Which replies get a tag
A tag names a body that a client may keep and ask about later. That is a successful read: a 200 to a GET or a HEAD on /books or on /books/{id}. Nothing else gets one. An error body such as {"error": "no book has that id"} is not something a client keeps. The reply to a write reports what just happened, and the next read is the one worth keeping. Redirects, /login and /me stay as they are. SERVICE_CONTRACT.md has a new section, “Caching”, that says the same in a table.
Each /books, /books?limit=1 and /books/2 are three different representations with three different tags, even though they share books. A client asking about /books?limit=1 sends the tag it got from that URL.
add_validators(response) adds both headers to a response
set
response["headers"]["etag"]tomake_etag(response["body"]);set
response["headers"]["cache-control"]tomax-age=60, built from the constantMAX_AGEat the top ofreading_server.py, for example with thef"max-age={MAX_AGE}".
handle calls it in exactly two places, the two branches that build a 200 for a read. The response = add_validators(json_response(200, {"books": books})), and the branch for one book does the same with json_response(200, state["books"][book_id]).
HEAD gets GET’s tag
Chapter 4 taught your server to answer HEAD with exactly the headers GET would send and no body, and the last lines of handle do that by emptying the body when the HEAD. The order of those two steps now matters. If the tag were computed after the body was emptied, a HEAD reply would carry the tag of an empty body, "e3b0c442...b855", a name for bytes that GET never sends. A client that checks a resource with HEAD and compares the tag with the one it holds would always see a difference.
Calling add_validators inside the read branches puts it before the HEAD rule, because the rule runs last, after every branch. GET and HEAD share those branches, so both get the tag of the full body, and only then does HEAD lose the body.
What etag does HEAD /books carry?
A write changes the tag by itself
Look at what the tag is computed from: the body, at the moment a read builds it. Nothing stores the tag, so nothing has to remember to change it. A PUT that renames book 2 changes the bytes of GET /books/2, so the next read of it gets a new tag. The same PUT changes the list, so /books gets a new tag too, as does every ?limit= page that includes book 2. Book 1’s bytes did not change, and neither does its tag.
A tag built any other way has to be kept in step by hand, and sooner or later it is not. A tag made from the book’s id stays the same after a PUT, and a client would keep showing the old title while the server says “unchanged”.
After PUT /books/2, which tags change?
Hashing every body on every read costs some work. A busy real service often computes a tag once, when the data changes, and stores it next to the data, which is faster and one more thing to keep in step. A tag computed on every read is never out of date, and this server keeps it that way.
Your task
In reading_server.py, write add_validators(response), and call it in the two read branches of handle, as above. Errors, writes and every other route stay untouched. Then run the scenario again:
python pl_http.py etag
Under each 200 for a read the helper now prints the two headers, and the tags of the two reads differ, because the PUT changed book 1’s bytes between them:
--- the two etag lines ---
before the PUT: '"ecfe550f34c7f26be888feb41dbb0bdc5dfff7e77c57a8e049eb0c721b08049b"'
after the PUT: '"929daebfa038d3a896d78c3e8b93fdac7fece4faed0d92572e8a88a4733bb5f4"'
The reply to the PUT itself carries neither header. Then press Check my work.
Every read is now tagged, and clients still download every body in full, because none of them sends a tag back. The next lesson teaches your server to answer a client that does: a request that says “I hold this tag” gets a 304 with no body when the tag is still current.
Task
Write add_validators(response) in reading_server.py and call it in handle.
add_validatorssetsresponse["headers"]["etag"]tomake_etag(response["body"])andresponse["headers"]["cache-control"]tomax-age=60(fromMAX_AGE), then returnsresponse.In
handle, call it on the 200 in the branch thatbooks and on the 200 in the branch that returns one book: response = add_validators(json_response(200, ...)). Those branches run before theHEADrule at the end ofhandle, so aHEADreply carries the tag of the bodyGETsends.No other reply gets either header: errors, redirects, the replies to
POST,PUTandDELETE,/login,/logoutand/mestay as they are.
Run python pl_http.py etag in the PUT.
Then press Check my work. It calls add_validators directly, then handle with a state of books, ids and tokens you have not seen. It checks that GET on /books, /books?limit=1 and /books/{id} carries an etag equal to the quoted SHA-256 digest of the body it sent and a cache-control that includes max-age=60; that HEAD carries the same tag, the same cache-control and content-length, and no body; that after a PUT the tags of that book and of /books change while another book’s tag does not; that a POST and a DELETE change the tag of /books; and that 404, 405, 400, 308, 401, 403, 201, 204, the PUT reply and /me carry no etag. Nothing from your terminal is graded.