---
title: 'HTTP Request and Response'
source: 'https://academia.sh/en/courses/how-the-internet-works/http-request-and-response'
course: 'How the Internet Works'
language: en
updated: '2026-08-17T18:07:05+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

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:

```python
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:

```sh
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:

```sh
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:

```sh
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:

```sh
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:

```python
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:

```sh
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:

```sh
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.
