0%

Caching en voorwaardelijke verzoeken · oefening

Beantwoord If-None-Match met 304

Elke leesactie draagt nu een tag, en een client kan die tag terugsturen. Je server luistert nog niet. Voer het scenario conditional van het hulpprogramma uit:

python pl_http.py conditional

Het stuurt vier verzoeken. Het eerste is een gewone GET /books. Het tweede stuurt de tag die het eerste antwoord droeg terug in een nieuwe header:

client -> server  b'GET /books HTTP/1.1\r\nHost: books.test\r\nIf-None-Match: "ba811c01801a2c40d5e507c3c439e7e966380095a4001999178055355cebbec1"\r\n\r\n'  (124 bytes)

Het derde is een HEAD met W/ voor dezelfde tag, en het vierde stuurt If-None-Match: "stale". Vandaag krijgen alle vier 200 OK met de volledige van 132 bytes, of de lengte ervan bij HEAD, hoewel het tweede verzoek die lijst al byte voor byte heeft.

De vraag in een header

If-None-Match maakt van een GET een vraag: “stuur de body alleen als zijn tag geen van deze is”. De client zet er de tag in van de kopie die hij heeft. De server vergelijkt die met de tag van wat hij nu zou sturen en antwoordt op een van twee manieren:

The client holds a body and its tag T. It sends:

  GET /books
  If-None-Match: T

The server's current tag is T:
  304 Not Modified: no body, keep your copy
The server's current tag is not T:
  200 OK: the new body, with its new tag

Een GET met If-None-Match heet een voorwaardelijke GET. De 304 ervan is kort, een statusregel en een paar headers, omdat de client de body al heeft.

Overeenkomen

De van de header is een lijst, geen enkele tag (RFC 9110, sectie 13.1.2). Een client die meerdere versies van een resource heeft, kan ze allemaal noemen, gescheiden door komma’s, en een client mag de header ook op meer dan één regel sturen. De adapter geeft je handle elke regel: request["headers"]["if-none-match"] is een lijst van str, één per regel, zoals bij elke header. Dus etag_matches(values, etag) kijkt naar elk lid van elke regel:

  1. Splits elke waarde bij komma’s, en strip elk lid, omdat er een spatie na een komma kan staan.

  2. Een lid dat * is, komt meteen overeen.

  3. Een lid dat met W/ begint, verliest die twee tekens: W/"ba81..." wordt "ba81...".

  4. Een lid dat gelijk is aan etag, is een overeenkomst. De aanhalingstekens blijven aan beide kanten staan: je tags dragen ze, en de leden ook.

Als geen enkel lid overeenkomt, is het antwoord False.

Wat drukt dit af?

print('W/"a1", "b2"'.split(","))

Stap 3 is de zwakke vergelijking, die RFC 9110 voor If-None-Match vereist: twee tags komen overeen als hun delen tussen aanhalingstekens gelijk zijn, of er nu wel of niet W/ voor een van beide staat. Je server stuurt nooit W/, maar leest het wel.

Stap 2 handelt * af, dat voor elke tag staat: “stuur het alleen als het geen van deze is” is onwaar zodra de resource bestaat. Het wordt vooral met schrijfacties meegestuurd, maar RFC 9110 laat het ook bij een leesactie met elke actuele representatie overeenkomen. Je server beantwoordt * met een 304 voor elke lijst of elk boek dat bestaat, en met de gebruikelijke 404 voor een boek dat niet bestaat.

Bij een schrijfactie krijgt een die mislukt een status die je in documentatie tegenkomt, 412 Precondition Failed; zulke voorwaarden voorkomen dat twee clients elkaars wijzigingen overschrijven. Deze server controleert geen voorwaarden bij schrijfacties.

Splitsen bij komma’s is juist voor de tags van deze server, die alleen hex-cijfers bevatten; RFC 9110 (sectie 8.8.3) staat een komma binnen een tag toe, en deze lezer zou zo’n tag verkeerd lezen.

Een client stuurt if-none-match: * met GET /books/99, en boek 99 bestaat niet. Welke status stuurt je server?

Wat een 304 draagt

Een 304 zegt “de kopie die je hebt, is nog steeds de juiste”, en RFC 9110 (sectie 15.4.5) zegt wat die moet herhalen van de 200 waar hij voor in de plaats komt: onder andere de ETag en de Cache-Control die de 200 zou hebben gedragen. Beide hebben hier een taak. De etag bevestigt welke versie de client heeft. De cache-control start een nieuwe periode van versheid, dus een client die de kopie met max-age=60 heeft opgeslagen, mag hem nog eens 60 seconden hergebruiken. Les 5 bouwt een client die precies dat doet.

