0%

Caching und bedingte Anfragen · Übung

Beantworte If-None-Match mit 304

Jeder Lesezugriff trägt jetzt ein Tag, und ein Client kann dieses Tag zurücksenden. Dein Server hört noch nicht hin. Starte das Szenario conditional des Hilfsprogramms:

python pl_http.py conditional

Es sendet vier Anfragen. Die erste ist ein einfaches GET /books. Die zweite sendet das Tag, das die erste Antwort trug, in einem neuen Header zurück:

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

Die dritte ist ein HEAD mit W/ vor demselben Tag, und die vierte sendet If-None-Match: "stale". Heute bekommen alle vier 200 OK mit der vollen von 132 Bytes, oder bei HEAD ihrer Länge, obwohl die zweite Anfrage diese Liste schon Byte für Byte hat.

Die Frage in einem Header

If-None-Match macht aus einem GET eine Frage: „Sende den Body nur, wenn sein Tag keins von diesen ist.“ Der Client setzt das Tag der Kopie ein, die er hat. Der Server vergleicht es mit dem Tag dessen, was er jetzt senden würde, und antwortet auf eine von zwei Arten:

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

Ein GET mit If-None-Match heißt bedingtes GET. Seine 304 ist kurz, eine Statuszeile und ein paar Header, weil der Client den Body schon hat.

Abgleichen

Der des Headers ist eine Liste, nicht ein einzelnes Tag (RFC 9110, Abschnitt 13.1.2). Ein Client, der mehrere Versionen einer Ressource hat, kann alle nennen, durch Kommas getrennt, und ein Client kann den Header auch auf mehr als einer Zeile senden. Der Adapter übergibt deinem handle jede Zeile: request["headers"]["if-none-match"] ist eine Liste von str, eins pro Zeile, wie bei jedem Header. Also sieht sich etag_matches(values, etag) jedes Element jeder Zeile an:

  1. Teile jeden Wert an Kommas und entferne bei jedem Element die Leerzeichen am Rand, weil nach einem Komma ein Leerzeichen folgen kann.

  2. Ein Element, das * ist, passt sofort.

  3. Ein Element, das mit W/ beginnt, verliert diese zwei Zeichen: W/"ba81..." wird zu "ba81...".

  4. Ein Element, das gleich etag ist, passt. Die Anführungszeichen bleiben auf beiden Seiten: Deine Tags tragen sie, und die Elemente auch.

Wenn kein Element passt, ist die Antwort False.

Was gibt das aus?

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

Schritt 3 ist der schwache Vergleich, den RFC 9110 für If-None-Match verlangt: Zwei Tags passen, wenn ihre Teile in Anführungszeichen gleich sind, egal ob eines davon W/ davor hat. Dein Server sendet nie W/, aber er liest es.

Schritt 2 behandelt *, das für überhaupt jedes Tag steht: „Sende es nur, wenn keins von diesen passt“ ist falsch, sobald die Ressource existiert. Es wird meist bei Schreibzugriffen gesendet, aber RFC 9110 lässt es auch bei einem Lesezugriff auf jede aktuelle Repräsentation passen. Dein Server beantwortet * mit einer 304 für jede Liste und jedes Buch, das existiert, und mit der üblichen 404 für ein Buch, das nicht existiert.

Bei einem Schreibzugriff bekommt eine fehlgeschlagene einen Status, den du in Dokumentationen genannt sehen wirst, 412 Precondition Failed; solche Bedingungen verhindern, dass zwei Clients gegenseitig ihre Änderungen überschreiben. Dieser Server prüft bei Schreibzugriffen keine Bedingungen.

An Kommas zu teilen ist für die Tags dieses Servers richtig, die nur Hex-Ziffern enthalten; RFC 9110 (Abschnitt 8.8.3) erlaubt ein Komma innerhalb eines Tags, und dieser Leser würde so eines falsch lesen.

Ein Client sendet if-none-match: * mit GET /books/99, und Buch 99 existiert nicht. Welchen Status sendet dein Server?

Was eine 304 trägt

Eine 304 sagt „die Kopie, die du hast, ist immer noch die richtige“, und RFC 9110 (Abschnitt 15.4.5) sagt, was sie von der 200, für die sie steht, wiederholen muss: unter anderem das ETag und das Cache-Control, das die 200 getragen hätte. Beide haben hier eine Aufgabe. Das etag bestätigt, welche Version der Client hat. Das cache-control startet einen neuen Frischezeitraum, also darf ein Client, der die Kopie mit max-age=60 gespeichert hat, sie weitere 60 Sekunden wiederverwenden. Lektion 5 baut einen Client, der genau das tut.

