0%

Anfrage-Bodys und eine JSON-API · Übung

Sag, was der Body ist

read_body übergibt handle jetzt genau die Bytes, die der Client gesendet hat. Bytes allein sagen nicht, was sie sind: {"title": "Dune"} könnte sein, eine Textzeile oder der Anfang von etwas anderem. Der Absender sagt im Header Content-Type, was sie sind, im selben Header, den dein Server bereits auf jede Antwort setzt, die er sendet, content-type: application/json. Bevor dein Server einen Body als JSON behandelt, prüft er zwei Dinge: dass der Client ihn als JSON gekennzeichnet hat und dass die Bytes wirklich JSON sind.

Der Medientyp

Ein Content-Type- beginnt mit einem Medientyp, type/subtype, und kann nach einem Semikolon hinzufügen:

Content-Type: application/json
Content-Type: Application/JSON; charset=utf-8

Beide Zeilen kennzeichnen den Body als application/json. Bei Namen von Medientypen spielt Groß- und Kleinschreibung keine Rolle, und charset=utf-8 ist ein Parameter, kein Teil des Typs. Der Medientyp ist also der Teil vor dem ersten ;, ohne Leerzeichen, kleingeschrieben verglichen.

application/json definiert überhaupt keinen Parameter charset: JSON, das zwischen Systemen ausgetauscht wird, ist (RFC 8259, Abschnitt 8.1). Clients fügen charset=utf-8 trotzdem hinzu, und es schadet nicht, also ignoriert dieser Server jeden Parameter und dekodiert JSON immer als UTF-8.

Was gibt das aus?

print("Application/JSON; charset=UTF-8".split(";")[0].strip().lower())

Vergleich den ganzen Medientyp, nicht ein Stück davon. "json" in value akzeptiert auch application/jsonx, und startswith("application/json") akzeptiert application/json-patch+json. Letzteres ist ein echter Medientyp: , die auf +json enden (RFC 6839), sind JSON mit eigenen zusätzlichen Regeln. Dieser Server akzeptiert genau application/json und lehnt den Rest ab. Das ist seine Entscheidung, festgehalten in SERVICE_CONTRACT.md, keine Regel von .

Im Anfrage- sind Headernamen kleingeschrieben, und jeder enthält eine von Werten, also ist die Kennzeichnung request["headers"]["content-type"][0]. Eine Anfrage hat vielleicht gar keine Kennzeichnung; request["headers"].get("content-type", [""]) liefert dann eine Liste mit einem leeren , der zu nichts passt.

415 oder 400

Zwei verschiedene Dinge können falsch sein, und jedes hat seinen eigenen Status:

  • Die Kennzeichnung ist falsch oder fehlt. Der Server akzeptiert diesen Medientyp nicht, egal was die Bytes sind: 415 Unsupported Media Type. Er schaut sich die Bytes nie an.

  • Die Kennzeichnung stimmt, die Bytes nicht. Der Body ist kein UTF-8 oder kein gültiges JSON: 400 Bad Request, eine fehlerhafte Anfrage.

Die beiden Status sagen dem Client, dass er Verschiedenes reparieren soll. Nach einer 415 sendet er ein Format, das dieser Server akzeptiert, und kennzeichnet es entsprechend; nach einer 400 repariert er die Bytes, die er sendet. Ein Server, der auf beides mit 400 antwortet, lässt den Client raten, was schiefgegangen ist.

json_body(request) steht bereits in reading_server.py: Es gibt json.loads(request["body"].decode("utf-8")) zurück. Darin können zwei Schritte fehlschlagen. decode löst bei Bytes, die kein UTF-8 sind, UnicodeDecodeError aus, und json.loads löst bei Text, der kein JSON ist, json.JSONDecodeError aus. Beide sind Unterklassen von , also fängt ein einziges except ValueError: beide ab:

Try it

