Serve HTTP Yourself · practice
Route by Method and Path
Your server answers. Ask it something it does not have, and it still answers with the
python pl_http.py routes
GET /books, GET /nowhere, and DELETE /books all come back HTTP/1.0 200 OK with the same 132-byte list. There is nothing at /nowhere, and nothing was deleted, yet the replies say that both requests succeeded. A client has no way to tell. The fourth request, PATCH /books, gets 501 Unsupported method ('PATCH') and a short HTML page, with Connection, Content-Type, and Content-Length written by http.server itself: as you read in Lesson 1, the adapter’s handler has no do_PATCH, so your handle never saw that request. That reply is the library’s, not yours, and it stays that way. This lesson teaches handle to say no to the other two, with the right status for each.
The path, decoded
A route is a decision made from the /books?limit=1 carries a query, and the query is not part of the route, so /books?limit=1 is still the list. The path can also arrive percent-encoded. In Chapter 2 your client encoded each segment with quote, and decode_segment turned one back with unquote. A client may encode a character that did not need it, so /b%6Foks is /books: %6F is the letter o.
Three steps turn a target into the list of segments a route compares:
Take the path with
urlsplit(target).path, which drops the query.urlsplitis theyou used in Chapter 2 to take a apart; on a target it works the same way. Split the path on
/and drop the first piece, the empty text before the leading/, with[1:].Decode each segment with
unquote.
Split first, decode second. A segment may hold an encoded slash, %2F, which is part of that segment’s text, such as a title with a slash in it. Decode the whole path first, and that slash would split one segment into two. Both urlsplit and unquote are already imported at the top of reading_server.py.
The list for the reading list is ["books"]. Comparing lists is exact, so /BOOKS is a different path, and so is /books/:
What does this print?
print("/books/".split("/")[1:])
The trailing / leaves an empty last segment, so ["books", ""] is not ["books"] and /books/ gets a 404 from this server. Some servers send such a request on to /books instead; Chapter 6 shows how.
404 or 405
Two error statuses cover what handle must refuse, and they say different things:
404 Not Found: nothing is at this path, whatever the method.
405 Method Not Allowed: something is at this path, but not with this method. A 405 must carry an
Allowheader listing the methods that do work there (RFC 9110, section 15.5.6), so the client can see what it may ask instead.
/nowhere is a 404 for every method, DELETE included. /books exists, so DELETE /books is a 405, and its allow header says GET. In Lesson 5, HEAD joins it. The order of the questions matters: first the path, then the method.
DELETE /books arrives at a server whose /books allows only GET and HEAD. Which reply is right?
Error bodies
An error is still a reply with a body, so it gets the same treatment as the list: content-type: application/json, and a byte-counted content-length. This server’s error body is an {"error": "a sentence naming the problem"}, as SERVICE_CONTRACT.md says. json_error(status, message) builds it with your json_response, and not_allowed(allowed) builds a 405 with json_error and then adds "allow" to that response’s headers. allowed is the header’s "GET".
One rule about header values: http.server writes each value exactly as given, without checking it. A value holding \r\n would end its line early and start a header of its own. Every value you write in this chapter is text you chose, so that cannot happen here; Chapter 7 returns to it when values come from a request.
Your task
Write
path_segments(target): the three steps above, returning the list of decoded segments.Write
json_error(status, message):json_response(status, {"error": message}).Write
not_allowed(allowed): a 405 fromjson_errorwith a message of your choice, plus an"allow"header whose value isallowed.Change
handle: compute the segments ofrequest["target"]. If they are["books"]and the method isGET, answer the list as before. If they are["books"]with any other method, answernot_allowed("GET"). Anything else getsjson_error(404, ...)with a message of your choice.
Run python pl_http.py routes again. GET /books still gets the list. GET /nowhere gets 404 Not Found with your error body, DELETE /books gets 405 Method Not Allowed with an allow line naming GET, and PATCH /books still gets the library’s 501.
Then press Check my work. One method you have not handled yet is HEAD: right now it gets a 405. The next lesson fixes that.
Task
Write three reading_server.py and extend handle.
path_segments(target)returns the decoded segments of the target’s path: takeurlsplit(target).path, split it on/, drop the first, empty piece with[1:], and decode each segment withunquote."/a%2Fb/c?x=1"gives["a/b", "c"], and"/"gives[""].json_error(status, message)returnsjson_response(status, {"error": message}).not_allowed(allowed)returns a 405 built withjson_error, with an"allow"header whoseis the text allowed, such as"GET".handle(request, state)routes on the segments ofrequest["target"]:["books"]withGETanswers theas before; ["books"]with any otheranswers not_allowed("GET"); any other path answersjson_error(404, ...), whatever the method. The error messages are yours to word.
Run python pl_http.py routes in the
Then press Check my work. It calls path_segments on targets you have not seen, with encoded slashes, encoded letters, and queries; json_error and not_allowed with their own values; and handle with a state of its own: GET on /books, on /books with a query, and on an encoded /books must answer 200 with the list; /BOOKS, /books/, /, and other paths must answer 404, also for DELETE; POST, PUT, and DELETE on /books must answer 405 with an allow header that names GET. Every error reply must carry a {"error": ...}, content-type: application/json, and a content-length equal to the body’s length in bytes. Nothing from your terminal is graded, and no server is started.