Serve HTTP Yourself · practice
Answer HEAD Like GET
In Chapter 1 your read_body returned b"" after a HEAD request without reading anything, because a reply to HEAD has no body even when its head announces a length. That was the client trusting the server. Now you are the server, and your handle answers HEAD with 405, because Lesson 4 allowed only GET. Run the scenario for this lesson, a GET /books and then a HEAD /books:
python pl_http.py head
The GET gets the HEAD gets 405 Method Not Allowed with allow: GET, and under that reply pl_http prints one more line, which counts the bytes that followed the reply and shows them: your error body. pl_http, like any client, reads no body after HEAD, so those bytes are extra; they arrived because nothing between your handle and the http.server sends it, after HEAD too. Sending no body after HEAD is your job.
One rule at the end of handle
HEAD asks for exactly what GET would get, without the body: the same status and the same headers. A client uses it to see whether something exists, or how big it is, without downloading it. So HEAD /books is a 200 with the list’s headers, and HEAD /nowhere is a 404 with the error reply’s headers.
The simplest way to get all of that right is one rule at the end of handle, after routing. Route HEAD exactly like GET, keep the response in a
if method == "HEAD":
response["body"] = b""
return response
Every branch above it, the list, the 404, and the 405, then answers HEAD the way it answers GET, without the body. A rule written inside the /books branch alone would still send the 404’s body after HEAD /nowhere.
Why put the HEAD rule after routing instead of inside the /books branch?
The length still describes GET
The rule empties the body and leaves the headers alone, so content-length stays what it was: the length of the body GET would send. HEAD (RFC 9110, section 9.3.2) asks a server to send the header fields it would have sent for GET, and lets it leave out fields whose content-length on a HEAD reply or leave it out, but if it sends one, the value must be the length GET would send (section 8.6). This server’s rule, written in SERVICE_CONTRACT.md, is to send it, because that is how a client learns the size without the download. A length of 0 would claim that the list is empty.
The client side is already safe. http.client reads no body after HEAD, whatever the length says, and so did your wire.py. A body sent anyway stays unread in the connection. Under HTTP/1.0 the server closes after one reply and the stray bytes vanish with the connection. From Chapter 9 one connection carries several requests, and those bytes would be read as the start of the next reply.
GET /books sends 132 body bytes. What content-length does HEAD /books carry on this server?
Your task
At the top of
handle, setmethod = request["method"]. Then routeHEADthe way you routeGET: on/books,method == "GET" or method == "HEAD"answers the list.Keep the reply of every branch in one variable,
response, and return it once, at the end.Just before that
return, setresponse["body"] = b""when theis HEAD.Change the 405 to
not_allowed("GET, HEAD"), since/booksnow supports both.
Run python pl_http.py head again and compare the two server -> client lines. The status line and every header are the same in both, content-length: 132 included. The GET reply goes on with the body; the HEAD reply stops at the blank line, and no line follows it.
Then press Check my work. Your server can list the books and answer HEAD, but it cannot show one book yet. The project lesson adds /books/{id}.
Task
Extend handle in reading_server.py so that HEAD gets exactly what GET would get, without the body.
Route
HEADlikeGET: on/books,GETandHEADboth answer the. Keep each branch’s reply in one
and return it once, at the end of handle. Just before that, set the body to b""when theis HEAD, so the rule covers every path, the 404 and the 405 included.Leave the headers alone:
content-lengthon aHEADreply is the length of the bodyGETwould send.Answer other methods on
/bookswithnot_allowed("GET, HEAD").
Run python pl_http.py head in the GET and HEAD replies.
Then press Check my work. It calls handle with states of its own and compares each HEAD reply with the GET reply for the same target: for /books, for an encoded /books with a query, for an empty list, and for paths that answer 404. Each HEAD reply must have the same status and the same headers as the GET reply, with content-length equal to the length of GET’s body, and an empty body. GET replies must keep their bodies, and DELETE, PUT, and POST on /books must answer 405 with an allow header naming both GET and HEAD. Nothing from your terminal is graded, and no server is started.