0%

Serve HTTP Yourself · capstone

Capstone project: Project: A Read-Only Reading List

Your server the books, refuses what it cannot do, and answers 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:

RequestResponse
GET /books200, {"books": [every book, by id, ascending]}
GET /books/{id}200, the book; 404 when no book has that id
HEAD on eitherthe status and headers GET would send, and no body
another on either405 with allow: GET, HEAD
any other path404

Every reply with a body is with a byte-counted 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 for "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 method isdigit() alone is not enough either:

Try it

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

  1. book_id_from(segment). Return int(segment) when the segment is ASCII digits, None otherwise.

  2. The /books/{id} branch. In handle, add a branch for segments of length two whose first segment is "books". Turn the second into an id. When the id is None or not a key of state["books"], answer 404 with json_error, whatever the method: there is nothing at that path to allow or refuse. For GET and HEAD, answer 200 with json_response and the one book as stored.

  3. 405 there too. Any other method on an existing book answers not_allowed("GET, HEAD"). Your HEAD rule at the end of handle already covers the new branch, since it runs after routing.

  4. Run the scenario. It sends five requests: GET /books, GET /books/2, HEAD /books/2, GET /books/99, and a POST /books with 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.

  1. Tests, if you like. tests/test_server.py already holds one test that calls handle directly, through a helper make_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 /1.1 request must carry a 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 is opened.

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 in reading_server.py, as SERVICE_CONTRACT.md describes it.

  • Write book_id_from(segment): return int(segment) when the segment is one or more ASCII digits (segment.isascii() and segment.isdigit()), and None for anything else.

  • Add /books/{id} to handle: for an id that is None or not a key of state["books"], answer 404 with json_error, whatever the ; for GET and HEAD, answer 200 with the book as stored; for any other method, answer not_allowed("GET, HEAD").

  • Keep everything from Lessons 2 to 5: /books answers GET and HEAD with the list and other methods with 405, every other path answers 404, HEAD gets GET’s status and headers with an empty body, and make_server and serve stay as you wrote them.

Run python pl_http.py reading-list in the . Writing tests in 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 body with a byte-counted 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 bound. Your own tests and your terminal are not graded.