Redirects · practice
Send the Old Path Somewhere New
The client side can now resolve any location. This lesson gives it something to resolve: the reading /v0/books paths with redirects. First, see what an old client gets today. Run the helper’s moved scenario, which sends three requests an old or careless client might send:
python pl_http.py moved
The requests are GET /v0/books/2?x=1, a PUT /v0/books/2 with a GET /books/ with a slash at the end. Each reply starts with HTTP/1.0 404 Not Found. For the first two, nothing exists at /v0 any more; for the third, path_segments gives ["books", ""], and an empty segment is not a book id. All three ask for things the server has, at addresses it no longer answers.
308 for every method
SERVICE_CONTRACT.md says what to send instead. Every request for /v0/books or a path below it, such as /v0/books/2, gets a 308 Permanent Redirect, whatever its PUT or POST must reach the new path unchanged, with its body. The reply carries the new address in location, content-length: 0, and an empty body.
It is tempting to do the work right away instead: the PUT is right there, with a valid book in its body, so why not store it and answer 200? Because then the server has two paths that write, and every rule you add to /books/{id} from now on (Chapter 7 adds who may write at all) has to be added to /v0 as well, or the old path becomes a way around it. A redirect keeps one path that does the work. The client sends the request again to the new path, and only that request changes anything. A redirect answer changes no state at all.
The new branch goes at the top of handle, before the routes you already have, so it answers every method the same way and no method check runs first.
Keep the rest
An old client that asked for /v0/books/7?draft=1 wants book 7, with the same query. The location has to keep everything after /v0: the rest of the path and the query. If it says /books, an old PUT meant for book 7 lands on the collection and gets a 405; if it loses ?draft=1, the request quietly asks for something else.
legacy_location(target) builds that urlsplit(target).path, cut the first three characters, /v0, with a ? and the query when there is one. Use the target itself, not the decoded segments from path_segments: the target is already percent-encoded, so it is plain ASCII and fits in a header. A header value must fit in Latin-1 to be written at all, and http.server raises UnicodeEncodeError on a location built from a decoded segment such as ✓, before a single byte of the reply is sent. Because the value starts with /, it is relative: the client resolves it against the resolve_location does, and the server does not have to copy the Host line into every location.
PUT /v0/books/7?draft=1 arrives. What location does the server send?
A slash too many
/books/ is a typo for /books, and a common one. The contract answers it with a 301 Moved Permanently to /books. 301 fits a tidy-up meant for reads: a GET for /books/ becomes a GET for /books. A client that sent a POST to /books/ may arrive as a GET after a 301, so its body is lost; a write belongs on the exact path, and the contract asks for 308 only where old clients write. Write the branch for exactly the path /books/, which path_segments turns into ["books", ""]. Nothing else redirects: /books/7/ stays a 404, and so does any /v0 path that is not /v0/books or below it.
redirect(status, location) builds both replies: the status, a location header, content-length set to "0", and the body b"". A redirect has nothing to say in its body, and content-length: 0 tells the client so, framing it like every other reply.
Your task
In reading_server.py:
Write
redirect(status, location).Write
legacy_location(target).At the top of
handle, before the/booksbranches, add two branches: a path whose first two segments arev0andbooksgetsredirect(308, legacy_location(...))with the request’s target, and the segments["books", ""]getredirect(301, "/books"). Leave every other route as it is.
Run the scenario again:
python pl_http.py moved
Each reply now carries a redirect. Read its location line:
server -> client b'HTTP/1.0 308 Permanent Redirect\r\nServer: ...\r\nDate: ...\r\nlocation: /books/2?x=1\r\ncontent-length: 0\r\n\r\n'
server -> client b'HTTP/1.0 308 Permanent Redirect\r\nServer: ...\r\nDate: ...\r\nlocation: /books/2\r\ncontent-length: 0\r\n\r\n'
server -> client b'HTTP/1.0 301 Moved Permanently\r\nServer: ...\r\nDate: ...\r\nlocation: /books\r\ncontent-length: 0\r\n\r\n'
The Server and Date values are left out here; yours name your Python version and the time of the run. The log block lists the three requests with 308, 308 and 301. The PUT changed nothing on the server: only a request that reaches /books/2 can change book 2.
pl_http sends these requests itself and reads exactly what comes back, so it stops at each redirect. So does http.client: asked for /v0/books/2?x=1, it returns the 308 and goes no further. Following is the client’s job, and the next lesson writes the client that does it.
Then press Check my work.
Task
Answer the old paths in reading_server.py with redirects.
redirect(status, location)returns a responsewith that status, a locationheader holdinglocation,content-lengthset to"0", and the bodyb"".legacy_location(target)returns where a/v0/books...target lives now: the path without its leading/v0, then?and the query when there is one. Build it from the target as it arrived, still percent-encoded.At the top of
handle, before the other routes: anyon a path whose first two segments are v0andbooksgetsredirect(308, legacy_location(request["target"])), and the segments["books", ""](the path/books/) getredirect(301, "/books").A redirect changes nothing in
state. Every other path, including/books/7/and/v0/other, keeps the reply it had.
Run python pl_http.py moved in the location line.
Then press Check my work. It calls redirect and legacy_location directly, then calls handle with a state and book ids you have not seen: GET, HEAD, POST, PUT and DELETE on old paths with and without a query (a PUT and a POST carry a valid book), /books/, paths that must not redirect, and the Chapter 5 routes. It checks the status, that location keeps the id, the rest of the path and the query exactly (an absolute http:// content-length 0, the empty body, and that the state did not change. It also writes one redirect through http_adapter in memory and reads the bytes. Nothing from your terminal is graded.