Bodies van verzoeken en een JSON-API · oefening
Zeg wat de body is
read_body geeft handle nu precies de bytes die de client stuurde. Bytes alleen zeggen niet wat ze zijn: {"title": "Dune"} kan Content-Type, dezelfde header die je server al op elk antwoord zet dat hij verstuurt, content-type: application/json. Voordat je server een body als JSON behandelt, controleert hij twee dingen: dat de client hem als JSON heeft gelabeld, en dat de bytes echt JSON zijn.
Het mediatype
Een Content-Type-type/subtype, en kan na een puntkomma
Content-Type: application/json
Content-Type: Application/JSON; charset=utf-8
Beide regels labelen de body als application/json. Namen van mediatypes zijn niet hoofdlettergevoelig, en charset=utf-8 is een parameter, geen deel van het type. Het mediatype is dus het deel vóór de eerste ;, zonder spaties eromheen, vergeleken in kleine letters.
application/json definieert helemaal geen parameter charset: JSON die tussen systemen wordt uitgewisseld, is charset=utf-8 toch toe, en dat kan geen kwaad, dus deze server negeert elke parameter en decodeert JSON altijd als UTF-8.
Wat drukt dit af?
print("Application/JSON; charset=UTF-8".split(";")[0].strip().lower())
Vergelijk het hele mediatype, niet een stuk ervan. "json" in value accepteert ook application/jsonx, en startswith("application/json") accepteert application/json-patch+json. Dat laatste is een echt mediatype: +json (RFC 6839) zijn JSON met eigen extra regels. Deze server accepteert precies application/json en weigert de rest. Dat is zijn keuze, vastgelegd in SERVICE_CONTRACT.md, geen regel van
In de verzoek-request["headers"]["content-type"][0]. Een verzoek kan helemaal geen label hebben; request["headers"].get("content-type", [""]) geeft dan een lijst met een lege
415 of 400
Er kunnen twee verschillende dingen mis zijn, en elk heeft een eigen status:
Het label is verkeerd of ontbreekt. De server accepteert dat mediatype niet, wat de bytes ook zijn: 415 Unsupported Media Type. Hij kijkt nooit naar de bytes.
Het label klopt, de bytes niet. De body is geen UTF-8, of het is geen geldige JSON: 400 Bad Request, een misvormd verzoek.
De twee statussen vertellen de client dat hij verschillende dingen moet herstellen. Na een 415 stuurt hij een formaat dat deze server accepteert, met het juiste label; na een 400 herstelt hij de bytes die hij stuurt. Een server die beide met 400 beantwoordt, laat de client raden wat er misging.
json_body(request) staat al in reading_server.py: die geeft json.loads(request["body"].decode("utf-8")) terug. Daarin kunnen twee stappen mislukken. decode werpt UnicodeDecodeError op bij bytes die geen UTF-8 zijn, en json.loads werpt json.JSONDecodeError op bij tekst die geen JSON is. Beide zijn subklassen van except ValueError: vangt ze allebei op:
Dat drukt {'title': 'Dune'} af, dan JSONDecodeError en dan UnicodeDecodeError. Houd de except zo smal. except Exception: zou ook een fout in je eigen code, zoals een verkeerd gespelde sleutel, veranderen in een 400 die de client de schuld geeft.
Decodeer niet als Latin-1 om de fout te laten verdwijnen. Elke byte is een Latin-1-teken, dus die decodering mislukt nooit, en bytes die geen UTF-8 zijn, zouden als de verkeerde tekst worden gelezen in plaats van te worden geweigerd.
Een body is geldige JSON, maar gelabeld als text/plain. Welke status?
Jouw opdracht
Schrijf body_error(request) in reading_server.py:
Haal het mediatype uit de
content-typevan het verzoek, zoals hierboven. Als het nietapplication/jsonis, geef je een 415-antwoord terug dat metjson_erroris gebouwd.Roep
json_body(request)aan binnentry. Als dieValueErroropwerpt, geef je een 400-antwoord terug.Geef anders
Noneterug.
Net als read_body geeft deze None terug of het antwoord dat verstuurd moet worden. Elk foutantwoord komt van json_error, dus het draagt zijn eigen content-type: application/json, ook als het het label van de client weigert.
Tot les 4 roept geen enkele route body_error aan, dus probeer de functie uit in het tabblad Shell. make_request in tests/test_server.py bouwt een verzoek-dict met een JSON-body, een content-type en een lengte; het script verandert het label van het ene verzoek en de bytes van een ander:
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
Het drukt None, 415 en 400 af.
Klik daarna op Check my work. In de volgende les stap je even weg van de code om te beslissen waar een nieuw boek terechtkomt en wat elke schrijfactie als antwoord geeft.
Opdracht
Controleer wat de body van een verzoek beweert te zijn voordat je hem als
Schrijf in reading_server.py de body_error(request):
Haal het mediatype uit de
content-typevan het verzoek: het deel van de eerstevóór een eventuele ;, zonder spaties eromheen en in kleine letters. Een verzoek zondercontent-typeheeft geen mediatype. Als het mediatype niet preciesapplication/jsonis, geef je een 415-antwoord terug.Roep de meegeleverde
json_body(request)aan binnentry. Als dieopwerpt, geef je een 400-antwoord terug. Geef anders
Noneterug.
Bouw beide foutantwoorden met json_error en een eigen zin. Vang alleen ValueError op, niet Exception.
In de body_error teruggeeft voor drie verzoeken die met make_request uit tests/test_server.py zijn gebouwd.
Klik daarna op Check my work. Die roept body_error aan met labels en bodies die je nog niet hebt gezien: application/json met andere hoofdletters en met json bevatten, helemaal geen label, bytes die geen