HTTP messages: requests, responses, and status codes
Read the text structure of an HTTP/1.1 request and response, and parse a response by hand.
On this page
Strings and dictionaries.
- Read the text structure of an HTTP/1.1 request and response, and parse a response by hand.
- Predict the sample output and explain the state changes.
- Complete the practice task and check the boundary cases.
The concept
HTTP is the application protocol of the web. A client sends a request and a server sends a response. In HTTP/1.1 these messages are plain text. A request starts with a request line, which has a method, a target, and a version. A response starts with a status line that has the version, a numeric status code, and a reason phrase. Header lines follow, then an empty line, then an optional body.
Methods describe intent: GET reads a resource, and POST submits data. A method is safe if it does not change server state, as with GET, and idempotent if repeating it has the same effect as doing it once, as with GET, PUT, and DELETE. Status codes are grouped by their first digit: 1xx informational, 2xx success, 3xx redirection, 4xx client error, and 5xx server error. Header names are case-insensitive.
The program parses a response by splitting at the blank line, then reading the status line and headers. The Content-Length header tells the receiver how many bytes of body to expect, so it must count bytes rather than characters. HTTP/2 and HTTP/3 keep the same semantics but send the messages in binary frames, and HTTP/3 runs over QUIC instead of TCP.
Code example
request = "\r\n".join(["GET /index.html HTTP/1.1", "Host: example.com", "Accept: text/html", "", ""])
response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\nhello"
def parse_response(text):
head, body = text.split("\r\n\r\n", 1)
status_line, *header_lines = head.split("\r\n")
version, status, reason = status_line.split(" ", 2)
headers = {}
for line in header_lines:
name, value = line.split(":", 1)
headers[name.strip().lower()] = value.strip()
return int(status), reason, headers, body
method, target, version = request.split("\r\n")[0].split(" ")
print(method, target, version)
status, reason, headers, body = parse_response(response)
print(status, reason)
print(headers["content-length"], len(body.encode()))
print(body)GET /index.html HTTP/1.1 200 OK 5 5 hello
Follow the code
Open the walkthrough to inspect the example one step at a time.
Show guided walkthrough
request = "\r\n".join(["GET /index.html HTTP/1.1", "Host: example.com", "Accept: text/html", "", ""])response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\nhello" def parse_response(text): head, body = text.split("\r\n\r\n", 1) status_line, *header_lines = head.split("\r\n") version, status, reason = status_line.split(" ", 2) headers = {} for line in header_lines: name, value = line.split(":", 1) headers[name.strip().lower()] = value.strip() return int(status), reason, headers, body method, target, version = request.split("\r\n")[0].split(" ")print(method, target, version)status, reason, headers, body = parse_response(response)print(status, reason)print(headers["content-length"], len(body.encode()))print(body)Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| The first line of a request holds the method, the target path, and the protocol version. | GET, /index.html, HTTP/1.1 | — | |
| An empty line (two CRLFs in a row) separates the headers from the body. | GET, /index.html, HTTP/1.1 | — | |
| The status line has the version, the numeric code, and the reason phrase; the phrase may contain spaces, so split at most twice. | 200, OK | — | |
| Status 200 means success. | 200, OK | GET /index.html HTTP/1.1 200 OK | |
| Content-Length: 5 matches the five bytes of the body. | 200, OK | GET /index.html HTTP/1.1 200 OK 5 5 hello |
Behind the scenes
How Python processes this code
Python runs this source through its interpreter. It creates objects for values and resolves names as each statement runs.
Cost and common mistakes
Parsing is O(n) in the message size n: one pass to split and one pass over the header lines.
- Header names are case-insensitive, so normalize them before comparing. Values are often case-sensitive.
- Content-Length counts bytes, not characters. For non-ASCII text the two differ.
- Real parsers must also handle chunked transfer encoding, repeated headers, and malformed input. Use a tested HTTP library in production.
Try it yourself
Which status code class does 404 belong to, and who is at fault?
Reveal answer and reasoning
4xx is a client error. 404 means the server could not find the requested resource; the request, rather than the server's internals, is the problem.