HTTP Is Text on a Socket · practice
Read the Status Line and Headers
fetch now returns the head of a reply as bytes. A program cannot do much with bytes: it needs the parse_head(head), which turns the head into a
{"version": "HTTP/1.1", "status": 200, "reason": "OK", "headers": {"content-type": ["application/json"], ...}}
The status line
The first line of the head is the status line. Like a request line, it has three parts separated by spaces: the version, the status code, and the reason phrase, a short description for people to read. The reason may contain spaces of its own, so cutting at every space would break it apart. split takes a second maxsplit, as in Lesson 2: with 2, it cuts at the first two spaces only, and the rest of the line stays in one piece.
What does this print?
print("HTTP/1.1 429 Too Many Requests".split(" ", 2))
Three rules decide whether a status line is one parse_head accepts:
The version starts with
HTTP/.The code is exactly three digits. Check with
isascii()andisdigit()together:isdigit()alone also accepts characters such as², whichintcannot convert.The reason may be empty. A server may send
HTTP/1.1 299with nothing after the last space, orHTTP/1.1 299with no last space at all. In the first casesplit(" ", 2)gives three items, the last one""; in the second it gives two. Either way the reason is"".
Anything else is not a status line, and parse_head raises int, so a caller can compare it with 200 or 404.
Bytes to text
The head arrives as bytes, and split(" ", 2) needs text. Decode the whole head with "iso-8859-1" before splitting it. That encoding maps every possible byte to exactly one character, so decoding a head never fails, whatever a server sends. A head is meant to be plain ASCII, and for ASCII bytes the result is the same as with any other common encoding. Python’s own http.client reads heads the same way.
Header lines
After the status line, split the head at \r\n and treat each line as a line.split(":", 1), because a value may contain colons of its own:
Then tidy both parts. strip() removes the spaces around the value, which lower() turns the name into lowercase, because header names are case-insensitive: Content-Length, content-length, and CONTENT-length are the same header. The requests library you used in Python in Practice lets you look up a response header in any casing; your parser gets the same effect by storing every name in one casing. A line without a colon is not a header line, and parse_head raises ValueError for it.
The same name may appear more than once, and a later line must not erase an earlier one. So the dictionary maps each name to a Vary: a and then vary: b gives {"vary": ["a", "b"]}.
A server sent the header line content-LENGTH: 17. Which lookup finds it in what parse_head returns?
Your task
In wire.py, write parse_head(head):
decode
headwith"iso-8859-1"and split it at"\r\n";split the first line with
split(" ", 2); raiseValueErrorunless it has at least two parts, the first starting with"HTTP/"and the second exactly three ASCII digits; the reason is the third part if there is one, and""otherwise;for every other line, raise
ValueErrorif it holds no colon; otherwise split it at the first colon, lowercase and strip the name, strip the value, and append the value to the list for that name;return
{"version": ..., "status": ..., "reason": ..., "headers": ...}, with the status as anint.
Leave everything else in wire.py as it is. fetch still returns the head as bytes, so try parse_head on a live head from the Shell tab:
python -c "import wire; print(wire.parse_head(wire.fetch('GET', '/v1/items/1')))"
The first three entries come from the status line. Your status line should read HTTP/1.1 200 OK. If it reads HTTP/1.0 200 OK, the reply came straight from the service, which answers in HTTP/1.0; parse_head accepts both, and Chapter 9 explains what the version changes. In the headers, content-type should hold ['application/json; charset=utf-8'] and content-length should hold ['53'], the size of this item’s body, which fetch has not read yet. If your headers include server and date, those lines come from the small web server that stands in front of practice-api; if a reply you receive has neither, it came from the service directly, and nothing in this course depends on them.
Then press Check my work. The next lesson uses content-length to read exactly the body that follows the head.
Task
Write parse_head(head) in wire.py. It takes a head as bytes, as read_head returns it, and returns a
"version": the version from the status line, as a str, such as"HTTP/1.1";"status": theas an int;"reason": the reason phrase, whole, spaces included, or""when the status line has none;"headers": a dictionary from each header name, in lowercase, to theof its values in the order they arrived, each stripped of the spaces around it.
Decode the head with "iso-8859-1", split the status line with split(" ", 2), and split each HTTP/ or whose code is not exactly three ASCII digits, and for a header line with no colon.
Leave the other wire.py as they are. In the python -c "import wire; print(wire.parse_head(wire.fetch('GET', '/v1/items/1')))".
Then press Check my work. The check calls parse_head on heads you have not seen, as bytes, with no ValueError. Your dictionary may hold extra keys. The check does not read your terminal or contact practice-api.