0%

Weiterleitungen · Übung

Folge Weiterleitungen mit einem Hop-Limit

Dein Server beantwortet GET /v0/books/2?x=1 jetzt mit 308 Permanent Redirect und location: /books/2?x=1, und http.client gibt diese Antwort zurück und hört auf. Nichts in sendet die zweite Anfrage für dich. Ob gefolgt wird, wohin genau, welche und welcher Body dorthin gehen und wann aufgegeben wird, entscheidet alles der Client. Diese Lektion schreibt einen Client, der diese Entscheidungen trifft: follow in wire.py, gebaut aus drei kleinen .

Eine Anfrage

request_once(method, url, *, body, headers, make_connection, timeout) sendet eine Anfrage und gibt einen Datensatz zurück. Du hast sie schon einmal geschrieben: Es ist get_json aus Kapitel 3, Lektion 1, mit jeder Methode, jeder und einem optionalen Body statt eines GET an practice-api.

  • url_parts(url) aus Lektion 2 liefert Host, und Ziel.

  • with contextlib.closing(make_connection(host, port, timeout=timeout)) as conn: erzeugt das Verbindungsobjekt und schließt es auf jedem Weg, auch wenn getresponse() TimeoutError auslöst.

  • conn.request(method, target, body=body, headers=headers) sendet die Anfrage. body ist None bei einer Anfrage ohne Body.

  • Bau innerhalb des Blocks das Header- aus response.getheaders() mit kleingeschriebenen Namen und von , genau wie get_json es tut, und lies den Body.

Der Datensatz hat "status", "reason", "headers", "body" und einen neuen Schlüssel, "url": die URL, an die diese Anfrage ging. Code, der folgt, braucht ihn, weil die URL, die geantwortet hat, nach einer Weiterleitung nicht mehr die ist, mit der er angefangen hat.

Die Methode nach einer Weiterleitung

Lektion 1 hat die fünf Weiterleitungsstatus danach geordnet, was sie mit der Methode machen. next_method(status, method) macht aus dieser Tabelle Code und gibt die Methode für die nächste Anfrage zurück:

StatusNächste Methode
303GET, egal was die Methode war (RFC 9110 erlaubt auch, ein HEAD beizubehalten)
301, 302GET, wenn die Methode POST war; sonst dieselbe Methode
307, 308dieselbe Methode

Wenn sich die Methode ändert, fällt auch der Body weg: Ein GET trägt keinen. Ebenso der Header content-type der Anfrage, der einen Body beschrieben hat, der nicht mehr gesendet wird. Wenn die Methode bleibt, gehen der Body und jeder Header unverändert an die neue Adresse. Genau dafür gibt es 307 und 308.

follow("PUT", "http://books.test/v0/books/2", body=...) bekommt eine 308 mit location: /books/2. Was sendet es als Nächstes?

Aufhören

Ein Server kann einen Client im Kreis schicken: /a leitet nach /b weiter und /b zurück nach /a. Ein Client, der jedem location vertraut, sendet Anfragen, bis etwas kaputtgeht. Deshalb zählt follow mit. Es nimmt max_hops, die höchste Anzahl an Weiterleitungen, denen es folgt, standardmäßig 5 (MAX_HOPS). Ein Hop ist eine gefolgte Weiterleitung. Wenn bereits max_hops Weiterleitungen gefolgt wurde und die Antwort noch eine Weiterleitung ist, löst follow aus, statt eine weitere Anfrage zu senden. Mit max_hops=3 gegen den Kreis sendet es vier Anfragen und löst nach der vierten aus:

AnfrageGesendetAntwortHops
1GET /a307, location /bHop 1
2GET /b307, location /aHop 2
3GET /a307, location /bHop 3
4GET /b307, location /aein vierter Hop würde das Limit überschreiten: ValueError

Was gibt das aus?

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)

Nur eine Antwort, die eine Weiterleitung ist, zählt. REDIRECTS oben in wire.py listet die fünf Status auf, denen follow folgt. Eine Antwort mit jedem anderen Status ist endgültig, und ebenso eine 3xx ohne Header location, die dem Client kein Ziel gibt. 304 Not Modified ist auch eine 3xx, steht aber nicht in der Liste: Sie ist eine Antwort, kein Verweis.

Der Schlüssel history

follow gibt den letzten Datensatz mit einem weiteren Schlüssel zurück, "history": einer Liste mit einem Dictionary pro gefolgter Weiterleitung, das jeweils den "status" der Weiterleitung und die "url" enthält, die sie gesendet hat. Eine leere Liste bedeutet, dass es keine Weiterleitung gab. requests hält dieselbe Information als Liste der Weiterleitungsantworten in response.history fest, was Kapitel 10 zeigt.

