Redirects · practice
Let urllib Follow for You
follow decides everything a redirect leaves open: where the location points, which urllib.request. This lesson does what Chapter 3 did with http.client: it takes code you wrote by hand and hands the job to a library, so you can see exactly which of your decisions it makes, and where it decides differently.
An opener
urllib.request sends requests through an opener, an urllib.request.build_opener() returns one with the usual steps in place, and opener.open(url, timeout=...) sends a GET and follows any redirects before it returns:
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()
The response works in a with statement, which closes it when the block ends; read the body inside the block. response.status is the final status, and response.geturl() is the final urllib.request.urlopen(url): it does the same through an opener the open_url takes opener=None and builds one only when none is given, the way session=None worked in Python in Practice, so a test can hand it a stand-in.
What urllib follows
Here are your rules from Lesson 4 beside urllib’s:
| Request and redirect | Your follow | urllib |
|---|---|---|
GET, any of the five | follows with GET | follows with GET |
HEAD, any of the five | follows with HEAD, except GET after 303 | follows with HEAD |
POST after 301, 302 or 303 | follows with GET, no body | follows with GET, no body |
POST after 307 or 308 | follows with POST and the body | raises HTTPError |
PUT after any of the five | keeps PUT and the body, except GET after 303 | raises HTTPError |
| a redirect | ValueError after max_hops redirects | HTTPError, after 9 requests in a two-path loop |
On HEAD after a 303, urllib keeps a HEAD a HEAD, which RFC 9110 allows; your follower sends GET and reads a body the caller did not ask for. For a GET the two agree: both resolve a relative location, and both keep the query. urllib’s limits come from its own counters: it gives up after ten redirects, or when the same URL comes back too often. The difference is in writes. urllib follows a request with a body only where history lets it turn into a GET. Wherever PUT through a 308, as old reading follow can.
opener.open sends PUT /v0/books/2 and gets a 308. What happens?
Errors are replies
opener.open raises urllib.error.HTTPError for any 4xx or 5xx reply. Its name says error, but the reply arrived: the exception carries everything get_json kept in Chapter 3. exc.code is the status, exc.geturl() the URL that answered, and exc.read() the body, which you read and then release with exc.close(). So open_url catches HTTPError and returns the same kind of record as for a success. A 404 is something the caller decides about, as get_json taught you.
Catch exactly that class, written through the module as urllib.error.HTTPError. It is tempting to catch something wider, and it is wrong. HTTPError is a kind of urllib.error.URLError, which is a kind of OSError. When the client cannot connect at all, open raises URLError, with the original error, such as ConnectionRefusedError, in its .reason. When the connection opens and no reply comes in time, open raises a bare TimeoutError, which is also an OSError. Neither is a reply, and neither has a status. An except OSError or except Exception would turn both into records as if a server had answered, the same mistake as reporting a timeout as a 504 in Chapter 3. open_url lets both through.
open_url("http://books.test/v0/books/2") is answered with a 308 and then a 200. What does response.geturl() return?
Your task
Write open_url(url, *, opener=None, timeout=TIMEOUT) in wire.py:
When
openerisNone, set it tourllib.request.build_opener().with opener.open(url, timeout=timeout) as response:return awith "status"(response.status),"url"(response.geturl()) and"body"(response.read()).except urllib.error.HTTPError as exc:read the body withexc.read(), callexc.close(), and return the same three keys fromexc.code,exc.geturl()and the body.
wire.py already has import urllib.error and import urllib.request. Keep them that way, and write the names through the module: a from urllib.error import HTTPError line would stop Check my work from handing your code a stand-in error.
Then run the helper:
python pl_http.py urllib
It calls your open_url twice with an opener whose connections go to your own server on this machine, so no name such as books.test is looked up. /v0/books/2 shows two requests, 308 Permanent Redirect and then 200 OK, and your record with url: 'http://books.test/books/2'. /books/99 returns your record with status: 404 and the error body. Then the helper sends its own urllib POST /v0/books, and the last block reads:
--- pl_http's own urllib POST /v0/books ---
raised HTTPError: HTTP Error 308: Permanent Redirect
That is urllib refusing to resend a body through a 308. Then press Check my work. The next lesson sends writes through your own follower instead, and proves that it and your server agree.
Task
Write open_url(url, *, opener=None, timeout=TIMEOUT) in wire.py.
When
openerisNone, build one withurllib.request.build_opener(). When an opener is given, use it and build none.Send the request with
opener.open(url, timeout=timeout)in awithstatement, and return awith "status"(response.status),"url"(response.geturl(), the finalafter redirects) and "body"(response.read(), read inside the block).Catch
urllib.error.HTTPError, and only that: read its body withexc.read(), callexc.close(), and return the same three keys fromexc.code,exc.geturl()and the body.Let every other
through, such as a URLErrorfor a refused connection or aTimeoutError.Keep
import urllib.errorandimport urllib.requestas they are, and writeurllib.error.HTTPErrorandurllib.request.build_openerthrough the, with no from urllib.error importorfrom urllib.request importline.
Run python pl_http.py urllib in the POST through a 308.
Then press Check my work. It hands open_url a stand-in opener and puts stand-ins for urllib.request.build_opener and urllib.error in place through the module. It checks that a followed GET returns the final status, URL and body and that the response was closed; that a 404 and a 503 come back as records with their bodies and are closed; that a TimeoutError, a URLError and another OSError all leave open_url; that the timeout, 2.0 by default, reaches open; and that build_opener is called only when no opener is given. Nothing from your terminal is graded.