0%

Was ein Framework für dich tut · Übung

Lass ein Modell den Body prüfen

In Kapitel 5 haben zwei jeden Schreibzugriff bewacht, den dein Server angenommen hat, und sie stehen immer noch in reading_server.py. body_error hat sich die Anfrage angesehen und book_error die geparsten Daten:

FunktionLehnt ab, wennStatus
body_error(request)content-type nicht application/json ist415
body_error(request)der Body kein - ist400
book_error(data)kein JSON- vorliegt422
book_error(data)Titel oder Autor fehlt422
book_error(data)Titel oder Autor kein nicht leerer ist422

handle hat sie in dieser Reihenfolge aufgerufen, und create_book und replace_book liefen nur, wenn beide None zurückgaben. Die App erledigt beide Aufgaben mit dem Modell, das du in Lektion 1 gelesen hast, BookIn, und einer Annotation in jeder def-Zeile: book: BookIn.

Das Modell prüft zuerst

Wenn POST /books ankommt, liest den Body, parst ihn als JSON und prüft das Ergebnis gegen BookIn, alles bevor create_book läuft. Deine Funktion wird nur mit einem Body aufgerufen, der die Prüfung bestanden hat, und sie bekommt diesen Body als BookIn-Objekt. Das Objekt gibt seine geprüften als Attribute heraus, mit einem Punkt geschrieben: book.title und book.author. Es ist kein , also löst book["title"] aus.

Mit den Versionen, die dieser Kurs benutzt, lehnt das Modell Folgendes ab:

  • einen Body, der kein JSON-Objekt ist oder dem title oder author fehlt;

  • ein Feld, das kein String ist: 7 und true werden abgelehnt, nie in "7" oder "true" umgewandelt;

  • ein Feld ohne ein anderes Zeichen als Leerzeichen, etwa "" oder " ". Das verlangt pattern=r"\S": ein Zeichen, irgendwo, das kein Leerzeichen, kein Tab, kein Zeilenumbruch und keine andere Art von Leerraum ist.

Den Rest akzeptiert es so, wie er ist. " Ærø " behält seine Leerzeichen, "7" ist ein String, und ein Titel mit 3000 Zeichen hat keine Grenze, die er überschreiten könnte. Felder, die das Modell nicht nennt, etwa "id": 99, werden verworfen, bevor deine Funktion den Body sieht, also kann ein Client die ID eines Buchs nicht wählen.

Ein POST-Body ist {"title": "Dune"}. Wann läuft create_book?

Die 422 von FastAPI lesen

Auf diesen Body hat dein Server aus Kapitel 5 mit {"error": "author is missing"} geantwortet. Die App antwortet mit 422 und diesem Body:

{"detail":[{"type":"missing","loc":["body","author"],"msg":"Field required","input":{"title":"Dune"}}]}

detail ist eine mit einem Eintrag pro Problem. In jedem Eintrag nennt type die Regel, die verletzt wurde (hier missing; string_type für eine Zahl, wo ein String hingehört; string_pattern_mismatch für einen leeren Titel), loc sagt, wo (["body", "author"], das Feld author des Bodys), msg sagt es in Worten, und input gibt den geprüften Wert wieder. Dein Fehler war ein Satz für einen Menschen. Dieser hier sind Daten, mit denen ein Client-Programm Feld für Feld etwas anfangen kann. Beide tragen 422.

Wo das Framework anders entscheidet

Die meisten fehlerhaften Bodys bekommen denselben Status wie von deinem Server. Diese nicht:

AnfrageKapitel 5Die FastAPI-App
ein gültiges JSON-Buch, als text/plain gekennzeichnet415422
ein gültiges JSON-Buch ohne content-type415422
JSON, das sich nicht parsen lässt400422
Bytes, die kein UTF-8 sind400400, {"detail":"There was an error parsing the body"}
ein Buch, als application/merge-patch+json gekennzeichnet415angenommen, 201
PUT /books/9999, kein solches Buch, mit kaputtem Body404422

FastAPI parst einen Body nur dann als JSON, wenn sein content-type JSON sagt: application/json oder einen application/-, dessen Untertyp auf +json endet, etwa application/merge-patch+json. Jede andere Kennzeichnung, oder gar keine, lässt den Body als Text, und das Modell lehnt Text ab, weil er kein Objekt ist; die 422 sagt, dass das Modell ein Objekt erwartet hat. Eine eigene 415 gibt es nicht. Kaputtes JSON ist ebenfalls eine 422, mit dem Typ json_invalid. Bytes, die sich nicht als UTF-8 decodieren lassen, sind das einzige Body-Problem, das FastAPI mit 400 beantwortet, und sein detail ist dort ein einfacher String.

Bei der PUT-Zeile geht es um die Reihenfolge. Dein Server hat die ID nachgeschlagen, bevor er den Body gelesen hat, also war eine unbekannte ID eine 404, egal was der Body enthielt. FastAPI prüft den Body, bevor es replace_book aufruft, also ist ein fehlerhafter Body eine 422, bevor deine Funktion irgendetwas nachschlagen kann. Ab Kapitel 7 hat dein Server außerdem vor beidem nach einem Token gefragt; diese App hat keine Tokens.

