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 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-type/subtype, und kann nach einem Semikolon
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 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: +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-request["headers"]["content-type"][0]. Eine Anfrage hat vielleicht gar keine Kennzeichnung; request["headers"].get("content-type", [""]) liefert dann eine Liste mit einem leeren
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 except ValueError: beide ab:
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:
Nimm den Medientyp aus dem
content-typeder Anfrage, wie oben. Wenn er nichtapplication/jsonist, gib eine 415-Antwort zurück, gebaut mitjson_error.Ruf
json_body(request)innerhalb vontryauf. Wenn esValueErrorauslöst, gib eine 400-Antwort zurück.Andernfalls gib
Nonezurü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
Schreib in reading_server.py die body_error(request):
Nimm den Medientyp aus dem
content-typeder Anfrage: den Teil seines erstenvor einem eventuellen ;, ohne Leerzeichen und kleingeschrieben. Eine Anfrage ohnecontent-typehat keinen. Wenn der Medientyp nicht genauapplication/jsonist, gib eine 415-Antwort zurück.Ruf das vorhandene
json_body(request)innerhalb vontryauf. Wenn esauslöst, gib eine 400-Antwort zurück. Andernfalls gib
Nonezurück.
Bau beide Fehlerantworten mit json_error und einem eigenen Satz. Fang nur ValueError ab, nicht Exception.
Im 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 json enthalten, gar keine Kennzeichnung, Bytes, die kein