0%

Doorverwijzingen · oefening

Volg doorverwijzingen met een hoplimiet

Je server beantwoordt GET /v0/books/2?x=1 nu met 308 Permanent Redirect en location: /books/2?x=1, en http.client geeft dat antwoord terug en stopt. Niets in stuurt het tweede verzoek voor je. Of je volgt, waar je precies heen gaat, welke en body je daar stuurt, en wanneer je opgeeft, zijn allemaal beslissingen van de client. Deze les schrijft een client die ze neemt: follow, in wire.py, opgebouwd uit drie kleine .

Eén verzoek

request_once(method, url, *, body, headers, make_connection, timeout) stuurt één verzoek en geeft één record terug. Je hebt hem al eens geschreven: het is get_json uit hoofdstuk 3, les 1, met elke methode, elke en een optionele body in plaats van een GET naar practice-api.

  • url_parts(url) uit les 2 geeft de host, de en het target.

  • with contextlib.closing(make_connection(host, port, timeout=timeout)) as conn: maakt het verbindingsobject en sluit het op elk pad, ook als getresponse() een TimeoutError opwerpt.

  • conn.request(method, target, body=body, headers=headers) stuurt het verzoek. body is None voor een verzoek zonder body.

  • Bouw binnen het blok de header- uit response.getheaders() met namen in kleine letters en , precies zoals get_json doet, en lees de body.

Het record heeft "status", "reason", "headers", "body", en één nieuwe sleutel, "url": de URL waar dit verzoek naartoe ging. Een volger heeft die nodig, omdat na een de URL die antwoordde niet meer de URL is waarmee hij begon.

De methode na een doorverwijzing

Les 1 deelde de vijf doorverwijsstatussen in naar wat ze met de methode doen. next_method(status, method) maakt van die tabel code en geeft de methode voor het volgende verzoek terug:

StatusVolgende methode
303GET, wat de methode ook was (RFC 9110 staat ook toe dat een HEAD behouden blijft)
301, 302GET als de methode POST was; anders dezelfde methode
307, 308dezelfde methode

Als de methode verandert, gaat de body ook weg: een GET draagt er geen. Hetzelfde geldt voor de header content-type van het verzoek, die een body beschreef die niet meer wordt verstuurd. Als de methode blijft, gaan de body en elke header ongewijzigd naar het nieuwe adres. Dat is precies waar 307 en 308 voor bestaan.

follow("PUT", "http://books.test/v0/books/2", body=...) krijgt een 308 met location: /books/2. Wat stuurt hij daarna?

Stoppen

Een server kan een client in een cirkel sturen: /a verwijst door naar /b, en /b terug naar /a. Een volger die elke location vertrouwt, stuurt verzoeken tot er iets breekt. Daarom telt follow. Hij krijgt max_hops, het maximale aantal doorverwijzingen dat hij volgt, standaard 5 (MAX_HOPS). Een hop is één gevolgde doorverwijzing. Als er al max_hops doorverwijzingen zijn gevolgd en het antwoord is weer een doorverwijzing, werpt follow op in plaats van nog een verzoek te sturen. Met max_hops=3 tegen de cirkel stuurt hij vier verzoeken en werpt hij na het vierde een exception op:

VerzoekVerstuurdAntwoordHops
1GET /a307, location /bhop 1
2GET /b307, location /ahop 2
3GET /a307, location /bhop 3
4GET /b307, location /aeen vierde hop zou over de limiet gaan: ValueError

Wat drukt dit af?

moves = {"/a": "/b", "/b": "/a"}
path = "/a"
requests = 1
while path in moves and requests < 4:
    path = moves[path]
    requests = requests + 1
print(path, requests)

Alleen een antwoord dat een doorverwijzing is, telt. REDIRECTS, bovenaan wire.py, somt de vijf statussen op die follow volgt. Een antwoord met een andere status is definitief, en een 3xx zonder header location ook, want die geeft de client geen plek om heen te gaan. 304 Not Modified is ook een 3xx, maar staat niet in de lijst: het is een antwoord, geen verwijzing.

De sleutel history

follow geeft het laatste record terug met nog één sleutel, "history": een lijst met één dict per gevolgde doorverwijzing, elk met de "status" van de doorverwijzing en de "url" die hem stuurde. Een lege lijst betekent dat er geen doorverwijzing was. requests bewaart dezelfde informatie, als een lijst van de doorverwijsantwoorden, in response.history, wat hoofdstuk 10 laat zien.