Zwei aus Kapitel 5 tauchen hier nie auf. 411 und 413 waren Framing-Arbeit in deinem read_body; TestClient sendet immer eine Länge, und die App setzt keine Größengrenze.

Ein gültiges JSON-Buch ist als text/plain gekennzeichnet. Kapitel 5 hat mit 415 geantwortet. Was antwortet die App?

PUT /books/9999, wobei kein Buch 9999 existiert, enthält einen kaputten Body. Kapitel 5 hat mit 404 geantwortet. Was antwortet die App?

201 und location

Die Zeile über create_book lautet @app.post("/books", status_code=201): Jeder Wert, den die Funktion zurückgibt, geht mit dem Status 201 hinaus. Die def-Zeile fragt außerdem nach response: Response. FastAPI übergibt ein Antwortobjekt, und jeder Header, den du darauf setzt, wird in die Antwort kopiert:

response.headers["location"] = f"/books/{book_id}"

Gib das gespeicherte Buch zurück, und FastAPI sendet es als JSON-Body der 201, mit deiner location. replace_book braucht beides nicht: Ein ersetztes Buch ist eine einfache 200.

Deine Aufgabe

Schreibe die Körper von create_book und replace_book in reading_app.py:

  1. create_book(book, response) nimmt die ID aus STATE["next_id"] und erhöht STATE["next_id"] um eins, speichert {"id": ..., "title": book.title, "author": book.author} unter dieser ID in STATE["books"], setzt location auf /books/ und die ID und gibt das gespeicherte Buch zurück.

  2. replace_book(book_id, book) löst für eine ID, die STATE["books"] nicht enthält, HTTPException(status_code=404, detail="no such book") aus. Sonst speichert es das Buch unter book_id, der ID aus der , und gibt es zurück.

Schreibe keine eigenen Prüfungen für title oder author: Das hat das Modell erledigt, und eine Regel von dir, die etwas ablehnt, das das Modell akzeptiert, etwa einen Titel mit Leerzeichen drumherum, würde die App mit ihrem eigenen Modell uneins machen. So probierst du eine Anfrage im Tab Shell aus, mit abgeschalteter Import-Warnung des Test-Clients:

python -W ignore -c "from fastapi.testclient import TestClient; import reading_app; r = TestClient(reading_app.app).post('/books', json={'title': 'Piranesi', 'author': 'Susanna Clarke'}); print(r.status_code, r.headers['location'], r.json())"

Es gibt 201 /books/3 {'id': 3, 'title': 'Piranesi', 'author': 'Susanna Clarke'} aus. Klicke dann auf Check my work. Bisher hat kein Byte der Antworten deiner App einen durchquert; Lektion 4 lässt sie für drei Anfragen unter einem echten Server laufen.

Aufgabe

Schreibe die Körper von create_book und replace_book in reading_app.py, unter die @app.post- und @app.put-Zeilen, die schon da sind.

  1. create_book(book, response) nimmt die neue ID aus STATE["next_id"] und erhöht STATE["next_id"] um eins. Es speichert {"id": ..., "title": book.title, "author": book.author} unter dieser ID in STATE["books"], setzt response.headers["location"] auf /books/, gefolgt von der ID, und gibt das gespeicherte Buch zurück. Das mitgelieferte status_code=201 macht die Antwort zu einer 201.

  2. replace_book(book_id, book) löst HTTPException(status_code=404, detail="no such book") aus, wenn STATE["books"] kein Buch mit dieser ID hat. Sonst speichert es {"id": book_id, "title": book.title, "author": book.author} unter book_id, der ID aus der , und gibt es zurück.

book ist ein BookIn, das schon geprüft hat: Lies book.title und book.author und füge keine eigenen Prüfungen dafür hinzu. Behalte jede @app-Zeile, jede def-Zeile und BookIn so, wie sie gekommen sind.

Im führt python -m pytest -q weiterhin deine Tests aus, und der einzeilige Befehl python -W ignore -c ... in der Lektion sendet ein POST.

Klicke dann auf Check my work. Es füllt STATE über reset_state mit Büchern, die du noch nicht gesehen hast, und einer next_id von 41 und sendet dann mit TestClient POST- und PUT-Anfragen an die App in diesem Prozess, mit Titeln und Autoren, die du noch nicht gesehen hast. Ein neues Buch muss eine 201 bekommen, eine location, die seine ID nennt, und das gespeicherte Buch im Body, und next_id muss weiterrücken; ein PUT muss die ID aus der URL behalten; ein PUT an eine unbekannte ID muss eine 404 bekommen. Bodys, die das Modell ablehnt, müssen die 422 von FastAPI bekommen, oder dessen 400 für Bytes, die kein sind, und nichts ändern; , die das Modell akzeptiert, etwa ein Titel mit Leerzeichen drumherum, müssen so gespeichert werden, wie sie sind. Außerdem ruft es deine zwei direkt mit BookIn- auf. Nichts aus deinem Terminal wird bewertet.