Serve HTTP Yourself · capstone
Capstone project: Project: A Read-Only Reading List
Your server HEAD the way it answers GET. A client still cannot ask for one book. This project adds that route and finishes the read-only reading list that SERVICE_CONTRACT.md in your workspace describes.
What the service promises
Open SERVICE_CONTRACT.md. Its table is the requirement:
| Request | Response |
|---|---|
GET /books | 200, {"books": [every book, by id, ascending]} |
GET /books/{id} | 200, the book; 404 when no book has that id |
HEAD on either | the status and headers GET would send, and no body |
| another | 405 with allow: GET, HEAD |
| any other path | 404 |
Every reply with a body is content-length, and every error body is {"error": "..."}. The first, third, fourth, and fifth rows already hold for /books. The new work is the second row, plus the HEAD and 405 rows for /books/{id}. {id} stands for one path segment: /books/2 asks for the book whose id is 2.
An id is digits
The segment arrives as text, "2", and the keys of state["books"] are ints, so "2" would never be found there. book_id_from(segment) turns a segment into the int it names, or returns None when it names no id. int(segment) alone is not enough: it raises "abc", and it also accepts text that is not an id here, such as " 12" with a space, "-1", and digits from other writing systems. The isdigit() alone is not enough either:
The superscript two and the Arabic-Indic digits for twelve both count as digits to isdigit(). int turns the second into 12 and raises ValueError for the first. Both can reach your server: a client sends %C2%B2 or %D9%A1%D9%A2 in the path, and path_segments decodes them. An id in this service is one or more ASCII digits and nothing else, so check segment.isascii() and segment.isdigit() first, and only then call int. An empty segment fails both checks, so /books/ still gets a 404.
Whether 007 names book 7 is your call; int("007") is 7, and this project does not decide either way.
Milestones
book_id_from(segment). Returnint(segment)when the segment is ASCII digits,Noneotherwise.The
/books/{id}branch. Inhandle, add a branch for segments of length two whose first segment is"books". Turn the second into an id. When the id isNoneor not a key ofstate["books"], answer 404 withjson_error, whatever the method: there is nothing at that path to allow or refuse. ForGETandHEAD, answer 200 withjson_responseand the one book as stored.405 there too. Any other method on an existing book answers
not_allowed("GET, HEAD"). YourHEADrule at the end ofhandlealready covers the new branch, since it runs after routing.Run the scenario. It sends five requests:
GET /books,GET /books/2,HEAD /books/2,GET /books/99, and aPOST /bookswith an empty body:
python pl_http.py reading-list
You should see the list; then book 2, Café Society, with content-length: 57; then the same head for HEAD /books/2 with nothing after the blank line; then a 404 with your error body; then a 405 with allow: GET, HEAD. The result block ends with serve() returned after 5 connections; the server is closed.
Tests, if you like.
tests/test_server.pyalready holds one test that callshandledirectly, through a helpermake_request(method, target). You can add your own the same way and run them all from the Shell tab:
def test_unknown_book_is_404():
"""GET /books/99 answers 404 with an error body."""
response = reading_server.handle(make_request("GET", "/books/99"), reading_server.sample_state())
assert response["status"] == 404
assert "error" in json.loads(response["body"].decode("utf-8"))
python -m pytest -q
These tests are for you; Check my work does not run them. Chapter 5 asks you to write tests that prove a property of the server, and grades those.
What this server does not check
An Host header, and an HTTP/1.1 server must answer a request without one with 400 Bad Request (RFC 9112, section 3.2). This course leaves that check out of the reading list. Everything else in the contract the server now does.
When you press Check my work, the whole chapter is checked again with books, ids, and paths of its own, and your handle also runs inside the real adapter class with request bytes held in memory, so the check reads the bytes a client would receive: the status line, a content-length equal to the body that follows it, nothing after the blank line for HEAD, and an allow line on a 405. No
The reading list can be read in every way HTTP allows, and changed in none: there is no way to add a book. Chapter 5 reads request bodies and makes the list writable with POST, PUT, and DELETE.
Task
Finish the read-only reading reading_server.py, as SERVICE_CONTRACT.md describes it.
Write
book_id_from(segment): returnint(segment)when the segment is one or more ASCII digits (segment.isascii()andsegment.isdigit()), andNonefor anything else.Add
/books/{id}tohandle: for an id that isNoneor not a key ofstate["books"], answer 404 withjson_error, whatever the; for GETandHEAD, answer 200 with the book as stored; for any other method, answernot_allowed("GET, HEAD").Keep everything from Lessons 2 to 5:
/booksanswersGETandHEADwith the list and other methods with 405, every other path answers 404,HEADgetsGET’s status and headers with an empty body, andmake_serverandservestay as you wrote them.
Run python pl_http.py reading-list in the tests/test_server.py and running python -m pytest -q is optional.
Then press Check my work. It repeats the checks of Lessons 2 to 5 with books and paths you have not seen. It calls book_id_from with ASCII digits and with text that is not an id, such as "-1", "1.5", "²", " 12", and Arabic-Indic digits. It calls handle for existing and missing ids, for segments that are not ids, for HEAD on every route, and for PUT, POST, and DELETE on both paths, expecting 200, 404, and 405 with allow naming GET and HEAD, and a content-length on every reply with a body. It then runs your handle inside the http_adapter handler class on request bytes held in memory and reads the bytes written back: a 200 status line and a content-length equal to the body for one book, nothing after the blank line for HEAD, and an allow line on a 405. It checks make_server and serve the way Lesson 3 did, with no