0%

HTTP ist Text auf einem Socket · Übung

Statuszeile und Header lesen

fetch gibt jetzt den Kopf einer Antwort als Bytes zurück. Mit Bytes kann ein Programm nicht viel anfangen: Es braucht den als Zahl, um zu entscheiden, ob die Anfrage funktioniert hat, und die Header nach Namen, um herauszufinden, wie lang der Body ist. Diese Lektion schreibt parse_head(head), das den Kopf in ein verwandelt:

{"version": "HTTP/1.1", "status": 200, "reason": "OK", "headers": {"content-type": ["application/json"], ...}}

Die Statuszeile

Die erste Zeile des Kopfes ist die Statuszeile. Wie eine Anfragezeile hat sie drei Teile, getrennt durch Leerzeichen: die Version, den Statuscode und den Statustext (reason phrase), eine kurze Beschreibung für Menschen. Der Statustext kann selbst Leerzeichen enthalten, also würde ein Schnitt an jedem Leerzeichen ihn auseinanderreißen. Genau dafür nimmt split ein zweites , maxsplit, wie in Lektion 2: Mit 2 schneidet es nur an den ersten beiden Leerzeichen, und der Rest der Zeile bleibt ein Stück.

Try it

Was gibt das aus?

print("HTTP/1.1 429 Too Many Requests".split(" ", 2))

Drei Regeln entscheiden, ob parse_head eine Statuszeile akzeptiert:

  • Die Version beginnt mit HTTP/.

  • Der Code besteht aus genau drei Ziffern. Prüfe das mit isascii() und isdigit() zusammen: isdigit() allein akzeptiert auch Zeichen wie ², die int nicht umwandeln kann.

  • Der Statustext darf leer sein. Ein Server kann HTTP/1.1 299 mit nichts nach dem letzten Leerzeichen senden oder HTTP/1.1 299 ganz ohne letztes Leerzeichen. Im ersten Fall ergibt split(" ", 2) drei Elemente, das letzte davon ""; im zweiten ergibt es zwei. So oder so ist der Statustext "".

Try it

Alles andere ist keine Statuszeile, und parse_head löst aus. Ein Code, der die Prüfungen besteht, wird zu einem int, damit ein Aufrufer ihn mit 200 oder 404 vergleichen kann.

Von Bytes zu Text

Der Kopf kommt als Bytes an, und split(" ", 2) braucht Text. Dekodiere den ganzen Kopf mit "iso-8859-1", bevor du ihn aufschneidest. Dieses Encoding bildet jedes mögliche Byte auf genau ein Zeichen ab, also schlägt das Dekodieren eines Kopfes nie fehl, egal was ein Server sendet. Ein Kopf soll reines ASCII sein, und für ASCII-Bytes ist das Ergebnis dasselbe wie mit jedem anderen gängigen Encoding. Pythons eigenes http.client liest Köpfe auf dieselbe Weise.

Headerzeilen

Schneide den Kopf nach der Statuszeile an \r\n auf und behandle jede Zeile als : ein Name, ein Doppelpunkt und ein . Schneide jede Zeile nur am ersten Doppelpunkt auf, line.split(":", 1), weil ein Wert selbst Doppelpunkte enthalten kann:

Try it

Bring dann beide Teile in Ordnung. strip() entfernt die Leerzeichen um den Wert, die erlaubt. lower() schreibt den Namen klein, weil bei Headernamen Groß- und Kleinschreibung keine Rolle spielt: Content-Length, content-length und CONTENT-length sind derselbe Header. Die Bibliothek requests, die du in Python in der Praxis benutzt hast, lässt dich einen Antwort-Header in beliebiger Schreibweise nachschlagen; dein Parser erreicht dasselbe, indem er jeden Namen in einer einzigen Schreibweise speichert. Eine Zeile ohne Doppelpunkt ist keine Headerzeile, und parse_head löst dafür ValueError aus.

Derselbe Name kann mehr als einmal vorkommen, und eine spätere Zeile darf eine frühere nicht löschen. Deshalb bildet das Dictionary jeden Namen auf eine von Werten ab, und jede Zeile hängt ihren Wert an die Liste für ihren Namen an, wobei eine neue Liste entsteht, wenn ein Name zum ersten Mal vorkommt. Eine Antwort mit Vary: a und danach vary: b ergibt {"vary": ["a", "b"]}.

