0%

Kapitel 5 · Übung

Anfrage-Bodys und eine JSON-API

Lies genau den Body der Anfrage

Jede Anfrage, die deine Leseliste bisher behandelt hat, endete an der Leerzeile nach ihren Headern. Eine Anfrage, die ein Buch hinzufügt oder ändert, muss das Buch mitbringen, und sie trägt es so, wie eine Antwort einen Body trägt: nach der Leerzeile, mit Content-Length, das sagt, wie viele Bytes folgen. In Kapitel 1 hat dein Client einen Antwort-Body anhand dieser Zahl gelesen; jetzt liest dein Server einen Anfrage-Body auf dieselbe Weise.

Öffne http_adapter.py und such die respond, die du in der ersten Lektion von Kapitel 4 gelesen hast:

def respond(self):
    request: dict = request_record(self)
    if read_body is not None:
        error = read_body(request, self.rfile.read)
        if error is not None:
            write_response(self, error)
            return
    write_response(self, handle(request, state))

Wenn make_handler_class eine read_body bekommt, ruft der Adapter sie vor handle auf, mit dem Anfrage- und self.rfile.read, der Funktion, die die Bytes nach dem Kopf aus der Verbindung liest. Dein make_server übergibt noch kein read_body, also erreicht jede Anfrage handle mit "body": b"", egal was der Client gesendet hat. Diese Lektion schreibt read_body und übergibt es.

Was read(n) tut

read(n) gibt Bytes aus der Verbindung zurück, beginnend direkt nach dem Kopf, den http.server bereits gelesen hat. Es gibt genau n Bytes zurück, außer der Client schließt vorher seine Senderichtung; dann gibt es zurück, was angekommen ist, möglicherweise b"". Innen drin ist es die read_exact- aus Kapitel 1, für dich geschrieben. Nach body = read(length) bedeutet die Prüfung len(body) < length also, dass der Client aufgehört hat zu senden, bevor der Body vollständig war.

Ein Client kann auch weniger Bytes senden und die Verbindung dann offen halten, während er auf eine Antwort wartet, die nie kommt. Der Adapter gibt jeder Verbindung einen Timeout von 2 Sekunden, also löst read nach zwei Sekunden TimeoutError aus, http.server schreibt Request timed out in sein Log, und der Client bekommt überhaupt keine Antwort. Deine Funktion sieht diesen Fall nie.

Übergib immer die Zahl. read() ohne Zahl liest, bis der Client seine Senderichtung schließt, und ein Client, der seine Anfrage gesendet hat und auf die Antwort wartet, tut das nie.

Drei Ablehnungen

Bevor read_body ein einziges Byte liest, entscheidet es anhand der Header, ob es einen Body gibt und wie groß er ist. request["headers"] enthält kleingeschriebene Namen mit jeweils einer von , also steht die Länge unter "content-length".

Keine Länge. Eine Anfrage ohne Längen-Header hat keinen Body, also ist ein GET, HEAD oder DELETE ohne einen solchen in Ordnung: Gib None zurück und lass request["body"] als b"". POST und PUT sind die Anfragen, von denen dieser Server erwartet, dass sie ein Buch tragen, und bei ihnen besteht er auf der Länge: Fehlt sie, gibt es 411 Length Required. Die Regel ist einfach: Jedes POST und PUT an diesen Server trägt Content-Length, und eine Anfrage, die nichts zu senden hat, sagt das mit content-length: 0.

Eine fehlerhafte Länge. Es muss genau ein Wert sein, der aus ASCII-Ziffern besteht. Zwei Werte sind mehrdeutig, und 12a oder -5 ist keine Anzahl von Bytes; beides bekommt 400 Bad Request. Prüf die Ziffern mit isascii() und isdigit(), wie es book_id_from tut, und wandle dann mit int um, das führende Nullen akzeptiert. isdigit() allein akzeptiert auch Ziffern aus anderen Schriften und Zeichen wie das hochgestellte ², die int() ablehnt; ² kann in einem Header ankommen, weil http.server als Latin-1 dekodiert.

Zu lang. MAX_BODY ist 10000, und read_body nimmt die Grenze als limit, dessen Standardwert es ist. Vergleich die angekündigte Länge mit limit, bevor du read aufrufst, und antworte mit 413 Content Too Large, wenn sie darüber liegt. Die Pinnwand in Networks with Python hat die angekündigte Länge eines Frames genauso geprüft, aus demselben Grund: Ein Server, der zuerst liest, hat bereits auf alles gewartet und alles festgehalten, was der Client zu senden beschlossen hat.

Warum wird die Länge mit limit verglichen, bevor read aufgerufen wird?

Was gibt das aus?

print(int("0012"), "12a".isdigit(), "١٢".isdigit(), "١٢".isascii())

Dann lies. Wenn read weniger Bytes zurückgibt, als die Länge angibt, hat der Body zu früh geendet: wieder 400. Die Prüfungen kommen also in dieser Reihenfolge: Ist eine Länge da, ist sie wohlgeformt, liegt sie innerhalb der Grenze, ist der ganze Body angekommen?

None oder der Fehler