Hier ist die , die alles zusammensetzt:

  1. Sende die Anfrage mit request_once.

  2. Wenn der Status nicht in REDIRECTS ist oder es kein location gibt, hör auf: Dieser Datensatz ist die Antwort.

  3. Wenn len(history) bereits gleich max_hops ist, löse ValueError aus.

  4. Hänge {"status": ..., "url": ...} für diese Antwort an history an.

  5. Ermittle die Methode mit next_method. Wenn sie sich geändert hat, setz den Body auf None und lass den content-type der Anfrage aus den Headern weg.

  6. Löse das location mit resolve_location gegen die aktuelle URL auf, mach das Ergebnis zur aktuellen URL und geh zurück zu Schritt 1.

Eine Schleife while True:, die bei Schritt 2 mit break endet, passt zu dieser Form. Gegen die aktuelle URL aufzulösen, nicht gegen die erste, hat im Quiz aus Lektion 2 die dritte URL zu /books/3 gemacht.

Echte Clients tun noch etwas: Sie lassen Zugangsdaten wie einen Header Authorization weg, damit eine Anmeldung, die für einen Server gedacht ist, nie an einen anderen weitergegeben wird. RFC 9110 (Abschnitt 15.4) verlangt mehr, als Bibliotheken üblicherweise tun: Es verlangt, dass ein Client Authorization und Cookie bei jeder automatischen Weiterleitung weglässt, während Bibliotheken wie requests sie nur weglassen, wenn sich der Host ändert. Kapitel 7 kommt darauf zurück.

Deine Aufgabe

Schreib in wire.py next_method, request_once und follow. follow macht vor der ersten Anfrage aus headers=None ein {}, schließt jede Verbindung über request_once und übergibt jeder einzelnen timeout. Starte dann das Hilfsprogramm:

python pl_http.py follow

Es ruft wire.follow("GET", "http://books.test/v0/books/2", make_connection=...) auf, wobei die Verbindung, die es übergibt, zu deinem eigenen Server auf diesem Rechner geht, also wird books.test nie nachgeschlagen. Jede Anfrage und der Datensatz, den dein follow zurückgegeben hat, werden ausgegeben:

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'}]

Weggelassen sind die Zeilen mit dem Grund, den Headern und dem Body von Buch 2.

Drück dann Check my work. Dein Client und dein Server sind sich jetzt bei einer Weiterleitung einig. Die nächste Lektion übergibt dieselbe Aufgabe an eine Bibliothek und vergleicht ihre Regeln mit deinen.

Aufgabe

Schreib next_method, request_once und follow in wire.py.

  • next_method(status, method) gibt nach einer 303 für jede "GET" zurück, und nach einer 301 oder 302 für ein POST; andernfalls gibt es method unverändert zurück.

  • request_once(method, url, *, body, headers, make_connection, timeout) teilt url mit url_parts auf, öffnet eine Verbindung mit make_connection(host, port, timeout=timeout) innerhalb von with contextlib.closing(...) as conn:, sendet conn.request(method, target, body=body, headers=headers) und gibt ein mit "status", "reason", "headers" (kleingeschriebene Namen, jeweils mit einer von ), "body" (innerhalb des Blocks gelesen) und "url" (die , die es bekommen hat) zurück.

  • follow(method, url, *, body, headers, make_connection, max_hops, timeout) sendet die Anfrage mit request_once und, solange der Status der Antwort in REDIRECTS ist und sie einen Header location hat: löst aus, wenn es bereits max_hops gefolgt ist; hängt {"status": ..., "url": ...} für die Antwort an eine History-Liste an; verwendet next_method für die nächste Methode und setzt, wenn sich die Methode ändert, den Body auf None und lässt den Header content-type der Anfrage weg; löst das location mit resolve_location gegen die aktuelle URL auf. Es gibt den letzten Datensatz mit hinzugefügtem "history" zurück. headers=None wird zu {}.

Führ python pl_http.py follow im aus, um follow gegen deinen eigenen Server zu beobachten.

Drück dann Check my work. Es ruft next_method direkt auf, dann request_once und follow mit einem Ersatz für make_connection, dessen Verbindungen mit vorbereiteten Antworten antworten, auf Hosts und , die du noch nicht gesehen hast: eine relative 308 für ein PUT, eine 303 und eine 301 nach einem POST, eine 301 nach einem PUT, eine 307 nach einem POST, zwei Weiterleitungen hintereinander, deren zweites location relativ zur zweiten URL ist, ein absolutes location auf einem anderen Host, genau max_hops Weiterleitungen, eine mehr als das, eine , eine 3xx ohne location, eine 304, ein https-location und ein TimeoutError mittendrin. Es prüft bei jeder Anfrage Host, Port, Timeout, Methode, Ziel, Body und content-type, den Datensatz und seine History, dass ValueError nach genau max_hops + 1 Anfragen kommt und dass jede Verbindung geschlossen wurde. Es stoppt Code, der beim Folgen mehr als 20 Verbindungen öffnet. Nichts aus deinem Terminal wird bewertet.