Ein Server hat die Headerzeile content-LENGTH: 17 gesendet. Mit welchem Zugriff findest du sie in dem, was parse_head zurückgibt?

Deine Aufgabe

Schreibe in wire.py die parse_head(head):

  1. Dekodiere head mit "iso-8859-1" und schneide es an "\r\n" auf;

  2. schneide die erste Zeile mit split(" ", 2) auf; löse ValueError aus, außer sie hat mindestens zwei Teile, von denen der erste mit "HTTP/" beginnt und der zweite aus genau drei ASCII-Ziffern besteht; der Statustext ist der dritte Teil, falls es einen gibt, und sonst "";

  3. löse für jede weitere Zeile ValueError aus, wenn sie keinen Doppelpunkt enthält; schneide sie sonst am ersten Doppelpunkt auf, schreibe den Namen klein und entferne seine Leerzeichen, entferne die Leerzeichen um den Wert und hänge den Wert an die Liste für diesen Namen an;

  4. gib {"version": ..., "status": ..., "reason": ..., "headers": ...} zurück, mit dem Status als int.

Lass alles andere in wire.py so, wie es ist. fetch gibt den Kopf immer noch als Bytes zurück, also probiere parse_head im Tab Shell an einem echten Kopf aus:

python -c "import wire; print(wire.parse_head(wire.fetch('GET', '/v1/items/1')))"

Die ersten drei Einträge kommen aus der Statuszeile. Deine Statuszeile sollte HTTP/1.1 200 OK lauten. Wenn sie HTTP/1.0 200 OK lautet, kam die Antwort direkt vom Dienst, der in HTTP/1.0 antwortet; parse_head akzeptiert beides, und Kapitel 9 erklärt, was die Version ändert. Bei den Headern sollte content-type den Wert ['application/json; charset=utf-8'] enthalten und content-length den Wert ['53'], die Größe des Bodys dieses Eintrags, den fetch noch nicht gelesen hat. Wenn deine Header server und date enthalten, kommen diese Zeilen von dem kleinen Webserver, der vor practice-api steht; wenn eine Antwort, die du bekommst, keine von beiden enthält, kam sie direkt vom Dienst, und nichts in diesem Kurs hängt von ihnen ab.

Drücke dann Check my work. Die nächste Lektion benutzt content-length, um genau den Body zu lesen, der auf den Kopf folgt.

Aufgabe

Schreibe parse_head(head) in wire.py. Die nimmt einen Kopf als Bytes, so wie read_head ihn zurückgibt, und gibt ein zurück:

  • "version": die Version aus der Statuszeile, als str, etwa "HTTP/1.1";

  • "status": den als int;

  • "reason": den Statustext, vollständig, mit Leerzeichen, oder "", wenn die Statuszeile keinen hat;

  • "headers": ein Dictionary von jedem Headernamen, kleingeschrieben, auf die seiner in der Reihenfolge ihrer Ankunft, jeder Wert ohne die Leerzeichen um ihn herum.

Dekodiere den Kopf mit "iso-8859-1", schneide die Statuszeile mit split(" ", 2) auf und schneide jede nur an ihrem ersten Doppelpunkt auf. Löse aus für eine Statuszeile, deren Version nicht mit HTTP/ beginnt oder deren Code nicht aus genau drei ASCII-Ziffern besteht, und für eine Headerzeile ohne Doppelpunkt.

Lass die anderen Funktionen in wire.py so, wie sie sind. Probiere sie im mit python -c "import wire; print(wire.parse_head(wire.fetch('GET', '/v1/items/1')))" an einem echten Kopf aus.

Drücke dann Check my work. Die Prüfung ruft parse_head auf Köpfen auf, die du nicht gesehen hast, als Bytes, ohne : Headernamen in ungewöhnlicher Schreibweise, ein Header, der zweimal vorkommt, ein Wert mit Leerzeichen um ihn herum und einem Doppelpunkt darin, ein Statustext mit Leerzeichen, ein Statuscode außerhalb der gängigen, eine Statuszeile mit leerem Statustext in beiden Formen und fehlerhafte Status- und Headerzeilen, die ValueError auslösen müssen. Dein Dictionary darf zusätzliche Schlüssel enthalten. Die Prüfung liest dein Terminal nicht und nimmt keinen Kontakt mit practice-api auf.