0%

HTTP is tekst op een socket · oefening

Precies de body lezen

read_head stopt bij de lege regel, en parse_head vertelt je wat de kop zegt. Wat na de lege regel volgt, is de body, en een deel daarvan zit misschien al in reader["buffer"], omdat het in hetzelfde stuk aankwam als het einde van de kop. De vraag van deze les is waar de body eindigt. Een reader die te vroeg stopt, levert een half document af; een reader die op meer wacht dan de server stuurt, wacht tot zijn time-out.

Er zijn twee mogelijke antwoorden: lezen totdat de server de verbinding sluit, of een aantal bytes lezen dat de server heeft aangekondigd. Het eerste lijkt eenvoudiger, dus probeer dat eerst. Houd daarbij één feit in gedachten: een /1.1-verbinding blijft na een antwoord open, zodat de client er nog een verzoek over kan sturen, tenzij een van beide kanten Connection: close zegt. Hoofdstuk 9 gaat precies daarover.

Lezen totdat de server sluit

Lezen tot het sluiten is de uit Networks with Python die alles verzamelt totdat recv b"" teruggeeft:

data = b""
chunk = sock.recv(4096)
while chunk != b"":
    data = data + chunk
    chunk = sock.recv(4096)

Probeer het tegen practice-api. Het commando hieronder is python -, dat het programma leest uit de regels die volgen, tot aan de regel met EOF, dus plak het hele blok in het tabblad Shell, eerste en laatste regel inbegrepen. Het verstuurt het openingsverzoek en voert de lus uit op een met een time-out van 2 seconden:

python - <<'EOF'
import socket
import wire

sock = socket.create_connection(wire.ADDRESS, timeout=2.0)
sock.sendall(wire.make_request("GET", wire.HOOK_TARGET))
data = b""
chunk = sock.recv(4096)
while chunk != b"":
    data = data + chunk
    chunk = sock.recv(4096)
print(data)
EOF

Je zou moeten zien dat het ongeveer twee seconden wacht en stopt met TimeoutError: practice-api houdt de verbinding na zijn antwoord open, dus er komt nooit een b"". Komt in plaats daarvan de hele body in één keer terug, dan sloot de dienst de verbinding direct na het antwoord; lezen tot het sluiten werkte deze keer toevallig.

Zelfs als het werkt, kan lezen tot het sluiten niet zien of de body compleet is. Een body die helemaal aankwam en een body die werd afgekapt door een verbroken verbinding eindigen op dezelfde manier, met b"", en de lus geeft terug wat hij heeft. En een server die de verbinding openhoudt, zoals HTTP/1.1 verwacht, stuurt die b"" helemaal nooit. Je kunt alle drie de eindes zonder netwerk zien, met de gescripte sockets uit fakes.py. Het eerste script levert een heel antwoord en sluit, het tweede sluit twee bytes te vroeg, en het derde levert het hele antwoord en blijft daarna stil, zoals een open verbinding doet:

python - <<'EOF'
from fakes import scripted_socket

def read_until_close(sock):
    data = b""
    chunk = sock.recv(4096)
    while chunk != b"":
        data = data + chunk
        chunk = sock.recv(4096)
    return data

reply = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"
print(read_until_close(scripted_socket([reply])))
print(read_until_close(scripted_socket([reply[:-2]])))
print(read_until_close(scripted_socket([reply, None])))
EOF

De eerste twee regels die het afdrukt, zijn:

b'HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello'
b'HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhel'

en de derde aanroep eindigt in een waarvan de laatste regel TimeoutError: scripted silence is. Het tweede antwoord is kapot, en niets erin zegt dat. Het derde was compleet, en de reader kwam daar nooit achter.

De lengte komt eerst

Het antwoord hierboven bevat het antwoord al: Content-Length: 5. De server kondigt de grootte van de body in bytes aan in een header, voordat de body begint. Dat is het lengteprefix van het prikbord uit Networks with Python, als tekst in een geschreven in plaats van als vier bytes vóór elk frame. De reader leest de kop, vindt de lengte en leest precies zoveel bytes. Hij heeft nooit nodig dat de server sluit, en hij weet wanneer een body werd afgekapt: read_exact werpt ConnectionError op als de verbinding eerst sluit.

read_exact(reader, n), dat al in wire.py staat, doet het tellen. Het neemt eerst bytes uit reader["buffer"] en roept recv alleen aan voor wat nog ontbreekt, zodat de bodybytes die met de kop aankwamen niet verloren gaan. Volg het voor een body van 182 bytes, waarvan de eerste 100 met de kop aankwamen:

read_exact(reader, 182) begint bij de 100 bodybytes die al in de buffer staan en roept recv aan totdat het er 182 heeft.

Na read_head bevat reader["buffer"] die eerste 100 bytes, en de andere 82 zijn nog onderweg. read_exact(reader, 182) begint bij die 100 bytes, roept recv aan totdat de buffer er 182 bevat, en geeft de eerste 182 bytes terug; alles daarna blijft in de buffer.

Regels voor deze client

