0%

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 . Run the scenario for this lesson, which sends four requests:

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 and the path. The target can hold more than the path: /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:

  1. Take the path with urlsplit(target).path, which drops the query. urlsplit is the you used in Chapter 2 to take a apart; on a target it works the same way.

  2. Split the path on / and drop the first piece, the empty text before the leading /, with [1:].

  3. 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.

Try it

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 Allow header 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.

Try it

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 with one key, {"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 as text, such as "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

  1. Write path_segments(target): the three steps above, returning the list of decoded segments.

  2. Write json_error(status, message): json_response(status, {"error": message}).

  3. Write not_allowed(allowed): a 405 from json_error with a message of your choice, plus an "allow" header whose value is allowed.

  4. Change handle: compute the segments of request["target"]. If they are ["books"] and the method is GET, answer the list as before. If they are ["books"] with any other method, answer not_allowed("GET"). Anything else gets json_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 in reading_server.py and extend handle.

  • path_segments(target) returns the decoded segments of the target’s path: take urlsplit(target).path, split it on /, drop the first, empty piece with [1:], and decode each segment with unquote. "/a%2Fb/c?x=1" gives ["a/b", "c"], and "/" gives [""].

  • json_error(status, message) returns json_response(status, {"error": message}).

  • not_allowed(allowed) returns a 405 built with json_error, with an "allow" header whose is the text allowed, such as "GET".

  • handle(request, state) routes on the segments of request["target"]: ["books"] with GET answers the as before; ["books"] with any other answers not_allowed("GET"); any other path answers json_error(404, ...), whatever the method. The error messages are yours to word.

Run python pl_http.py routes in the to see the four replies.

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 body {"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.