Chapter 5 · practice
Request Bodies and a JSON API
Read Exactly the Request Body
Every request your reading Content-Length saying how many bytes follow. In Chapter 1 your client read a response body by that number; now your server reads a request body the same way.
Open http_adapter.py and find the respond
def respond(self):
request: dict = request_record(self)
if read_body is not None:
error = read_body(request, self.rfile.read)
if error is not None:
write_response(self, error)
return
write_response(self, handle(request, state))
When make_handler_class is given a read_body handle, with the request self.rfile.read, the function that reads the bytes after the head from the connection. Your make_server passes no read_body yet, so every request reaches handle with "body": b"", whatever the client sent. This lesson writes read_body and passes it in.
What read(n) does
read(n) returns bytes from the connection, starting right after the head that http.server has already read. It returns exactly n bytes unless the client closes its sending side first; then it returns whatever arrived, possibly b"". Inside, it is the read_exact body = read(length), the check len(body) < length means the client stopped sending before the body was complete.
A client can also send fewer bytes and then keep the connection open, waiting for an answer that never comes. The adapter gives every connection a 2-second timeout, so after two seconds read raises TimeoutError, http.server writes Request timed out to its log, and the client gets no reply at all. Your function never sees that case.
Always pass the number. read() with no number reads until the client closes its sending side, and a client that has sent its request and is waiting for the reply never does.
Three refusals
Before reading a single byte, read_body decides from the headers whether there is a body and how big it is. request["headers"] holds lowercase names, each with a list of values, so the length is under "content-length".
No length. A request with no length header has no body, so a GET, HEAD, or DELETE without one is fine: return None and leave request["body"] as b"". POST and PUT are the requests this server expects to carry a book, and on those it insists on the length: a missing one gets 411 Length Required. The rule is simple: every POST and PUT to this server carries Content-Length, and a request with nothing to send says so with content-length: 0.
A malformed length. The 12a or -5 is not a number of bytes; both get 400 Bad Request. Check digits with isascii() and isdigit(), as book_id_from does, then convert with int, which accepts leading zeros. isdigit() alone also accepts digits from other scripts and symbols such as the superscript ², which int() refuses; ² can arrive in a header, because http.server decodes
Too long. MAX_BODY is 10000, and read_body takes the limit as its limit limit before calling read, and answer 413 Content Too Large when it is over. The notice board in Networks with Python checked a frame’s announced length the same way, for the same reason: a server that reads first has already waited for, and held, whatever the client chose to send.
Why compare the length with limit before calling read?
What does this print?
print(int("0012"), "12a".isdigit(), "١٢".isdigit(), "١٢".isascii())
Then read. If read returns fewer bytes than the length, the body ended early: 400 again. So the checks go in this order: is a length there, is it well formed, is it within the limit, did the whole body arrive?
None or the error
read_body has two kinds of result. When the body is fine, it stores the bytes in request["body"] and returns None, and the adapter goes on to call handle. When it is not, it returns a complete response dict, and the adapter sends that instead of calling handle. Build each refusal with json_error, so it is content-length like every other reply. The functions you write in Lessons 2 and 5 work the same way: None, or the response to send.
One consequence: the adapter checks the framing before handle sees the request, so a POST /nowhere without a length gets 411, not 404. SERVICE_CONTRACT.md says so too.
A client can also send a body with Transfer-Encoding: chunked, in pieces that each carry their own size, and no Content-Length at all. An POST has no length, so it gets 411.
Your task
Write read_body(request, read, *, limit=MAX_BODY) in reading_server.py with the four outcomes above, then change make_server to pass it to the adapter: http_adapter.make_handler_class(handle, state, read_body=read_body).
You can try the function in the Shell tab without a server. io.BytesIO holds bytes in memory, and its read(n) behaves like the connection’s:
python - <<'EOF'
import io
import reading_server
request = {"method": "POST", "target": "/books", "version": "HTTP/1.1", "headers": {"content-length": ["5"]}, "body": b""}
print(reading_server.read_body(request, io.BytesIO(b"hello, and more").read))
print(request["body"])
EOF
It prints None and then b'hello': five bytes, and nothing after them. Then run the helper:
python pl_http.py body
It sends four POST /books requests. Before your change, all four get HTTP/1.0 405 Method Not Allowed from handle. After it, the first, with a complete JSON body, still gets 405, because handle accepts no POST until Lesson 4, but the adapter has read its body first. The second has no length: HTTP/1.0 411 Length Required. The third announces 5000000 bytes and sends none: HTTP/1.0 413 Content Too Large, with nothing read. The fourth announces 40 bytes, sends 10, and closes its sending side: HTTP/1.0 400 Bad Request.
A POST /books arrives with no Content-Length. Which status?
Then press Check my work. Next, the server learns to ask what the bytes it read are supposed to be.
Task
Read request bodies by their length, and refuse the ones this server cannot frame.
In reading_server.py, write read_body(request, read, *, limit=MAX_BODY). Check, in this order:
No
"content-length"inrequest["headers"]: return a 411 response for aPOSTorPUT, andNonefor any other, leaving request["body"]asb"".Not exactly one
, or a value that is not ASCII digits: return a 400 response. A length greater than
limit: return a 413 response, without callingread.Call
read(length)with the length. If it returns fewer bytes, return a 400 response. Otherwise store the bytes inrequest["body"]and returnNone.
Build every error reply with json_error and a sentence of your own. Then change make_server to pass the http_adapter.make_handler_class(handle, state, read_body=read_body).
In the python pl_http.py body sends four POST requests to your server and shows what came back.
Then press Check my work. It calls read_body with request read it can watch, using bodies, lengths, and limits you have not seen: complete bodies, content-length: 0, missing and malformed lengths, lengths just over and exactly at the limit, and a body that ends early. It checks that read gets the length and is never called before a 411, 400, or 413. Then it builds your server with make_server without opening a handle and that the 411 goes out instead of calling it. Nothing from your terminal is graded.