Das gibt {'title': 'Dune'} aus, dann JSONDecodeError, dann UnicodeDecodeError. Halte das except so eng. except Exception: würde auch einen Fehler in deinem eigenen Code, etwa einen falsch geschriebenen Schlüssel, in eine 400 verwandeln, die dem Client die Schuld gibt.

Dekodiere nicht als Latin-1, damit der Fehler verschwindet. Jedes Byte ist ein Latin-1-Zeichen, also schlägt dieses Dekodieren nie fehl, und Bytes, die kein UTF-8 sind, würden als falscher Text gelesen, statt abgelehnt zu werden.

Ein Body ist gültiges JSON, aber als text/plain gekennzeichnet. Welcher Status?

Deine Aufgabe

Schreib body_error(request) in reading_server.py:

  1. Nimm den Medientyp aus dem content-type der Anfrage, wie oben. Wenn er nicht application/json ist, gib eine 415-Antwort zurück, gebaut mit json_error.

  2. Ruf json_body(request) innerhalb von try auf. Wenn es ValueError auslöst, gib eine 400-Antwort zurück.

  3. Andernfalls gib None zurück.

Wie read_body gibt es None oder die zu sendende Antwort zurück. Jede Fehlerantwort kommt von json_error, also trägt sie ihr eigenes content-type: application/json, selbst wenn sie die Kennzeichnung des Clients ablehnt.

Bis Lektion 4 ruft keine Route body_error auf, also probier es im Tab Shell aus. make_request in tests/test_server.py baut ein Anfrage-Dictionary mit einem JSON-Body, einem content-type und einer Länge; das Skript ändert die Kennzeichnung einer Anfrage und die Bytes einer anderen:

python - <<'EOF'
import reading_server
from tests.test_server import make_request

book = {"title": "Dune", "author": "Frank Herbert"}
print(reading_server.body_error(make_request("POST", "/books", book)))
labeled = make_request("POST", "/books", book)
labeled["headers"]["content-type"] = ["text/plain"]
print(reading_server.body_error(labeled)["status"])
broken = make_request("POST", "/books", book)
broken["body"] = b'{"title": '
print(reading_server.body_error(broken)["status"])
EOF

Es gibt None, 415 und 400 aus.

Drück dann Check my work. Die nächste Lektion tritt einen Schritt vom Code zurück, um zu entscheiden, wohin ein neues Buch kommt und was jeder Schreibzugriff antwortet.

Aufgabe

Prüf, was ein Anfrage-Body zu sein behauptet, bevor du ihn als behandelst.

Schreib in reading_server.py die body_error(request):

  1. Nimm den Medientyp aus dem content-type der Anfrage: den Teil seines ersten vor einem eventuellen ;, ohne Leerzeichen und kleingeschrieben. Eine Anfrage ohne content-type hat keinen. Wenn der Medientyp nicht genau application/json ist, gib eine 415-Antwort zurück.

  2. Ruf das vorhandene json_body(request) innerhalb von try auf. Wenn es auslöst, gib eine 400-Antwort zurück.

  3. Andernfalls gib None zurück.

Bau beide Fehlerantworten mit json_error und einem eigenen Satz. Fang nur ValueError ab, nicht Exception.

Im gibt das Skript aus der Lektion aus, was body_error für drei Anfragen zurückgibt, die mit make_request aus tests/test_server.py gebaut wurden.

Drück dann Check my work. Es ruft body_error mit Kennzeichnungen und Bodys auf, die du noch nicht gesehen hast: application/json in anderen Schreibweisen und mit , Kennzeichnungen, die nur json enthalten, gar keine Kennzeichnung, Bytes, die kein sind, und Text, der kein JSON ist. Es prüft, dass die Kennzeichnung vor den Bytes beurteilt wird, dass ein Fehler aus deinem eigenen Code nicht in eine 400 verwandelt wird und dass ein Headername in ungewöhnlicher Schreibweise deine Funktion trotzdem über den Adapter erreicht. Nichts aus deinem Terminal wird bewertet.