Skip to content
academia.sh

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-Length is required because it states where the body ends in a byte stream; Content-Type states 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 4xx requires fixing the request, a 5xx requires 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.

Start typing to search.

↑↓ Esc navigate · open · close