0%

Weiterleitungen · Übung

Lass urllib für dich folgen

follow entscheidet alles, was eine offenlässt: wohin das location zeigt, welche und welcher Body dorthin gehen und wann aufgehört wird. Pythons Standardbibliothek hat einen Client, der dieselben Entscheidungen für dich trifft, urllib.request. Diese Lektion tut, was Kapitel 3 mit http.client getan hat: Sie nimmt Code, den du von Hand geschrieben hast, und übergibt die Aufgabe an eine Bibliothek, damit du genau sehen kannst, welche deiner Entscheidungen sie trifft und wo sie anders entscheidet.

Ein Opener

urllib.request sendet Anfragen über einen Opener, ein , das die Schritte enthält, die eine Anfrage durchläuft: die Verbindung herstellen, Weiterleitungen folgen, Fehlerstatus in verwandeln. urllib.request.build_opener() gibt einen mit den üblichen Schritten zurück, und opener.open(url, timeout=...) sendet ein GET und folgt allen Weiterleitungen, bevor es zurückkehrt:

import urllib.request

opener = urllib.request.build_opener()
with opener.open("http://books.test/v0/books/2", timeout=2.0) as response:
    print(response.status, response.geturl())
    body = response.read()

Die Antwort funktioniert in einer with-Anweisung, die sie schließt, wenn der Block endet; lies den Body innerhalb des Blocks. response.status ist der endgültige Status, und response.geturl() ist die endgültige , nach allen Weiterleitungen. Vielleicht kennst du urllib.request.urlopen(url): Es tut dasselbe über einen Opener, den das einmal baut und behält. Stattdessen deinen eigenen Opener zu übergeben, ist die Nahtstelle (seam), die du seit Kapitel 1 verwendest. open_url nimmt opener=None und baut nur dann einen, wenn keiner übergeben wird, so wie session=None in Python in der Praxis funktioniert hat, damit ein Test ihm einen Ersatz übergeben kann.

Wem urllib folgt

Hier sind deine Regeln aus Lektion 4 neben denen von urllib:

Anfrage und WeiterleitungDein followurllib
GET, einer der fünffolgt mit GETfolgt mit GET
HEAD, einer der fünffolgt mit HEAD, außer GET nach 303folgt mit HEAD
POST nach 301, 302 oder 303folgt mit GET, ohne Bodyfolgt mit GET, ohne Body
POST nach 307 oder 308folgt mit POST und dem Bodylöst HTTPError aus
PUT nach einem der fünfbehält PUT und den Body, außer GET nach 303löst HTTPError aus
eine WeiterleitungsschleifeValueError nach max_hops WeiterleitungenHTTPError, nach 9 Anfragen in einer über zwei Pfade

Bei HEAD nach einer 303 behält urllib ein HEAD als HEAD bei, was RFC 9110 erlaubt; dein Code sendet GET und liest einen Body, nach dem der Aufrufer nicht gefragt hat. Bei einem GET sind sich die beiden einig: Beide lösen ein relatives location auf, und beide behalten die Query. Die Grenzen von urllib kommen aus eigenen Zählern: Es gibt nach zehn Weiterleitungen auf oder wenn dieselbe URL zu oft zurückkommt. Der Unterschied liegt bei Schreibzugriffen. urllib folgt einer Anfrage mit Body nur dort, wo die Geschichte erlaubt, dass sie zu einem GET wird. Überall dort, wo sagt, dieselbe Methode und denselben Body noch einmal zu senden, und genau dafür gibt es 307 und 308, weigert sich urllib und löst stattdessen einen Fehler aus. Für eine allgemeine Bibliothek ist das eine sichere Wahl, aber ein Client, der ein PUT durch eine 308 senden muss, wie es alte Leseliste-Clients tun, kann dafür nicht das Folgen von urllib verwenden. Dein follow kann es.

opener.open sendet PUT /v0/books/2 und bekommt eine 308. Was passiert?

Fehler sind Antworten

opener.open löst bei jeder 4xx- oder 5xx-Antwort urllib.error.HTTPError aus. Sein Name sagt Fehler, aber die Antwort ist angekommen: Die Ausnahme trägt alles, was get_json in Kapitel 3 behalten hat. exc.code ist der Status, exc.geturl() die URL, die geantwortet hat, und exc.read() der Body, den du liest und dann mit exc.close() freigibst. Also fängt open_url HTTPError ab und gibt dieselbe Art von Datensatz zurück wie bei einem Erfolg. Über eine 404 entscheidet der Aufrufer, wie dir get_json beigebracht hat.

