0%

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 zijn, een regel tekst, of het begin van iets anders. De verzender zegt wat ze zijn in de header 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- begint met een mediatype, type/subtype, en kan na een puntkomma toevoegen:

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 (RFC 8259, sectie 8.1). Clients voegen 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: die eindigen op +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- zijn headernamen in kleine letters en bevat elke naam een waarden, dus het label is request["headers"]["content-type"][0]. Een verzoek kan helemaal geen label hebben; request["headers"].get("content-type", [""]) geeft dan een lijst met een lege , die met niets overeenkomt.

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 , dus één except ValueError: vangt ze allebei op:

Try it

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:

  1. Haal het mediatype uit de content-type van het verzoek, zoals hierboven. Als het niet application/json is, geef je een 415-antwoord terug dat met json_error is gebouwd.

  2. Roep json_body(request) aan binnen try. Als die ValueError opwerpt, geef je een 400-antwoord terug.

  3. Geef anders None terug.

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 behandelt.

Schrijf in reading_server.py de body_error(request):

  1. Haal het mediatype uit de content-type van het verzoek: het deel van de eerste vóór een eventuele ;, zonder spaties eromheen en in kleine letters. Een verzoek zonder content-type heeft geen mediatype. Als het mediatype niet precies application/json is, geef je een 415-antwoord terug.

  2. Roep de meegeleverde json_body(request) aan binnen try. Als die opwerpt, geef je een 400-antwoord terug.

  3. Geef anders None terug.

Bouw beide foutantwoorden met json_error en een eigen zin. Vang alleen ValueError op, niet Exception.

In de drukt het script uit de les af wat 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 , labels die alleen json bevatten, helemaal geen label, bytes die geen zijn, en tekst die geen JSON is. Hij controleert dat het label vóór de bytes wordt beoordeeld, dat een fout uit je eigen code niet in een 400 verandert, en dat een headernaam met ongebruikelijke hoofdletters je functie via de adapter nog steeds bereikt. Niets uit je terminal wordt beoordeeld.