Serve HTTP Yourself · practice
A Handler Is a Function
The adapter is ready to call handle(request, state) for every request, and handle still raises NotImplementedError. This lesson writes it, together with a helper that builds handle takes a
The response dict
Open reading_server.py. A response dict has three keys:
"status": theas an int, such as 200."headers": a dict from header name to. Write the names in lowercase, such as "content-type".compares header names without regard to case, so content-typeandContent-Typemean the same thing to every client; HTTP/2 even requires lowercase. The values are. "body": bytes, because bytes are what go onto the socket.
handle gets the books from its second state, not from a dict stored at the top of the file. sample_state() builds the two books you will see in the
Why does handle take state instead of reading a module-level dict?
Count bytes, not characters
json.dumps(data) turns data into JSON text, and .encode("utf-8") turns that text into bytes. Pass ensure_ascii=False to keep characters such as é as they are; without it, json.dumps writes é as the six characters \u00e9, which is also valid JSON, only harder to read on the wire:
The text is 25 characters, and the body is 26 bytes, because é takes two bytes in Content-Length counts bytes, exactly as it did when build_request set it for a request body in Chapter 2. Take the length of the encoded body, never of the text.
What does this print?
print(len("Café Society"), len("Café Society".encode("utf-8")))
The content type is application/json, with no charset parameter: JSON is always UTF-8 on the wire, and the definition of application/json has no such parameter. practice-api added one anyway; clients ignore it.
No length, no ending
A client reads the head up to the blank line, and then it needs to know how many body bytes follow. content-length tells it. Without one, the only way to find the end is to read until the server closes the connection. Under HTTP/1.0, which the adapter speaks, the server does close after every reply, so the end can still be found that way, and http.client reads such a reply to the close. Your wire.py from Chapter 1 refuses it instead: its read_body raises Content-Length arrived. In Chapter 9 the connection stays open after a reply, and a reply without a length leaves the client waiting for a close that never comes. Send the length on every reply that has a body.
Your task
Write
json_response(status, data). Build the body withjson.dumps(data, ensure_ascii=False).encode("utf-8")and return a response dict with that status, the headers"content-type"(the constantJSON_TYPE, already at the top of the file) and"content-length"(str(len(body))), and the body.Write the first version of
handle(request, state). Build aof every book in state["books"], by id, ascending, and returnjson_response(200, {"books": books}).SERVICE_CONTRACT.mdpromises that order. The state already keeps its books that way, because each new book gets the next, higher id, and a dict keeps its keys in the order they were added, so aforover state["books"]meets them by id, ascending. For nowhandleignores the request: everyand every target gets the list. Lesson 4 teaches it to say no.
Check it from the Shell tab. main() calls handle for three targets and prints each response dict, still with no server and no socket:
python reading_server.py
You should see three lines, for /books, /books/2, and /nowhere, each with status 200, your two headers with content-length '132', and the same body. In the body, é shows as \xc3\xa9, its two UTF-8 bytes. All three targets get the whole list because handle does not look at the target yet.
Then press Check my work. Next, a real connection reaches handle for the first time.
Task
Write two reading_server.py.
json_response(status, data)returns a response: "status"is the status it was given,"body"isjson.dumps(data, ensure_ascii=False).encode("utf-8"), and"headers"holds"content-type"set toJSON_TYPEand"content-length"set tostr(len(body)), the body’s length in bytes.handle(request, state)returnsjson_response(200, {"books": books}), wherebooksevery book in state["books"], by id, ascending. For now it answers every request this way, whatever theor target. It reads the books from stateand changes nothing in it.
Run python reading_server.py in the handle returns for three targets.
Then press Check my work. It calls json_response with statuses and data you have not seen, including text outside ASCII, and checks the status, that the body is bytes holding that data as application/json label, and that content-length equals the body’s length in bytes. It calls handle for GET /books with states of its own, with three books, two books, and none, and checks the status, the headers, the {"books": [...]} body, and that the state is unchanged afterward. Nothing from your terminal is graded, and no server is started.