Fang genau diese Klasse ab, über das Modul geschrieben als urllib.error.HTTPError. Es ist verlockend, etwas Breiteres abzufangen, und es ist falsch. HTTPError ist eine Art von urllib.error.URLError, das eine Art von OSError ist. Wenn sich der Client überhaupt nicht verbinden kann, löst open URLError aus, mit dem ursprünglichen Fehler, etwa ConnectionRefusedError, in seinem .reason. Wenn die Verbindung aufgeht und keine Antwort rechtzeitig kommt, löst open ein bloßes TimeoutError aus, das ebenfalls ein OSError ist. Keins von beiden ist eine Antwort, und keins hat einen Status. Ein except OSError oder except Exception würde beide in Datensätze verwandeln, als hätte ein Server geantwortet, derselbe Fehler, wie in Kapitel 3 einen Timeout als 504 zu melden. open_url lässt beide durch.

open_url("http://books.test/v0/books/2") wird mit einer 308 und dann einer 200 beantwortet. Was gibt response.geturl() zurück?

Deine Aufgabe

Schreib open_url(url, *, opener=None, timeout=TIMEOUT) in wire.py:

  1. Wenn opener None ist, setz ihn auf urllib.request.build_opener().

  2. with opener.open(url, timeout=timeout) as response: gib ein mit "status" (response.status), "url" (response.geturl()) und "body" (response.read()) zurück.

  3. except urllib.error.HTTPError as exc: lies den Body mit exc.read(), ruf exc.close() auf und gib dieselben drei Schlüssel aus exc.code, exc.geturl() und dem Body zurück.

wire.py hat bereits import urllib.error und import urllib.request. Lass das so und schreib die Namen über das Modul: Eine Zeile from urllib.error import HTTPError würde Check my work daran hindern, deinem Code einen Ersatzfehler zu übergeben.

Starte dann das Hilfsprogramm:

python pl_http.py urllib

Es ruft dein open_url zweimal mit einem Opener auf, dessen Verbindungen zu deinem eigenen Server auf diesem Rechner gehen, also wird kein Name wie books.test nachgeschlagen. /v0/books/2 zeigt zwei Anfragen, 308 Permanent Redirect und dann 200 OK, und deinen Datensatz mit url: 'http://books.test/books/2'. /books/99 gibt deinen Datensatz mit status: 404 und dem Fehler-Body zurück. Dann sendet das Hilfsprogramm sein eigenes urllib-POST /v0/books, und der letzte Block lautet:

--- pl_http's own urllib POST /v0/books ---
raised HTTPError: HTTP Error 308: Permanent Redirect

Das ist urllib, das sich weigert, einen Body durch eine 308 erneut zu senden. Drück dann Check my work. Die nächste Lektion sendet Schreibzugriffe stattdessen über deine eigene Folgefunktion und beweist, dass sie und dein Server sich einig sind.

Aufgabe

Schreib open_url(url, *, opener=None, timeout=TIMEOUT) in wire.py.

  • Wenn opener None ist, bau einen mit urllib.request.build_opener(). Wenn ein Opener übergeben wird, verwende ihn und bau keinen.

  • Sende die Anfrage mit opener.open(url, timeout=timeout) in einer with-Anweisung und gib ein mit "status" (response.status), "url" (response.geturl(), die endgültige nach ) und "body" (response.read(), innerhalb des Blocks gelesen) zurück.

  • Fang urllib.error.HTTPError ab, und nur das: Lies seinen Body mit exc.read(), ruf exc.close() auf und gib dieselben drei Schlüssel aus exc.code, exc.geturl() und dem Body zurück.

  • Lass jede andere durch, etwa ein URLError für eine abgelehnte Verbindung oder ein TimeoutError.

  • Lass import urllib.error und import urllib.request, wie sie sind, und schreib urllib.error.HTTPError und urllib.request.build_opener über das , ohne Zeile from urllib.error import oder from urllib.request import.

Führ python pl_http.py urllib im aus, um zu sehen, wie urllib einer Weiterleitung deines Servers folgt, eine 404 als Datensatz zurückgibt und sich weigert, ein POST durch eine 308 erneut zu senden.

Drück dann Check my work. Es übergibt open_url einen Ersatz-Opener und setzt über das Modul Ersatzobjekte für urllib.request.build_opener und urllib.error ein. Es prüft, dass ein gefolgtes GET den endgültigen Status, die URL und den Body zurückgibt und dass die Antwort geschlossen wurde; dass eine 404 und eine 503 als Datensätze mit ihren Bodys zurückkommen und geschlossen werden; dass ein TimeoutError, ein URLError und ein anderer OSError alle open_url verlassen; dass der Timeout, standardmäßig 2.0, open erreicht; und dass build_opener nur aufgerufen wird, wenn kein Opener übergeben wird. Nichts aus deinem Terminal wird bewertet.