read_body(reader, method, headers) leest de lengte uit de geparste headers, die parse_head als opsloeg onder de naam in kleine letters content-length. Voordat het ook maar één bodybyte leest, controleert het de lengte:

  • Precies één . Een lijst met twee waarden is dubbelzinnig: de server zei twee verschillende dingen, of twee keer hetzelfde. Het protocol eist dat een client verschillende waarden weigert; deze client weigert elke herhaling.

  • Alleen cijfers. De waarde moet uit ASCII-cijfers bestaan, gecontroleerd met isascii() en isdigit(), net als de . 12a of -5 is geen aantal bytes.

  • Niet te groot. Hoogstens MAX_BODY, een miljoen bytes. Vergelijk het getal met de limiet vóór het lezen, zoals het prikbord de aangekondigde lengte van een frame met zijn limiet vergeleek: een reader die eerst leest en later controleert, heeft al ontvangen wat de server ook maar besloot te sturen.

Een lengte die een van deze regels breekt, is misvormd, en read_body werpt op.

Twee soorten antwoorden zijn geldige HTTP die deze client nog niet kan lezen. Een antwoord zonder Content-Length kan zijn body beëindigen door de verbinding te sluiten, en een antwoord met een header Transfer-Encoding, zoals Transfer-Encoding: chunked, stuurt zijn body in stukken, elk met een eigen lengte. Geen van beide is fout, maar deze client kan geen van beide bodies nog afmeten, dus read_body weigert ze allebei met ValueError en een melding die dat zegt. Hoofdstuk 9 voegt beide toe. Controleer eerst op transfer-encoding, en daarna op een ontbrekende content-length.

Bytes, geen tekens

Content-Length telt bytes, en read_exact telt bytes, dus die twee komen overeen. Decodeer de body pas als hij helemaal is aangekomen. Zoals je in Networks with Python zag, kan één teken in meerdere bytes kosten, en een stuk van een stroom kan midden in zo’n teken eindigen; stuk voor stuk decoderen zou daar mislukken, en tekens tellen in plaats van bytes zou te vroeg stoppen.

json_body(response) doet het decoderen, zodra de body compleet is: json.loads(response["body"].decode("utf-8")).

Na read_head bevat de buffer 10 bytes van de body, en Content-Length is 25. Hoeveel bytes moeten er nog van de socket komen?

Je opdracht

In wire.py:

  1. Schrijf read_body(reader, method, headers). Werp ValueError op als headers "transfer-encoding" bevat, als het geen "content-length" bevat, als "content-length" meer dan één waarde bevat, als die waarde niet uit ASCII-cijfers bestaat, of als het getal groter is dan MAX_BODY. Geef anders read_exact(reader, length) terug. De method is voor de volgende les; deze les gebruikt hem niet.

  2. Schrijf json_body(response): decodeer response["body"] met "utf-8" en geef terug wat json.loads oplevert.

  3. Maak fetch af. Parse binnen het with-blok, na het maken van de reader, de kop met parse_head(read_head(reader)), sla read_body(reader, method, response["headers"]) op onder de sleutel "body" van die , en geef de dictionary terug.

Verstuur daarna twee verzoeken vanuit het tabblad Shell:

python wire.py
python -c "import wire; print(wire.json_body(wire.fetch('GET', '/v1/items/1')))"

Het eerste drukt het hele antwoord af als dictionary, met de body als bytes; het tweede drukt het item zelf af, {'category': 'color', 'id': 1, 'name': 'blue', 'value': 10}. Beide komen terug zodra de body compleet is. Geen van beide wacht tot de server de verbinding sluit.

Druk daarna op Check my work. De volgende les verstuurt het ene verzoek waarvan het antwoord een lengte aankondigt en toch geen body heeft.

Opdracht

Schrijf in wire.py read_body en json_body, en maak fetch af.

read_body(reader, method, headers) krijgt een reader waarvan de kop is gelezen en de headers die parse_head teruggaf. Het werpt op:

  • als headers "transfer-encoding" bevat, of geen "content-length" bevat: zo’n body kan deze client nog niet afmeten;

  • als "content-length" meer dan één bevat, of een waarde die niet uit ASCII-cijfers bestaat;

  • als de lengte groter is dan MAX_BODY, voordat er een bodybyte is ontvangen.

Anders geeft het read_exact(reader, length) terug: precies het aangekondigde aantal bytes, te beginnen met de bytes die al in reader["buffer"] staan, en met elke byte na de body nog in de buffer. De method is voor de volgende les.

json_body(response) geeft json.loads(response["body"].decode("utf-8")) terug.

fetch houdt zijn with-blok. Daarbinnen zet het, na het maken van de reader, response = parse_head(read_head(reader)), daarna response["body"] = read_body(reader, method, response["headers"]), en geeft het response terug.

Voer in de python wire.py en python -c "import wire; print(wire.json_body(wire.fetch('GET', '/v1/items/1')))" uit.

Druk daarna op Check my work. De controle roept read_body aan na de kop van antwoorden die je niet hebt gezien, geleverd in één stuk, één byte tegelijk, op elke positie in tweeën geknipt (ook binnen de body en binnen een teken van twee bytes) en in stukken van 7 bytes, elke keer zowel met een server die daarna sluit als met een die de verbinding openhoudt. Op één antwoord volgt het begin van een volgend. Ze geeft read_body ook misvormde en herhaalde lengtes, een antwoord zonder Content-Length, antwoorden met Transfer-Encoding, een lengte ver boven de limiet, en een server die halverwege de body sluit. Ze roept fetch aan met een vervangende connect op een antwoord met 200 en een met 404, en json_body op een body met tekens buiten ASCII. Ze leest je terminal niet en neemt geen contact op met practice-api.