HTTP is tekst op een socket · oefening
De statusregel en de headers lezen
fetch geeft nu de kop van een antwoord terug als bytes. Met bytes kan een programma niet veel: het heeft de parse_head(head), dat de kop omzet in een
{"version": "HTTP/1.1", "status": 200, "reason": "OK", "headers": {"content-type": ["application/json"], ...}}
De statusregel
De eerste regel van de kop is de statusregel. Net als een verzoekregel heeft hij drie delen, gescheiden door spaties: de versie, de statuscode en de reason phrase, een korte omschrijving voor mensen om te lezen. De reason phrase kan zelf spaties bevatten, dus knippen bij elke spatie zou hem uit elkaar halen. split heeft precies hiervoor een tweede maxsplit, zoals in les 2: met 2 knipt het alleen bij de eerste twee spaties, en de rest van de regel blijft één stuk.
Wat drukt dit af?
print("HTTP/1.1 429 Too Many Requests".split(" ", 2))
Drie regels bepalen of parse_head een statusregel accepteert:
De versie begint met
HTTP/.De code bestaat uit precies drie cijfers. Controleer dat met
isascii()enisdigit()samen:isdigit()alleen accepteert ook tekens zoals², dieintniet kan omzetten.De reason phrase mag leeg zijn. Een server kan
HTTP/1.1 299sturen met niets na de laatste spatie, ofHTTP/1.1 299helemaal zonder laatste spatie. In het eerste geval geeftsplit(" ", 2)drie items, waarvan het laatste""is; in het tweede geeft het er twee. Hoe dan ook is de reason phrase"".
Al het andere is geen statusregel, en parse_head werpt int, zodat een aanroeper hem kan vergelijken met 200 of 404.
Van bytes naar tekst
De kop komt aan als bytes, en split(" ", 2) heeft tekst nodig. Decodeer de hele kop met "iso-8859-1" voordat je hem splitst. Die codering koppelt elke mogelijke byte aan precies één teken, dus het decoderen van een kop mislukt nooit, wat een server ook stuurt. Een kop hoort gewone ASCII te zijn, en voor ASCII-bytes is het resultaat hetzelfde als met elke andere gangbare codering. Python’s eigen http.client leest koppen op dezelfde manier.
Headerregels
Splits de kop na de statusregel bij \r\n en behandel elke regel als een line.split(":", 1), omdat een waarde zelf dubbele punten kan bevatten:
Ruim daarna beide delen op. strip() verwijdert de spaties rond de waarde, die lower() zet de naam in kleine letters, omdat headernamen niet hoofdlettergevoelig zijn: Content-Length, content-length en CONTENT-length zijn dezelfde header. Met de bibliotheek requests die je in Python in Practice gebruikte, kun je een header van een antwoord in elke schrijfwijze opzoeken; je parser bereikt hetzelfde door elke naam in één schrijfwijze op te slaan. Een regel zonder dubbele punt is geen headerregel, en parse_head werpt er ValueError voor op.
Dezelfde naam kan meer dan eens voorkomen, en een latere regel mag een eerdere niet wissen. Daarom koppelt de dictionary elke naam aan een Vary: a en daarna vary: b geeft {"vary": ["a", "b"]}.
Een server stuurde de headerregel content-LENGTH: 17. Welke opzoeking vindt hem in wat parse_head teruggeeft?
Je opdracht
Schrijf in wire.py de parse_head(head):
decodeer
headmet"iso-8859-1"en splits het bij"\r\n";splits de eerste regel met
split(" ", 2); werpValueErrorop tenzij hij minstens twee delen heeft, waarvan het eerste met"HTTP/"begint en het tweede uit precies drie ASCII-cijfers bestaat; de reason phrase is het derde deel als dat er is, en anders"";werp voor elke andere regel
ValueErrorop als hij geen dubbele punt bevat; splits hem anders bij de eerste dubbele punt, zet de naam in kleine letters en strip hem, strip de waarde, en voeg de waarde toe aan de lijst voor die naam;geef
{"version": ..., "status": ..., "reason": ..., "headers": ...}terug, met de status alsint.
Laat al het andere in wire.py zoals het is. fetch geeft de kop nog steeds als bytes terug, dus probeer parse_head op een live kop vanuit het tabblad Shell:
python -c "import wire; print(wire.parse_head(wire.fetch('GET', '/v1/items/1')))"
De eerste drie items komen uit de statusregel. Je statusregel zou HTTP/1.1 200 OK moeten luiden. Luidt hij HTTP/1.0 200 OK, dan kwam het antwoord rechtstreeks van de dienst, die in HTTP/1.0 antwoordt; parse_head accepteert beide, en hoofdstuk 9 legt uit wat de versie verandert. In de headers zou content-type ['application/json; charset=utf-8'] moeten bevatten en content-length ['53'], de grootte van de body van dit item, die fetch nog niet heeft gelezen. Als je headers server en date bevatten, komen die regels van de kleine webserver die vóór practice-api staat; als een antwoord dat je ontvangt geen van beide heeft, kwam het rechtstreeks van de dienst, en niets in deze cursus hangt ervan af.
Druk daarna op Check my work. De volgende les gebruikt content-length om precies de body te lezen die op de kop volgt.
Opdracht
Schrijf parse_head(head) in wire.py. Het krijgt een kop als bytes, zoals read_head die teruggeeft, en geeft een
"version": de versie uit de statusregel, als str, zoals"HTTP/1.1";"status": deals int;"reason": de reason phrase, in zijn geheel, inclusief spaties, of""als de statusregel er geen heeft;"headers": een dictionary die elke headernaam, in kleine letters, koppelt aan devan zijn in de volgorde waarin ze aankwamen, elke waarde ontdaan van de spaties eromheen.
Decodeer de kop met "iso-8859-1", splits de statusregel met split(" ", 2) en splits elke HTTP/ begint of waarvan de code niet uit precies drie ASCII-cijfers bestaat, en voor een headerregel zonder dubbele punt.
Laat de andere wire.py zoals ze zijn. Probeer het in de python -c "import wire; print(wire.parse_head(wire.fetch('GET', '/v1/items/1')))".
Druk daarna op Check my work. De controle roept parse_head aan op koppen die je niet hebt gezien, als bytes, zonder ValueError moeten opwerpen. Je dictionary mag extra sleutels bevatten. De controle leest je terminal niet en neemt geen contact op met practice-api.