Where to Go Next
What our simple HTTP server cannot do yet, and a few experiments to make it better.
You built a working HTTP server, and it is small enough to read in one sitting. That is the point of a simple server: nothing is hidden, so you can see how every part works.
But a simple server also has limits. In this page, we look at them honestly. Then you get a list of experiments, so you can fix some of them yourself.
What Our Server Can't Do
- It serves one client at a time - While it talks to one client, everyone else waits. A client that connects and says nothing blocks the others until the 5-second timeout.
- It handles one request per connection - We always send
Connection: close, so a browser must open a new connection for every file. A page with an image and a stylesheet needs three. - It has no size limits - A client can send headers forever, and we keep reading. We also load each file fully into memory before sending it.
- It knows only six file types - Any other extension is sent as
application/octet-stream, and the extensions are case-sensitive, soSTYLE.CSSis not recognized as CSS. - It does not decode URLs - A browser asks for
my page.htmlas/my%20page.html, and we look for a file with%20in its name. - Only
/has a default page - A request for/docs/does not servedocs/index.html. - It only supports
GETandHEAD- Anything else gets a501, and we never read a request body. - It sends no
Dateheader and no caching headers. The browser downloads every file again on every visit. - It has no HTTPS. Everything travels as plain text, and the server only listens on
127.0.0.1, so only your own computer can reach it.
None of these are mistakes. Each one is something we left out to keep the server simple, and each one is a good next lesson.
Experiments
The experiments are in three groups, from small to big. Each one changes only a small part of the code, and you already know all the pieces. Pick any order.
Warm-ups
1. Make extensions case-insensitive.
Copy public/style.css to public/UP.CSS and open http://127.0.0.1:8000/UP.CSS with curl -i. It arrives as application/octet-stream, so a browser would ignore it as a stylesheet. Fix it so .CSS and .css are treated the same.
Hint: strings have a .lower() method.
2. Add Date and Server headers.
Every response from a real server says when it was sent and which software sent it. Add both to build_response().
Hint: email.utils.formatdate(usegmt=True) returns a date in the format HTTP expects.
3. Send a custom 404 page.
Create public/404.html, and send it with the 404 status instead of the plain text message.
Hint: the status stays 404, and only the body and the Content-Type change.
Bigger
4. Serve a default page for folders.
Make /docs/ send public/docs/index.html.
Hint: Path has an is_dir() method. Be careful, the public folder itself is not in its own parents, which is why we needed a special case for /.
5. Decode URLs.
Create a file named my page.html, and make /my%20page.html find it.
Hint: urllib.parse.unquote() decodes %20. Decode before you build the file path, then test that curl --path-as-is -i http://127.0.0.1:8000/%2e%2e/server.py still gets a 404.
6. Limit the size of a request.
Stop reading after 8,192 bytes with no blank line, and answer 431 Request Header Fields Too Large.
Hint: read_request() is the only place that needs to know about the limit. To test it, send 20,000 bytes without \r\n\r\n.
Stretch
7. Serve many clients at once.
First, see the problem. In one terminal, run the silent client from the error page, then run curl -i http://127.0.0.1:8000/ in another. curl waits until the timeout. Now fix it so curl answers immediately.
Hint: look at the threading module. The work for one connection already lives in handle(), so each connection can run in its own thread.
8. Handle many requests on one connection.
Stop sending Connection: close, and keep reading requests from the same connection until the client leaves or goes silent.
Hint: read_request() must not throw away bytes that belong to the next request, and a client that sends Connection: close still expects the server to close.
There are many more ideas. You could redirect an old path to a new one with a 301 and a Location header. You could let Python guess content types with the mimetypes module. You could add Last-Modified and answer repeat visits with 304 Not Modified.
Keep Reading
To see what a finished server does more, run Python's own server next to yours:
python3 -m http.server 8001 --bind 127.0.0.1Request the same pages from both with curl -i and compare the headers. Every difference is something to learn about.
For the full rules, the HTTP specifications are written for people who build servers: RFC 9110 describes what requests and responses mean, and RFC 9112 describes how HTTP/1.1 puts them on the wire. You now know enough to read the important parts.
Pick one experiment, break something, and fix it. That is how a server stops being a mystery.