Een 304 heeft geen body: hij eindigt bij de lege regel na zijn headers. http.client weet dat en leest na een 304 niets, dus een body die je server toch stuurde, zou ongelezen op de verbinding blijven staan. Op een verbinding die meer dan één antwoord draagt, zoals in hoofdstuk 9, zouden die bytes worden gelezen als het begin van het volgende antwoord. Hij heeft ook geen content-length nodig. RFC 9110 (sectie 8.6) staat er alleen een toe als die gelijk is aan de lengte die de 200 zou hebben gehad, en hem weglaten is de eenvoudige juiste keuze. content-length: 0 is fout, omdat het niet de lengte van de 200 is.

Dus not_modified(response) neemt de 200 en geeft een nieuwe antwoord- terug: status 304, headers met alleen etag en cache-control gekopieerd van de 200, en de body b"". http.server voegt zijn eigen regels Date en Server toe.

Welke 304 is juist voor deze server?

Waar de regel komt

De regel hoort in handle na de routering, waar de 200 al bestaat met zijn tag, en vóór de HEAD-regel. Alleen een 200 voor een leesactie draagt een etag, dus dat is het antwoord om te controleren: als het er een heeft en de if-none-match-waarden van het verzoek ermee overeenkomen, vervang je het door not_modified(response). Elk ander antwoord blijft ongemoeid, en daarom krijgt * op een ontbrekend boek nog steeds zijn 404. GET en HEAD delen dit pad, dus beide kunnen een 304 krijgen.

Jouw opdracht

Schrijf in reading_server.py etag_matches(values, etag) en not_modified(response), en voeg de regel toe aan handle. Voer daarna het scenario opnieuw uit:

python pl_http.py conditional

Het tweede en derde verzoek krijgen nu 304 Not Modified, het vierde krijgt nog steeds de volledige 200, en onder elke 304 drukt het hulpprogramma de twee headers af die hij herhaalt:

  etag: "ba811c01801a2c40d5e507c3c439e7e966380095a4001999178055355cebbec1"
  cache-control: max-age=60

Als een 304 van jou een body draagt, toont het hulpprogramma die bytes op een eigen regel, (N bytes followed the reply: ...), direct onder het antwoord. Een correcte 304 heeft zo’n regel niet. Klik daarna op Check my work.

Je server kan een client nu de download besparen. Nog geen enkele client vraagt erom: les 5 geeft wire.py een geheugen dat een antwoord bewaart, het hergebruikt zolang het vers is, en met If-None-Match vraagt zodra het dat niet meer is.

Opdracht

Schrijf etag_matches(values, etag) en not_modified(response) in reading_server.py, en voeg de If-None-Match-regel toe aan handle.

  • etag_matches(values, etag): values is de met if-none-match-regels. Splits elke regel bij komma’s en strip elk lid. Geef True terug als een lid * is, of als het lid, met een eventuele W/ aan het begin verwijderd, gelijk is aan etag. Geef anders False terug.

  • not_modified(response): geef een nieuwe antwoord- terug met status 304, headers met alleen de etag en cache-control van de 200, en de body b"". Geen content-length.

  • In handle, nadat de route haar antwoord heeft gemaakt en vóór de HEAD-regel: als het antwoord een header etag heeft en het verzoek if-none-match- heeft die ermee overeenkomen, vervang je het antwoord door not_modified(response).

Voer python pl_http.py conditional uit in de om de 304’s te zien.

Klik daarna op Check my work. Die roept etag_matches aan met één tag, een lijst op één regel, twee regels, W/ ervoor, *, geen waarden, en tags die één teken verschillen of hun aanhalingstekens missen, en not_modified met een 200 waarvan je de cache-control nog niet hebt gezien. Hij roept handle aan met een state die je nog niet hebt gezien: GET en HEAD met de actuele tag, met W/, en binnen een lijst krijgen 304 zonder body, met de etag en cache-control van de 200, en zonder content-length (een die gelijk is aan die van de 200 wordt geaccepteerd, 0 niet); een verouderde tag krijgt de 200 met de body; na een POST krijgt de oude tag de nieuwe lijst; * krijgt 304 op /books en elders de gebruikelijke 404 of 400; schrijfacties houden hun antwoorden uit hoofdstuk 7. Hij schrijft ook één 304 in het geheugen door http_adapter, met de headernaam in vreemde hoofdletters, en controleert dat de bytes bij de lege regel eindigen. Niets uit je terminal wordt beoordeeld.