Lesson 09 / 12
HTTP Request and Response
The parts of a request and response, the safety and idempotency of methods, status code classes, and the statelessness of the protocol.
Contents
The previous topic established how the name typed into the address bar reaches a server. The Client–Server Model lesson connected to that server and both ends of the exchange were seen. What was not explained is why the text sent takes that particular shape.
This lesson defines that shape. HTTP is the protocol that sets the rules for the exchange between client and server, and it is the contract the web is built on.
A Text-Based Protocol
The server in the Client–Server Model lesson wrote the bytes it received to the screen exactly as they arrived:
GET / HTTP/1.1 Host: example.test:8080 User-Agent: example-client Accept: */*
This is not a binary structure, it is readable text. That the design goes in this direction is a choice, and it has a cost: a text representation carries the same information in more bytes than a binary representation would, and parsing it is more expensive. In exchange, the protocol can be inspected without a special tool and can be extended; adding a new header does not break existing implementations.
Line endings are written with the two characters \r\n. What marks the end of the headers
is an empty line — that is the blank space at the end of the output above.
The Structure of a Request
A request has three parts.
The request line is the first line and carries three fields: method, target, version.
GET / HTTP/1.1
GET states the operation to perform, / the path of the requested resource, HTTP/1.1
the protocol version the client speaks. Only the path portion of the target is written;
the domain name is carried in a separate header.
Headers carry a name: value pair on each line. Header names are case-insensitive.
The three headers above each serve a different function: Host states which site is
being connected to and, as seen in the Hosting Models lesson, is the basis of virtual
hosting; User-Agent identifies the client; Accept states which content formats the
client can handle.
The body is optional data that comes after the empty line. It is usually absent in
GET requests; it is present in methods that send data.
The Structure of a Response
A response follows the same layout, only its first line differs.
HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 Content-Length: 74 Connection: close <!doctype html> <html lang="en"><body><h1>Example Page</h1></body></html>
The status line carries the version, the numeric status code, and its readable description. The description text is informational; decisions are made on the number.
The Content-Length header states how many bytes the body is. This header is not a
detail — it is information required for the protocol to work: the receiver needs to know
where the body ends. A connection is a byte stream, and a stream has no natural boundary.
When the length is not known in advance, the body is sent in chunks, with the size of each
chunk written before it.
Content-Type states how the body should be interpreted. The charset=utf-8 part here
answers the question set up in the Character Encodings lesson of the How Computers Work
course: which rule converts bytes to characters is not left to the receiver’s guess, it is
stated explicitly.
Methods
The method states the operation to perform on the resource. Two properties distinguish methods, and these properties determine client behavior.
A safe method makes no change on the server; it only reads. An idempotent method leaves the same result whether it is applied once or many times. Every safe method is idempotent; the reverse is not true.
| Method | Function | Safe | Idempotent |
|---|---|---|---|
GET |
Retrieves the resource | Yes | Yes |
HEAD |
Retrieves only the headers | Yes | Yes |
POST |
Sends data, may create a new resource | No | No |
PUT |
Replaces the resource with the given content | No | Yes |
DELETE |
Deletes the resource | No | Yes |
These properties are not theoretical. Whether a request can be retried when no response is
received follows directly from them: an idempotent request can be safely repeated, but if
a POST request is repeated the same operation can happen twice. Even if the DELETE
method returns a different status code on the second call, the server’s state stays the
same; idempotency is a property of the state left behind, not of the code returned.
These properties must be preserved by the server; the protocol cannot enforce them.
Writing a GET handler that changes state is syntactically possible and violates the
protocol.
The HEAD method returns the same headers as GET but sends no body. It is used to learn
a resource’s size or existence without downloading its content.
Status Codes
A status code has three digits, and its first digit gives the class of the result.
| Class | Meaning |
|---|---|
1xx |
Informational; processing continues |
2xx |
Success |
3xx |
Redirection; the resource is elsewhere |
4xx |
Client error; the request is malformed |
5xx |
Server error; the request was valid but the server could not fulfill it |
The distinction between 4xx and 5xx is decisive from an operational standpoint: the
first requires fixing the request, the second requires examining the server.
The following server produces three different status codes and defines a redirect:
from http.server import BaseHTTPRequestHandler, HTTPServer class Handler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" def log_message(self, fmt: str, *args: object) -> None: pass def _send(self, status: int, body: bytes, extra: list[tuple[str, str]] = []) -> None: self.send_response_only(status) self.send_header("Content-Type", "text/plain; charset=utf-8") self.send_header("Content-Length", str(len(body))) for name, value in extra: self.send_header(name, value) self.end_headers() self.wfile.write(body) def do_GET(self) -> None: if self.path == "/old": self._send(301, b"", [("Location", "/new")]) elif self.path == "/new": self._send(200, b"content at the new address\n") else: self._send(404, b"not found\n") def do_POST(self) -> None: length = int(self.headers.get("Content-Length", "0")) body = self.rfile.read(length) self._send(201, b"received body: " + body + b"\n") HTTPServer(("127.0.0.1", 8080), Handler).serve_forever()
The codes returned when three paths are queried:
for path in /new /old /missing; do printf "%-6s " "$path" curl -s -o /dev/null -w "%{http_code}\n" --resolve example.test:8080:127.0.0.1 "http://example.test:8080$path" done
/new 200 /old 301 /missing 404
Examining the redirect response itself, the new location of the resource is seen to be carried in a header:
curl -sD - -o /dev/null --resolve example.test:8080:127.0.0.1 http://example.test:8080/old
HTTP/1.1 301 Moved Permanently Content-Type: text/plain; charset=utf-8 Content-Length: 0 Location: /new
The body of the response is empty; the information carried is in the Location header.
Once the client receives this response, it makes a second request to the new target.
curl does not do this on its own; it does so when asked:
curl -sL --resolve example.test:8080:127.0.0.1 http://example.test:8080/old
content at the new address
Browsers follow redirects on their own. A 301 code states that the move is permanent
and lets the client not remember the old address; separate codes exist for temporary
moves, and this distinction matters, because permanent redirects can be cached by the
client.
A request carrying a body follows the same layout:
curl -s -X POST -d "name=test" --resolve example.test:8080:127.0.0.1 http://example.test:8080/register
received body: name=test
Statelessness
HTTP is a stateless protocol: the server remembers nothing about the client between two requests. Each request is interpreted on its own, independent of previous requests.
This is an observable property. The following server reads the counter carried by the request and returns an incremented value to the client:
from http.server import BaseHTTPRequestHandler, HTTPServer class Handler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" def log_message(self, fmt: str, *args: object) -> None: pass def do_GET(self) -> None: incoming = self.headers.get("Cookie", "") counter = int(incoming.removeprefix("counter=")) if incoming.startswith("counter=") else 0 body = f"observed counter: {counter}\n".encode() self.send_response_only(200) self.send_header("Content-Type", "text/plain; charset=utf-8") self.send_header("Content-Length", str(len(body))) self.send_header("Set-Cookie", f"counter={counter + 1}") self.end_headers() self.wfile.write(body) HTTPServer(("127.0.0.1", 8080), Handler).serve_forever()
If three requests are made carrying nothing between them:
for i in 1 2 3; do curl -s --resolve example.test:8080:127.0.0.1 http://example.test:8080/; done
observed counter: 0 observed counter: 0 observed counter: 0
The server saw zero all three times. It keeps no record of its own.
If the same three requests are made while carrying back the value the server gave:
rm -f jar.txt for i in 1 2 3; do curl -s -b jar.txt -c jar.txt --resolve example.test:8080:127.0.0.1 http://example.test:8080/; done
observed counter: 0 observed counter: 1 observed counter: 2
The server code did not change. What changed is that the client stored the value given
by Set-Cookie and sent it back in the next request with the Cookie header. The state
is carried not in the server, but in the request.
The reason for statelessness is scaling. Because the server holds no client-specific memory, two consecutive requests can be answered by different machines; if the server crashes, there is no session state that gets lost when it restarts. The cost is that every request must carry again the information needed to identify itself.
Summary
- HTTP is a text-based protocol; it trades parsing cost for inspectability and extensibility.
- A request consists of a request line, headers, an empty line, and an optional body; a response follows the same layout with a status line.
Content-Lengthis required because it states where the body ends in a byte stream;Content-Typestates how the bytes should be interpreted.- Methods are distinguished by their safety and idempotency properties; these properties determine whether a request can be retried and must be preserved by the server.
- The first digit of a status code gives its class; a
4xxrequires fixing the request, a5xxrequires fixing the server. - The protocol is stateless; session continuity is built not by the server’s memory but by data the client carries back with every request.
Next Step
This lesson examined a single request and a single response. Yet when a page is opened, only one request is not made: the document that arrives refers to other resources, and each of them is requested separately. The next lesson takes up the stages from the text typed into the address bar to the drawing on the screen, and shows through a real server’s log how these additional requests arise.
To keep your progress and take notes, Log in
My notes
Log in to take notes.