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 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 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 wenngetresponse()TimeoutErrorauslöst.conn.request(method, target, body=body, headers=headers)sendet die Anfrage.bodyistNonebei einer Anfrage ohne Body.Bau innerhalb des Blocks das Header-
aus response.getheaders()mit kleingeschriebenen Namen undvon , genau wie get_jsones 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
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:
| Status | Nächste Methode |
|---|---|
| 303 | GET, egal was die Methode war (RFC 9110 erlaubt auch, ein HEAD beizubehalten) |
| 301, 302 | GET, wenn die Methode POST war; sonst dieselbe Methode |
| 307, 308 | dieselbe 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 max_hops=3 gegen den Kreis sendet es vier Anfragen und löst nach der vierten aus:
| Anfrage | Gesendet | Antwort | Hops |
|---|---|---|---|
| 1 | GET /a | 307, location /b | Hop 1 |
| 2 | GET /b | 307, location /a | Hop 2 |
| 3 | GET /a | 307, location /b | Hop 3 |
| 4 | GET /b | 307, location /a | ein 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
Sende die Anfrage mit
request_once.Wenn der Status nicht in
REDIRECTSist oder es keinlocationgibt, hör auf: Dieser Datensatz ist die Antwort.Wenn
len(history)bereits gleichmax_hopsist, löseValueErroraus.Hänge
{"status": ..., "url": ...}für diese Antwort anhistoryan.Ermittle die Methode mit
next_method. Wenn sie sich geändert hat, setz den Body aufNoneund lass dencontent-typeder Anfrage aus den Headern weg.Löse das
locationmitresolve_locationgegen 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 einPOST; andernfalls gibt esmethodunverändert zurück.request_once(method, url, *, body, headers, make_connection, timeout)teilturlmiturl_partsauf, öffnet eine Verbindung mitmake_connection(host, port, timeout=timeout)innerhalb vonwith contextlib.closing(...) as conn:, sendetconn.request(method, target, body=body, headers=headers)und gibt einmit "status","reason","headers"(kleingeschriebene Namen, jeweils mit einervon ), "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 mitrequest_onceund, solange der Status der Antwort inREDIRECTSist und sie einen Headerlocationhat: löstaus, wenn es bereits max_hopsgefolgt ist; hängt {"status": ..., "url": ...}für die Antwort an eine History-Liste an; verwendetnext_methodfür die nächste Methode und setzt, wenn sich die Methode ändert, den Body aufNoneund lässt den Headercontent-typeder Anfrage weg; löst daslocationmitresolve_locationgegen die aktuelle URL auf. Es gibt den letzten Datensatz mit hinzugefügtem"history"zurück.headers=Nonewird zu{}.
Führ python pl_http.py follow im 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 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 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.