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 parse_head(head), das den Kopf in ein
{"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.
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()undisdigit()zusammen:isdigit()allein akzeptiert auch Zeichen wie², dieintnicht umwandeln kann.Der Statustext darf leer sein. Ein Server kann
HTTP/1.1 299mit nichts nach dem letzten Leerzeichen senden oderHTTP/1.1 299ganz ohne letztes Leerzeichen. Im ersten Fall ergibtsplit(" ", 2)drei Elemente, das letzte davon""; im zweiten ergibt es zwei. So oder so ist der Statustext"".
Alles andere ist keine Statuszeile, und parse_head löst 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 line.split(":", 1), weil ein Wert selbst Doppelpunkte enthalten kann:
Bring dann beide Teile in Ordnung. strip() entfernt die Leerzeichen um den Wert, die 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 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):
Dekodiere
headmit"iso-8859-1"und schneide es an"\r\n"auf;schneide die erste Zeile mit
split(" ", 2)auf; löseValueErroraus, 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"";löse für jede weitere Zeile
ValueErroraus, 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;gib
{"version": ..., "status": ..., "reason": ..., "headers": ...}zurück, mit dem Status alsint.
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 read_head ihn zurückgibt, und gibt ein
"version": die Version aus der Statuszeile, als str, etwa"HTTP/1.1";"status": denals int;"reason": den Statustext, vollständig, mit Leerzeichen, oder"", wenn die Statuszeile keinen hat;"headers": ein Dictionary von jedem Headernamen, kleingeschrieben, auf dieseiner 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 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 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 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.