read_body hat zwei Arten von Ergebnis. Wenn der Body in Ordnung ist, speichert es die Bytes in request["body"] und gibt None zurück, und der Adapter ruft anschließend handle auf. Wenn nicht, gibt es ein vollständiges Antwort-Dictionary zurück, und der Adapter sendet dieses, statt handle aufzurufen. Bau jede Ablehnung mit json_error, damit sie mit einem nach Bytes gezählten content-length ist wie jede andere Antwort. Die Funktionen, die du in den Lektionen 2 und 5 schreibst, funktionieren genauso: None oder die zu sendende Antwort.

Eine Folge davon: Der Adapter prüft die Rahmung, bevor handle die Anfrage sieht, also bekommt ein POST /nowhere ohne Länge 411, nicht 404. SERVICE_CONTRACT.md sagt das ebenfalls.

Ein Client kann einen Body auch mit Transfer-Encoding: chunked senden, in Stücken, die jeweils ihre eigene Größe tragen, und ganz ohne Content-Length. Ein /1.1-Server muss diese Rahmung lesen können (RFC 9112, Abschnitt 7.1). Deiner antwortet in HTTP/1.0 und liest sie nicht: Ein solches POST hat keine Länge, also bekommt es 411.

Deine Aufgabe

Schreib read_body(request, read, *, limit=MAX_BODY) in reading_server.py mit den vier Ergebnissen von oben und ändere dann make_server so, dass es die Funktion an den Adapter übergibt: http_adapter.make_handler_class(handle, state, read_body=read_body).

Du kannst die Funktion im Tab Shell ohne Server ausprobieren. io.BytesIO hält Bytes im Speicher, und sein read(n) verhält sich wie das der Verbindung:

python - <<'EOF'
import io
import reading_server

request = {"method": "POST", "target": "/books", "version": "HTTP/1.1", "headers": {"content-length": ["5"]}, "body": b""}
print(reading_server.read_body(request, io.BytesIO(b"hello, and more").read))
print(request["body"])
EOF

Es gibt None und dann b'hello' aus: fünf Bytes und nichts danach. Starte dann das Hilfsprogramm:

python pl_http.py body

Es sendet vier Anfragen POST /books. Vor deiner Änderung bekommen alle vier HTTP/1.0 405 Method Not Allowed von handle. Danach bekommt die erste, mit einem vollständigen JSON-Body, immer noch 405, weil handle bis Lektion 4 kein POST annimmt, aber der Adapter hat ihren Body vorher gelesen. Die zweite hat keine Länge: HTTP/1.0 411 Length Required. Die dritte kündigt 5000000 Bytes an und sendet keine: HTTP/1.0 413 Content Too Large, ohne dass etwas gelesen wurde. Die vierte kündigt 40 Bytes an, sendet 10 und schließt ihre Senderichtung: HTTP/1.0 400 Bad Request.

Ein POST /books kommt ohne Content-Length an. Welcher Status?

Drück dann Check my work. Als Nächstes lernt der Server zu fragen, was die gelesenen Bytes sein sollen.

Aufgabe

Lies Anfrage-Bodys anhand ihrer Länge und lehne die ab, die dieser Server nicht abgrenzen kann.

Schreib in reading_server.py die read_body(request, read, *, limit=MAX_BODY). Prüf in dieser Reihenfolge:

  1. Kein "content-length" in request["headers"]: Gib bei einem POST oder PUT eine 411-Antwort zurück und bei jeder anderen None, wobei request["body"] als b"" bleibt.

  2. Nicht genau ein , oder ein Wert, der nicht aus ASCII-Ziffern besteht: Gib eine 400-Antwort zurück.

  3. Eine Länge größer als limit: Gib eine 413-Antwort zurück, ohne read aufzurufen.

  4. Ruf read(length) mit der Länge auf. Wenn es weniger Bytes zurückgibt, gib eine 400-Antwort zurück. Andernfalls speichere die Bytes in request["body"] und gib None zurück.

Bau jede Fehlerantwort mit json_error und einem eigenen Satz. Ändere dann make_server so, dass es die Funktion an den Adapter übergibt: http_adapter.make_handler_class(handle, state, read_body=read_body).

Im sendet python pl_http.py body vier POST-Anfragen an deinen Server und zeigt, was zurückkam.

Drück dann Check my work. Es ruft read_body mit Anfrage- und einem Ersatz-read auf, das es beobachten kann, und verwendet Bodys, Längen und Grenzen, die du noch nicht gesehen hast: vollständige Bodys, content-length: 0, fehlende und fehlerhafte Längen, Längen knapp über und genau an der Grenze und einen Body, der zu früh endet. Es prüft, dass read die Länge bekommt und vor einer 411, 400 oder 413 nie aufgerufen wird. Dann baut es deinen Server mit make_server, ohne einen zu öffnen, und schickt zwei Anfragen durch die Handler-Klasse, eine mit Body und eine ohne Länge, um zu sehen, dass der Body handle erreicht und dass die 411 hinausgeht, statt die Funktion aufzurufen. Nichts aus deinem Terminal wird bewertet.