Hoofdstuk 5 · oefening
Bodies van verzoeken en een JSON-API
Lees precies de body van het verzoek
Elk verzoek dat je leeslijst tot nu toe heeft afgehandeld, eindigde bij de lege regel na de headers. Een verzoek dat een boek toevoegt of wijzigt, moet dat boek meesturen, en het doet dat zoals een antwoord een body meestuurt: na de lege regel, met Content-Length die zegt hoeveel bytes er volgen. In hoofdstuk 1 las je client een body van een antwoord aan de hand van dat getal; nu leest je server de body van een verzoek op dezelfde manier.
Open http_adapter.py en zoek de respond die je in de eerste les van hoofdstuk 4 hebt gelezen:
def respond(self):
request: dict = request_record(self)
if read_body is not None:
error = read_body(request, self.rfile.read)
if error is not None:
write_response(self, error)
return
write_response(self, handle(request, state))
Als make_handler_class een read_body krijgt, roept de adapter die aan vóór handle, met de verzoek-self.rfile.read, de functie die de bytes na de kop van de verbinding leest. Je make_server geeft nog geen read_body mee, dus elk verzoek bereikt handle met "body": b"", wat de client ook stuurde. In deze les schrijf je read_body en geef je die mee.
Wat read(n) doet
read(n) geeft bytes van de verbinding terug, te beginnen direct na de kop die http.server al heeft gelezen. Het geeft precies n bytes terug, tenzij de client eerst zijn verzendkant sluit; dan geeft het terug wat er is aangekomen, mogelijk b"". Vanbinnen is het de read_exact-body = read(length) betekent de controle len(body) < length dat de client stopte met verzenden voordat de body compleet was.
Een client kan ook minder bytes sturen en de verbinding daarna openhouden, wachtend op een antwoord dat nooit komt. De adapter geeft elke verbinding een time-out van 2 seconden, dus na twee seconden werpt read een TimeoutError op, schrijft http.server Request timed out in zijn log en krijgt de client helemaal geen antwoord. Je functie ziet dat geval nooit.
Geef altijd het getal mee. read() zonder getal leest tot de client zijn verzendkant sluit, en een client die zijn verzoek heeft gestuurd en op het antwoord wacht, doet dat nooit.
Drie weigeringen
Voordat read_body ook maar één byte leest, beslist de functie aan de hand van de headers of er een body is en hoe groot die is. request["headers"] bevat namen in kleine letters, elk met een "content-length".
Geen lengte. Een verzoek zonder lengte-header heeft geen body, dus een GET, HEAD of DELETE zonder die header is prima: geef None terug en laat request["body"] op b"" staan. POST en PUT zijn de verzoeken waarvan deze server verwacht dat ze een boek meesturen, en daarop staat hij op de lengte: als die ontbreekt, volgt 411 Length Required. De regel is eenvoudig: elke POST en PUT naar deze server stuurt Content-Length mee, en een verzoek dat niets te sturen heeft, zegt dat met content-length: 0.
Een misvormde lengte. De waarde moet precies één waarde zijn die uit ASCII-cijfers bestaat. Twee waarden zijn dubbelzinnig, en 12a of -5 is geen aantal bytes; beide krijgen 400 Bad Request. Controleer de cijfers met isascii() en isdigit(), zoals book_id_from doet, en zet daarna om met int, dat voorloopnullen accepteert. isdigit() alleen accepteert ook cijfers uit andere schriften en tekens zoals de superscript ², die int() weigert; ² kan in een header aankomen, omdat http.server
Te lang. MAX_BODY is 10000, en read_body krijgt de limiet als limit, met die waarde als standaard. Vergelijk de aangekondigde lengte met limit voordat je read aanroept, en antwoord met 413 Content Too Large als hij erboven zit. Het prikbord in Networks with Python controleerde de aangekondigde lengte van een frame op dezelfde manier, om dezelfde reden: een server die eerst leest, heeft al gewacht op, en vastgehouden, wat de client ook maar besloot te sturen.
Waarom vergelijk je de lengte met limit voordat je read aanroept?
Wat drukt dit af?
print(int("0012"), "12a".isdigit(), "١٢".isdigit(), "١٢".isascii())
Lees daarna. Als read minder bytes teruggeeft dan de lengte, is de body te vroeg geëindigd: weer 400. De controles gaan dus in deze volgorde: is er een lengte, is die goed gevormd, valt die binnen de limiet, is de hele body aangekomen?
None of de fout
read_body heeft twee soorten resultaat. Als de body in orde is, bewaart de functie de bytes in request["body"] en geeft ze None terug, en de adapter gaat door met het aanroepen van handle. Als de body niet in orde is, geeft ze een complete antwoord-dict terug, en de adapter stuurt die in plaats van handle aan te roepen. Bouw elke weigering met json_error, zodat het content-length, net als elk ander antwoord. De functies die je in les 2 en 5 schrijft, werken op dezelfde manier: None, of het antwoord dat verstuurd moet worden.
Eén gevolg: de adapter controleert de framing voordat handle het verzoek ziet, dus een POST /nowhere zonder lengte krijgt 411, niet 404. Dat staat ook in SERVICE_CONTRACT.md.
Een client kan een body ook versturen met Transfer-Encoding: chunked, in stukken die elk hun eigen grootte meedragen, en helemaal zonder Content-Length. Een POST heeft geen lengte, dus krijgt hij 411.
Jouw opdracht
Schrijf read_body(request, read, *, limit=MAX_BODY) in reading_server.py met de vier uitkomsten hierboven, en verander daarna make_server zodat die de functie aan de adapter meegeeft: http_adapter.make_handler_class(handle, state, read_body=read_body).
Je kunt de functie zonder server uitproberen in het tabblad Shell. io.BytesIO houdt bytes in het geheugen vast, en zijn read(n) gedraagt zich als dat van de verbinding:
python - <<'EOF'
import io
import reading_server
request = {"method": "POST", "target": "/books", "version": "HTTP/1.1", "headers": {"content-length": ["5"]}, "body": b""}
print(reading_server.read_body(request, io.BytesIO(b"hello, and more").read))
print(request["body"])
EOF
Het drukt None af en daarna b'hello': vijf bytes, en niets erna. Voer daarna het hulpprogramma uit:
python pl_http.py body
Het stuurt vier POST /books-verzoeken. Vóór je wijziging krijgen alle vier HTTP/1.0 405 Method Not Allowed van handle. Daarna krijgt het eerste, met een complete JSON-body, nog steeds 405, omdat handle tot les 4 geen POST accepteert, maar de adapter heeft de body eerst gelezen. Het tweede heeft geen lengte: HTTP/1.0 411 Length Required. Het derde kondigt 5000000 bytes aan en stuurt er geen: HTTP/1.0 413 Content Too Large, zonder dat er iets is gelezen. Het vierde kondigt 40 bytes aan, stuurt er 10 en sluit zijn verzendkant: HTTP/1.0 400 Bad Request.
Er komt een POST /books binnen zonder Content-Length. Welke status?
Klik daarna op Check my work. Hierna leert de server te vragen wat de bytes die hij heeft gelezen, horen te zijn.
Opdracht
Lees bodies van verzoeken aan de hand van hun lengte, en weiger de bodies die deze server niet kan afbakenen.
Schrijf in reading_server.py de read_body(request, read, *, limit=MAX_BODY). Controleer, in deze volgorde:
Geen
"content-length"inrequest["headers"]: geef een 411-antwoord terug voor eenPOSTofPUT, enNonevoor elke andere, waarbij request["body"]opb""blijft.Niet precies één
, of een waarde die niet uit ASCII-cijfers bestaat: geef een 400-antwoord terug. Een lengte groter dan
limit: geef een 413-antwoord terug, zonderreadaan te roepen.Roep
read(length)aan met de lengte. Als het minder bytes teruggeeft, geef je een 400-antwoord terug. Anders bewaar je de bytes inrequest["body"]en geef jeNoneterug.
Bouw elk foutantwoord met json_error en een eigen zin. Verander daarna make_server zodat die de functie aan de adapter meegeeft: http_adapter.make_handler_class(handle, state, read_body=read_body).
In de python pl_http.py body vier POST-verzoeken naar je server en laat zien wat er terugkwam.
Klik daarna op Check my work. Die roept read_body aan met verzoek-read die hij kan volgen, met bodies, lengtes en limieten die je nog niet hebt gezien: complete bodies, content-length: 0, ontbrekende en misvormde lengtes, lengtes net boven en precies op de limiet, en een body die te vroeg eindigt. Hij controleert dat read de lengte krijgt en nooit wordt aangeroepen vóór een 411, 400 of 413. Daarna bouwt hij je server met make_server zonder een handle bereikt en dat de 411 wordt verstuurd in plaats van dat die functie wordt aangeroepen. Niets uit je terminal wordt beoordeeld.