Eine 304 hat keinen Body: Sie endet an der Leerzeile nach ihren Headern. http.client weiß das und liest nach einer 304 nichts, also würde ein Body, den dein Server trotzdem sendet, ungelesen auf der Verbindung liegen bleiben. Auf einer Verbindung, die mehr als eine Antwort trägt, wie in Kapitel 9, würden diese Bytes als Anfang der nächsten Antwort gelesen. Sie braucht auch kein content-length. RFC 9110 (Abschnitt 8.6) erlaubt eins nur, wenn es der Länge entspricht, die die 200 gehabt hätte, und es wegzulassen ist die einfache, korrekte Wahl. content-length: 0 ist falsch, weil es nicht die Länge der 200 ist.

Also nimmt not_modified(response) die 200 und gibt ein neues Antwort- zurück: Status 304, Header, in die nur etag und cache-control aus der 200 kopiert sind, und den Body b"". http.server fügt seine eigenen Zeilen Date und Server hinzu.

Welche 304 ist für diesen Server richtig?

Wohin die Regel kommt

Die Regel gehört in handle nach dem Routing, wo die 200 mit ihrem Tag bereits existiert, und vor die HEAD-Regel. Nur eine 200 für einen Lesezugriff trägt ein etag, also ist das die Antwort, die geprüft wird: Wenn sie eins hat und die if-none-match-Werte der Anfrage dazu passen, ersetze sie durch not_modified(response). Jede andere Antwort bleibt unberührt, und deshalb bekommt * bei einem fehlenden Buch weiterhin seine 404. GET und HEAD teilen sich diesen Weg, also können beide eine 304 bekommen.

Deine Aufgabe

Schreib in reading_server.py etag_matches(values, etag) und not_modified(response) und füge die Regel zu handle hinzu. Starte dann das Szenario noch einmal:

python pl_http.py conditional

Die zweite und dritte Anfrage bekommen jetzt 304 Not Modified, die vierte bekommt weiterhin die volle 200, und unter jeder 304 gibt das Hilfsprogramm die zwei Header aus, die sie wiederholt:

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

Wenn eine deiner 304-Antworten einen Body trägt, zeigt das Hilfsprogramm diese Bytes in einer eigenen Zeile, (N bytes followed the reply: ...), direkt unter der Antwort. Eine korrekte 304 hat keine solche Zeile. Drück dann Check my work.

Dein Server kann einem Client jetzt den Download ersparen. Noch fragt kein Client: Lektion 5 gibt wire.py ein Gedächtnis, das eine Antwort aufbewahrt, sie wiederverwendet, solange sie frisch ist, und mit If-None-Match fragt, sobald sie es nicht mehr ist.

Aufgabe

Schreib etag_matches(values, etag) und not_modified(response) in reading_server.py und füge die If-None-Match-Regel zu handle hinzu.

  • etag_matches(values, etag): values ist die der if-none-match-Zeilen. Teile jede Zeile an Kommas und entferne bei jedem Element die Leerzeichen am Rand. Gib True zurück, wenn ein Element * ist oder wenn das Element ohne ein führendes W/ gleich etag ist. Andernfalls gib False zurück.

  • not_modified(response): Gib ein neues Antwort- mit Status 304 zurück, mit Headern, die nur etag und cache-control der 200 enthalten, und dem Body b"". Kein content-length.

  • In handle, nachdem die Route ihre Antwort erzeugt hat und vor der HEAD-Regel: Wenn die Antwort einen Header etag hat und die Anfrage if-none-match- hat, die dazu passen, ersetze die Antwort durch not_modified(response).

Führ python pl_http.py conditional im aus, um die 304er zu sehen.

Drück dann Check my work. Es ruft etag_matches mit einem Tag auf, mit einer Liste auf einer Zeile, mit zwei Zeilen, mit W/ davor, mit *, ohne Werte und mit Tags, die sich in einem Zeichen unterscheiden oder denen die Anführungszeichen fehlen, und not_modified mit einer 200, deren cache-control du noch nicht gesehen hast. Es ruft handle mit einem Zustand auf, den du noch nicht gesehen hast: GET und HEAD mit dem aktuellen Tag, mit W/ und innerhalb einer Liste bekommen 304 ohne Body, mit etag und cache-control der 200 und ohne content-length (eins, das dem der 200 entspricht, wird akzeptiert, 0 nicht); ein veraltetes Tag bekommt die 200 mit dem Body; nach einem POST bekommt das alte Tag die neue Liste; * bekommt 304 auf /books und anderswo die übliche 404 oder 400; Schreibzugriffe behalten ihre Antworten aus Kapitel 7. Es schreibt außerdem eine 304 im Speicher durch http_adapter, mit dem Headernamen in ungewöhnlicher Schreibweise, und prüft, dass die Bytes an der Leerzeile enden. Nichts aus deinem Terminal wird bewertet.