Redirects · practice
Resolve a Relative Location
A server that moves a book answers with a header such as location: /books/2. That is not a Host line (Chapter 2, Lesson 4), but it would have to copy that location, and a proxy in front of it may have rewritten it; a relative value lets the client, which knows the URL it asked for, do the resolving. The rules for resolving are the ones every browser uses for links, written down in RFC 3986, section 5. This lesson writes the two location, and one that splits the result into what http.client asks for.
The urljoin function
You do not write those rules yourself. urljoin, from the same urllib.parse urlsplit, takes the URL that was requested and the location value, and returns the absolute URL they name together. It is already imported at the top of wire.py. Here are the common shapes of location, each resolved against one request URL:
from urllib.parse import urljoin
asked = "http://books.test/v0/books/2?x=1"
print(urljoin(asked, "/books/2"))
print(urljoin(asked, "../books/2"))
print(urljoin(asked, "?page=2"))
print(urljoin(asked, "//other.test/x"))
print(urljoin(asked, "http://books.test/books/2"))
The output, line by line:
http://books.test/books/2
http://books.test/v0/books/2
http://books.test/v0/books/2?page=2
http://other.test/x
http://books.test/books/2
A value starting with / replaces the whole path and query, and keeps the scheme, host and port. A value starting with // replaces the host too. A value starting with ? keeps the path and replaces only the query. An absolute URL is returned as it is. Everything else, such as ../books/2 or 3, is resolved like a file name relative to the folder the request URL points into: the path up to and including its last /. For /v0/books/2 that folder is /v0/books/, so .. steps up to /v0/ and books/2 lands back on /v0/books/2.
The folder rule has one surprise: whether the request URL ends in a slash changes the folder.
What does this print?
from urllib.parse import urljoin
print(urljoin("http://books.test/v0/books/2", "/books/2"))
print(urljoin("http://books.test/books", "2"))
The second line is http://books.test/2, not http://books.test/books/2. /books has no slash at the end, so its last segment is treated like a file name, and 2 replaces it. A server that means /books/2 should say so with a value that starts with /, which is what the reading
urljoin neither decodes nor encodes anything: %C3%A6 in a location stays %C3%A6 in the result, ready to go into a request line.
Each location against its own URL
A redirect can lead to another redirect. Each location is resolved against the URL that returned it, which after the first hop is no longer the URL you started with. A follower keeps a current URL, resolves each new location against it, and then makes the result the current URL.
A client asks for http://books.test/v0/books/2. That reply says location: /books/2. The second reply says location: 3. Which URL does the client ask for third?
Only http here
urljoin resolves any URL, including one whose scheme is https or ftp. A location may well point there: a server can send location: https://books.test/books/2. wire.py speaks plain HTTP only, as build_request has since Chapter 2, and following that redirect would mean sending the request without the encryption the server asked for. So resolve_location checks the scheme of the resolved URL, and raises http. Chapter 9 explains what https adds and why the site cannot show it.
Host, port, target
http.client does not take a URL. HTTPConnection takes a host and a port, and conn.request takes a build_request and fetch:
urlsplit(url).hostnameis the host.urlsplit(url).portis the port, orNonewhen the URL names none; then it is 80, the default forhttp.The target is the path, then
?and the query when there is one. An empty path, as inhttp://books.test, becomes/.The fragment, the part after
#, is left out. It is never sent.
A browser that follows a redirect carries the fragment of the old URL over to the new one, so the page still scrolls to the same place. Your client sends no fragment at all, so it has nothing to carry.
Your task
In wire.py, write two functions:
resolve_location(request_url, location)returnsurljoin(request_url, location), and raisesValueErrorwhen the result’s scheme is nothttp.url_parts(url)returns awith "host","port"and"target"for anhttpURL, with port 80 when the URL names none,/for an empty path, the query kept, and no fragment.
Together they take about eleven lines. Try them:
python -c "
import wire
print(wire.resolve_location('http://books.test/v0/books/2?x=1', '/books/2?x=1'))
print(wire.resolve_location('http://books.test/books/2', '3'))
print(wire.url_parts('http://books.test:8080/books/2?x=1#top'))
"
The first two lines are http://books.test/books/2?x=1 and http://books.test/books/3, and the dict has host books.test, port 8080 and target /books/2?x=1. Nothing from the
Then press Check my work. The client side can now turn any location into a request. The server side has nothing to send yet: the next lesson makes the reading list answer its old paths with redirects.
Task
Write resolve_location and url_parts in wire.py.
resolve_location(request_url, location)returns the absolutethat locationnames when it came back fromrequest_url:urljoin(request_url, location). When the scheme of that result is nothttp, such ashttpsorftp, raiseinstead. url_parts(url)returns awith three keys for an httpURL:"host", the host name fromurlsplit;"port", an int, 80 when the URL names no; and "target", the path followed by?and the query when there is one, with/for an empty path and the fragment left out.
Run the python -c lines from the lesson in the
Then press Check my work. It calls both location values you have not seen: a /, one with ../, a bare name against a URL with and without a final slash, a value that is only a query, one starting with //, an absolute http URL, and https and ftp values that must raise ValueError; then URLs with and without a port, with an empty path, with a query, and with a fragment. Each expected answer is checked exactly. Nothing from your terminal is graded.