Hier is de die alles samenbrengt:

  1. Stuur het verzoek met request_once.

  2. Als de status niet in REDIRECTS staat, of er geen location is, stop dan: dit record is het antwoord.

  3. Als len(history) al gelijk is aan max_hops, werp je ValueError op.

  4. Voeg {"status": ..., "url": ...} voor dit antwoord toe aan history.

  5. Bepaal de methode met next_method. Als die veranderd is, zet je de body op None en laat je de content-type van het verzoek weg uit de headers.

  6. Los de location op ten opzichte van de huidige URL met resolve_location, maak dat de huidige URL, en ga terug naar stap 1.

Een while True:-lus die bij stap 2 met break eindigt, past bij deze vorm. Oplossen ten opzichte van de huidige URL, niet de eerste, is wat de derde URL in de quiz van les 2 /books/3 maakte.

Echte clients doen nog één ding: ze laten inloggegevens zoals een header Authorization vallen, zodat een login die voor de ene server bedoeld is, nooit aan een andere wordt gegeven. RFC 9110 (sectie 15.4) vraagt meer dan bibliotheken meestal doen: het vraagt een client om Authorization en Cookie bij elke automatische doorverwijzing te laten vallen, terwijl bibliotheken zoals requests ze alleen laten vallen als de host verandert. Hoofdstuk 7 komt hierop terug.

Jouw opdracht

Schrijf in wire.py next_method, request_once en follow. follow maakt van headers=None een {} vóór het eerste verzoek, sluit elke verbinding via request_once, en geeft timeout aan elke verbinding mee. Voer daarna het hulpprogramma uit:

python pl_http.py follow

Het roept wire.follow("GET", "http://books.test/v0/books/2", make_connection=...) aan, waarbij de verbinding die het meegeeft naar je eigen server op deze machine gaat, dus books.test wordt nooit opgezocht. Elk verzoek en het record dat je follow teruggaf, worden afgedrukt:

request 1  GET /v0/books/2  ->  308 Permanent Redirect
request 2  GET /books/2  ->  200 OK
--- your client returned ---
status: 200
...
url: 'http://books.test/books/2'
history: [{'status': 308, 'url': 'http://books.test/v0/books/2'}]

De weggelaten regels zijn de reason, de headers en de body van boek 2.

Klik daarna op Check my work. Je client en je server zijn het nu eens over één doorverwijzing. De volgende les geeft dezelfde taak aan een bibliotheek en vergelijkt de regels daarvan met de jouwe.

Opdracht

Schrijf next_method, request_once en follow in wire.py.

  • next_method(status, method) geeft "GET" terug na een 303 voor elke , en na een 301 of 302 voor een POST; anders geeft hij method ongewijzigd terug.

  • request_once(method, url, *, body, headers, make_connection, timeout) splitst url met url_parts, opent één verbinding met make_connection(host, port, timeout=timeout) binnen with contextlib.closing(...) as conn:, stuurt conn.request(method, target, body=body, headers=headers), en geeft een terug met "status", "reason", "headers" (namen in kleine letters, elk met een ), "body" (gelezen binnen het blok) en "url" (de die hij kreeg).

  • follow(method, url, *, body, headers, make_connection, max_hops, timeout) stuurt het verzoek met request_once en doet, zolang de status van het antwoord in REDIRECTS staat en het een header location heeft, het volgende: werpt op als hij al max_hops heeft gevolgd; voegt {"status": ..., "url": ...} voor het antwoord toe aan een geschiedenislijst; gebruikt next_method voor de volgende methode, en zet bij een veranderde methode de body op None en laat de header content-type van het verzoek weg; lost de location op ten opzichte van de huidige URL met resolve_location. Hij geeft het laatste record terug met "history" erbij. headers=None wordt {}.

Voer python pl_http.py follow uit in de om follow tegen je eigen server aan het werk te zien.

Klik daarna op Check my work. Die roept next_method rechtstreeks aan, en daarna request_once en follow met een vervanger voor make_connection waarvan de verbindingen met vooraf vastgelegde antwoorden reageren, op hosts en die je nog niet hebt gezien: een relatieve 308 voor een PUT, een 303 en een 301 na een POST, een 301 na een PUT, een 307 na een POST, twee doorverwijzingen achter elkaar waarvan de tweede location relatief is aan de tweede URL, een absolute location op een andere host, precies max_hops doorverwijzingen, één meer dan dat, een , een 3xx zonder location, een 304, een https-location, en een TimeoutError halverwege. Hij controleert bij elk verzoek de host, poort, time-out, methode, target, body en content-type, het record en de geschiedenis ervan, dat ValueError na precies max_hops + 1 verzoeken komt, en dat elke verbinding werd gesloten. Hij stopt een volger die meer dan 20 verbindingen opent. Niets uit je terminal wordt beoordeeld.