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.

Python
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)
Output
GET /index.html HTTP/1.1
200 OK
5 5
hello

Explanation

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.

Explore the dry run
Guided walkthroughReady
1request = "\r\n".join(["GET /index.html HTTP/1.1", "Host: example.com", "Accept: text/html", "", ""])
2response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\nhello"
3
4
5def parse_response(text):
6 head, body = text.split("\r\n\r\n", 1)
7 status_line, *header_lines = head.split("\r\n")
8 version, status, reason = status_line.split(" ", 2)
9 headers = {}
10 for line in header_lines:
11 name, value = line.split(":", 1)
12 headers[name.strip().lower()] = value.strip()
13 return int(status), reason, headers, body
14
15
16method, target, version = request.split("\r\n")[0].split(" ")
17print(method, target, version)
18status, reason, headers, body = parse_response(response)
19print(status, reason)
20print(headers["content-length"], len(body.encode()))
21print(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
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput 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

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.

Further reading