0%

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 replies. Because handle takes a and returns a dict, you can check it by calling it, with no server running, the same way you tested in Python in Practice and handed fake to your code in Networks with Python. The server and the socket come in the next lesson.

The response dict

Open reading_server.py. A response dict has three keys:

  • "status": the as 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-type and Content-Type mean 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 , but a test can pass any state it likes and then look at that same state afterward. Nothing the function depends on is hidden in a -level name, and nothing it changes can survive from one test to the next unless the test passed it in.

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:

Try it

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 when no 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

  1. Write json_response(status, data). Build the body with json.dumps(data, ensure_ascii=False).encode("utf-8") and return a response dict with that status, the headers "content-type" (the constant JSON_TYPE, already at the top of the file) and "content-length" (str(len(body))), and the body.

  2. Write the first version of handle(request, state). Build a of every book in state["books"], by id, ascending, and return json_response(200, {"books": books}). SERVICE_CONTRACT.md promises 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 a for over state["books"] meets them by id, ascending. For now handle ignores the request: every and 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 in reading_server.py.

  • json_response(status, data) returns a response : "status" is the status it was given, "body" is json.dumps(data, ensure_ascii=False).encode("utf-8"), and "headers" holds "content-type" set to JSON_TYPE and "content-length" set to str(len(body)), the body’s length in bytes.

  • handle(request, state) returns json_response(200, {"books": books}), where books every book in state["books"], by id, ascending. For now it answers every request this way, whatever the or target. It reads the books from state and changes nothing in it.

Run python reading_server.py in the to see what 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 , the 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.