How an HTTP request reaches your code and comes back¶
~20 min ยท prerequisite: Parts 1 to 4 (functions, strings, dictionaries, exceptions) and Part 7 (classes)
What you will be able to do¶
- Name the parts of an HTTP request and an HTTP response.
- Write a small
http.serverhandler that answers aGETrequest with JSON. - Choose a status code, such as 200, 404, or 501, that matches what happened.
The idea¶
A client asks for something, such as a web browser or the curl command. A server waits, listens, and answers. They talk over HTTP, the HyperText Transfer Protocol, a set of rules for what each message looks like.
Think of a letter and its reply. The request says what you want through the method and path. It may carry headers, which are labelled details such as the content type, and a body, which is the content itself. The response has a status code that says how things went, its own headers, and a body with the answer.
The first line of a request holds the method, the path, and the version, separated by spaces: GET /hello HTTP/1.1. GET asks for data without changing anything. POST creates something, PUT replaces an item, and DELETE removes one. A query string may follow the path after a ?, as in /hello?name=Ada.
A status code is a three-digit number. A 2xx code means success, such as 200 OK. A 4xx code means the client sent something the server cannot accept, such as 404 Not Found. A 5xx code means the server failed.
One request follows these steps:
- The client opens a connection and sends the request text.
- The server reads the request line and the headers.
- The server chooses code to run from the method and path. This choice is called routing.
- The code builds a response, and the server writes the status line, the headers, and the body.
- The connection closes. HTTP is stateless: the server remembers nothing about earlier requests unless your code stores that memory, such as in a database.
Worked example¶
Save this as hello_server.py. It is a plain Python file with no extra installs.
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/hello":
status = 200
body = {"message": "hello"}
else:
status = 404
body = {"error": "not found"}
data = json.dumps(body).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def log_message(self, format, *args):
pass # silence the default access log so the output stays readable
if __name__ == "__main__":
server = HTTPServer(("127.0.0.1", 8000), Handler)
print("Listening on http://127.0.0.1:8000")
server.serve_forever()
Line by line, this is what Python does:
class Handler(BaseHTTPRequestHandler)is the class the standard library uses for each request. You write itsdo_GETmethod.do_GETruns forGETrequests, so its name must be exactlydo_GET.self.pathholds the path, such as/hello. Theifpicks a status and a body.json.dumps(body).encode("utf-8")turns the dictionary into JSON text, then into bytes. The socket sends bytes.send_responsewrites the status line, andsend_headeradds each header.Content-Lengthtells the client how many bytes to expect.end_headersends the header section with a blank line.self.wfile.write(data)sends the body.- The
if __name__ == "__main__":block starts the server only when you run the file directly.serve_foreverwaits for requests until you press Ctrl+C.
Try it¶
Run the server in one terminal:
In a second terminal, ask for the route and then a missing one:
$ curl -i http://127.0.0.1:8000/hello
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 20
{"message": "hello"}
$ curl -i http://127.0.0.1:8000/nothing
HTTP/1.0 404 Not Found
Content-Type: application/json
Content-Length: 22
{"error": "not found"}
curl -i prints the headers too. The Server and Date headers are left out above.
The next blocks need no server.
Common mistakes¶
-
Writing text instead of bytes. Writing the
strfromjson.dumpsdirectly fails with:Call
.encode("utf-8")first. -
Forgetting
end_headers(). Without it the body is not separated from the headers. Python raises no error.curlreports:Add the call back before the
write. -
Starting two servers on one port. The second fails with:
On Linux the number is 98. Stop the first server with Ctrl+C.
-
Sending a method you did not write. A
POSTrequest to the server above returns status501. The status line readsHTTP/1.0 501 Unsupported method ('POST'), and the body is an HTML error page. The fix is to write ado_POSTmethod. -
Comparing the path with a query string. For
/hello?name=Ada,self.pathis/hello?name=Ada, so the checkself.path == "/hello"is false and the server answers 404.
Practice¶
- Call
parse_request_linewith"DELETE /notes/3 HTTP/1.1"and assert that the method is"DELETE". - Write
status_class(code)that returns"success","client error", or"server error"for 2xx, 4xx, or 5xx codes. Assert it for 200, 404, and 500. - Add a
/healthroute tohello_server.pythat returns{"status": "ok"}with status 200. - Write
make_response(body)that returns(status_line, data), wherestatus_lineis"HTTP/1.0 200 OK"anddatais the JSON bytes. Assert the length ofdata. - Stretch: add a
do_POSTmethod for/echothat sends back the request body with status 200.
Solution
-
python parsed = parse_request_line("DELETE /notes/3 HTTP/1.1") assert parsed["method"] == "DELETE" -
```python def status_class(code): kinds = {2: "success", 4: "client error", 5: "server error"} return kinds.get(code // 100, "other")
assert status_class(200) == "success" assert status_class(404) == "client error" assert status_class(500) == "server error" ```
- Add this branch inside
do_GET, before theelse:
Check it with curl -i http://127.0.0.1:8000/health.
- ```python import json
def make_response(body): data = json.dumps(body).encode("utf-8") return "HTTP/1.0 200 OK", data
status_line, data = make_response({"message": "hello"}) assert status_line == "HTTP/1.0 200 OK" assert len(data) == len('{"message": "hello"}') ```
python def do_POST(self): if self.path == "/echo": length = int(self.headers["Content-Length"]) data = self.rfile.read(length) self.send_response(200) self.send_header("Content-Length", str(len(data))) self.end_headers() self.wfile.write(data) else: self.send_error(404)
Test with curl -i -X POST -d hi http://127.0.0.1:8000/echo.
Recap¶
- A request has a method, a path, headers, and an optional body. A response has a status code, headers, and a body.
http.servercalls the method named after the request method, such asdo_GET.- Send bytes, set
Content-Length, and callend_headers()before the body.