0%

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 . 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 removes a body. The adapter writes whatever your response holds, and 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 instead of returning from each branch, and empty the body just before the one :

    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. ’s definition of 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 it only learns while producing the body. A server may therefore send 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

  1. At the top of handle, set method = request["method"]. Then route HEAD the way you route GET: on /books, method == "GET" or method == "HEAD" answers the list.

  2. Keep the reply of every branch in one variable, response, and return it once, at the end.

  3. Just before that return, set response["body"] = b"" when the is HEAD.

  4. Change the 405 to not_allowed("GET, HEAD"), since /books now 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 HEAD like GET: on /books, GET and HEAD both 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 the is HEAD, so the rule covers every path, the 404 and the 405 included.

  • Leave the headers alone: content-length on a HEAD reply is the length of the body GET would send.

  • Answer other methods on /books with not_allowed("GET, HEAD").

Run python pl_http.py head in the and compare 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.