requests and httpx, Now That You Know · practice
Every Feature Is a Mechanism
In Lesson 1 a session carried a wire.py or in your server. Here they are side by side:
| requests | What it does | Your version |
|---|---|---|
response.history | the redirects followed on the way, oldest first | follow and its history, Lesson 6.4 |
allow_redirects=False | return the first 3xx instead of following it | http.client, which never follows, Lesson 3.1 |
auth=(user, password) | the Authorization: Basic line | basic_header, Lesson 7.5 |
session.cookies | the jar: keep Set-Cookie, send Cookie | make_cookie_opener, Lesson 7.3 |
raise_for_status() | raise for a 4xx or 5xx status | classify and its client and server error classes, Chapter 3 |
| the session’s connection pool | several requests on one kept-open connection | fetch_many, Lesson 9.4 |
.json() | decode the body, read to the length the headers gave | read_body and json_body, Chapters 1 and 3 |
timeout=(1.0, 2.0) | a bounded connect, then bounded reads | the timeouts of Lessons 3.1 and 3.5 |
The table has no row for a cache. Keeping a response, deciding whether it is fresh, and sending If-None-Match were your cached_get in Lesson 8.5, and requests does none of it. More on that below.
Take the first two rows in code. After response = session.get("http://books.test/v0/books/2"), response.history holds the 308 response, and response.url is the final http://books.test/books/2. Each response in history has its own status_code and url, the same two facts your follow kept in its history entries. With allow_redirects=False, requests behaves like http.client: the 308 itself comes back, response.is_redirect is True, and response.next is the request it would have sent next.
Most rows behave exactly as your code did. Three do not: the 302 rule, the head() default, and the encoding auth= uses. Those are worth knowing before you trust the library.
Where the library differs from you
Your follow changes the POST; after a 307 or 308 it keeps the method and the body. requests agrees on 301, 307, and 308, and on 303 for every method except HEAD, which it keeps. On 302 it does something else: it turns every method except HEAD into GET, so a PUT answered with 302 is sent on as a GET with no body. Its POST to GET after a 301 or 302. httpx, in Lesson 3, has the same browser rule.
That is one reason your server uses 308 for the old /v0 paths: under a 308, every client keeps the method and the body.
A server answers PUT /books/2 with 302 and location: /books/2-new. What does requests send next?
The other difference is a default. session.get, post, put, and delete follow redirects unless you pass allow_redirects=False. session.head does the opposite: it returns a 3xx as it is, unless you pass allow_redirects=True.
session.head(url) meets a 308. What comes back?
Credentials are bytes
auth=(user, password) builds the same header as your basic_header: user:password, then basic_header encoded the text as
For ASCII names that changes nothing, because ASCII bytes are the same in both. For zoë it does:
What does this print?
print("zoë".encode("latin-1"), "zoë".encode("utf-8"))
Your server’s strict UTF-8 decode cannot read Latin-1’s lone \xeb, so a login as zoë gets 401 with the right password. A character Latin-1 does not have at all, such as ✓, is worse: requests raises UnicodeEncodeError before anything is sent.
The fix is to hand requests bytes, which it sends as they are:
credentials = (user.encode("utf-8"), password.encode("utf-8"))
response = session.post(base_url + "/login", auth=credentials, timeout=timeout)
httpx, in Lesson 3, encodes text credentials as UTF-8 and needs no such step.
No cache in requests
A session keeps cookies, headers, and connections. It does not keep responses. Two session.get(url) calls a second apart send two full requests, even when the first reply said cache-control: max-age=60. Revalidating is still your job: you hold the tag and send it.
response = session.get(url, headers={"if-none-match": etag}, timeout=timeout)
A 304 means your copy is still current; a 200 brings a new body and a new tag.
Two session.get(url) calls a second apart; the first reply had cache-control: max-age=60 and an etag. What does the second call send?
Your task
In side_by_side.py:
redirect_trail(response)returns one line,f"{r.status_code} {r.url}", for each response inresponse.history, oldest first, and then one forresponseitself. With no redirect, that is one line.revalidate(url, etag, *, session=None, timeout=TIMEOUT)sends oneGET urlwith the headerif-none-match: etagandtimeout=timeout, and returns the. Use client = requests if session is None else session, the line from Python in Practice, and let requests follow any redirect.Change
who_am_ito pass its credentials as UTF-8 bytes.
Then run:
python pl_http.py mechanisms
pl_http adds a user, zoë, with the password pässwörd ✓, to your server’s state. Before your change, the last part ends in requests’ refusal:
--- your who_am_i() as zoë ---
raised UnicodeEncodeError: 'latin-1' codec can't encode character '\u2713' in position 9: ordinal not in range(256)
After it, the trail shows the old path’s 308 and the book, and the tag you hold decides between 200 and 304:
--- pl_http's session.get() of /v0/books/2?x=1, then your redirect_trail() ---
value: ['308 http://books.test/v0/books/2?x=1', '200 http://books.test/books/2?x=1']
--- requests and replies ---
request 1 GET /books/2 (If-None-Match: "stale") -> 200 OK (connection 1)
--- your revalidate() of /books/2 with the tag "stale" ---
value: 200
The next part sends the book’s current tag and gets value: 304, and who_am_i as zoë returns value: 'zoë'. Press Check my work. Lesson 3 does the same jobs with httpx, whose defaults differ from requests’ in five ways.
Task
Read a redirect trail, revalidate by hand, and send credentials as
In side_by_side.py:
redirect_trail(response)returns aof : one f"{r.status_code} {r.url}"for each response inresponse.history, oldest first, then one forresponseitself.revalidate(url, etag, *, session=None, timeout=TIMEOUT)sends oneGET urlwith the headerif-none-match: etag, exactly as given, andtimeout=timeout, and returns the reply’sstatus_code. Useclient = requests if session is None else session. Let requests follow redirects, as it does by default: do not passallow_redirects=False.who_am_ipasses its credentials as UTF-8 bytes:auth=(user.encode("utf-8"), password.encode("utf-8")). Everything else in it stays as in Lesson 1.
In the python pl_http.py mechanisms adds the user zoë to your server, then runs your redirect_trail on a request for an old /v0 path, your revalidate with a stale tag and with the current one, and your who_am_i as zoë.
Then press Check my work. It runs your redirect_trail must list a 308 and the book it leads to, and keep the order of a longer history; revalidate through a 308 must get 304 for the current tag and 200 for a stale one, sending the tag and the timeout on both hops, with a session and without one. Nothing from your terminal is graded.