Computer Networks Module 15 – Web and application protocols
HTTP messages, methods and status codes
How HTTP/1.1 requests and responses are built, what methods and status codes promise, and how headers set body length, connection reuse and cookies.
What you will learn
- Read and write HTTP/1.1 requests and responses by hand
- Choose methods and status codes with the correct safety and idempotency semantics
- Use headers for content negotiation, connection reuse and cookies
Before you start
What you’ll learn · Before you start
What you will learn
- Read and write HTTP/1.1 requests and responses by hand
- Choose methods and status codes with the correct safety and idempotency semantics
- Use headers for content negotiation, connection reuse and cookies
Before you start
On this page
HTTP is a request and response protocol: a client sends a request message, and the server answers with exactly one
response message. Every message has the same four parts: a start line, header fields, an empty line and an optional
body. The request’s start line names a method (what the client wants done, such as GET or POST) and a target;
the response’s start line carries a three-digit status code that says how it went, such as 200 or 404. This
lesson reads real messages byte by byte, then covers the rules that make HTTP reliable in practice: which requests
are safe to repeat, which status code fits which situation, and how a receiver knows where a message ends.
One meaning, three wire formats
The meaning of methods, status codes and header fields is defined once, in HTTP Semantics (RFC 9110), and shared by every version of the protocol. Only the way messages are written onto a connection differs:
- HTTP/1.1 (RFC 9112): lines of text over a TCP connection, one message after another.
- HTTP/2 (RFC 9113): binary frames over one TCP connection, many requests at the same time.
- HTTP/3 (RFC 9114): binary frames on QUIC streams, which run over UDP.
So a 404 means the same thing in all three, and a lesson about methods and status codes is a lesson about all of
HTTP. The text format of HTTP/1.1 is the one you can read with your own eyes, which is why it comes first.
The parts of a message
The four parts of every HTTP/1.1 message
Text description of the diagram
The diagram shows two boxes, one above the other, joined by an arrow labelled "same TCP connection".
The upper box is a request, sent from the client to the server. From top to bottom it has four parts:
- The request line, POST /notes HTTP/1.1: the method, the target and the version.
- The header fields, one per line: Host: notes.example, Content-Type: application/json and Content-Length: 20.
- An empty line, a lone CR LF, which ends the header section.
- The body, exactly 20 bytes long because Content-Length says so: {"text": "buy milk"}.
The lower box is the response, sent back from the server to the client, with the same four parts:
- The status line, HTTP/1.1 201 Created: the version, the status code and a reason phrase.
- The header fields: Location: /notes/7, Content-Type: application/json and Content-Length: 29.
- An empty line.
- The body, exactly 29 bytes: {"id": 7, "text": "buy milk"}.
- The start line is the request line (
method target version) or the status line (version code reason). The reason phrase, such asCreated, is only for people; programs read the code. - Header fields follow, one per line, as
Name: value. Names are case-insensitive, socontent-lengthandContent-Lengthare the same field. - An empty line ends the header section. In HTTP/1.1 every line ends with the two bytes CR LF (carriage return, line feed), so the empty line is a lone CR LF.
- The body, when there is one, comes last. The receiver must be told how long it is, because nothing in the bytes themselves marks its end; the section on body length below explains how.
A real exchange, byte by byte
The program below lets Python’s HTTP client talk to Python’s HTTP server over a connected pair of sockets, so it
needs no network, and prints every byte the client sends and receives. It makes three requests on one connection:
a GET, a POST that creates a note, and a GET whose answer the server sends in pieces.
"""Three HTTP/1.1 exchanges on one connection, printed byte for byte.
Python's own client (http.client) talks to Python's own server (http.server) over a
connected pair of sockets, socket.socketpair(), so no network is needed. A small tap
records every byte the client sends and receives. The output shows line ends as \\r\\n
(CR LF) and \\n (LF alone), and leaves out the value of the Date header, which changes on
every run.
"""
import http.client
import http.server
import io
import json
import re
import socket
import threading
class NotesHandler(http.server.BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1" # persistent connections, as HTTP/1.1 defines them
def do_GET(self):
if self.path == "/hello":
body = b"Hello, HTTP\n"
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
elif self.path == "/count":
# No length known in advance: send the body in chunks.
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Transfer-Encoding", "chunked")
self.end_headers()
for part in (b"one\n", b"two\n", b"three\n"):
self.wfile.write(b"%x\r\n%s\r\n" % (len(part), part))
self.wfile.write(b"0\r\n\r\n") # the last chunk has size 0
else:
self.send_error(404)
def do_POST(self):
length = int(self.headers["Content-Length"])
note = json.loads(self.rfile.read(length))
body = json.dumps({"id": 7, "text": note["text"]}).encode()
self.send_response(201)
self.send_header("Location", "/notes/7")
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args):
pass # no access log on standard error
class Tap:
"""Wraps the client's socket and records what goes out and what comes in."""
def __init__(self, sock):
self.sock = sock
self.sent = bytearray()
self.received = bytearray()
def sendall(self, data):
self.sent += data
self.sock.sendall(data)
def makefile(self, mode):
tap = self
class Reader(io.RawIOBase):
def readable(self):
return True
def readinto(self, buffer):
n = tap.sock.recv_into(buffer)
tap.received += buffer[:n]
return n
return io.BufferedReader(Reader())
def close(self):
self.sock.close()
def show(label, data):
"""Prints a message line by line, with its line ends made visible."""
print(label)
*lines, rest = bytes(data).decode().split("\r\n")
for line in lines:
line = re.sub(r"^Date: .*", "Date: (the time of the run, left out)", line)
print(" ", line.replace("\n", "\\n") + "\\r\\n")
if rest:
print(" ", rest.replace("\n", "\\n"))
client_end, server_end = socket.socketpair()
server = threading.Thread(target=NotesHandler, args=(server_end, ("client", 0), None))
server.start()
tap = Tap(client_end)
conn = http.client.HTTPConnection("notes.example")
conn.sock = tap # use the socket pair instead of connecting
for method, path, body, headers in [
("GET", "/hello", None, {}),
("POST", "/notes", json.dumps({"text": "buy milk"}), {"Content-Type": "application/json"}),
("GET", "/count", None, {}),
]:
tap.sent.clear()
tap.received.clear()
conn.request(method, path, body=body, headers=headers)
response = conn.getresponse()
content = response.read()
show(f"--> {method} {path}: the client sent {len(tap.sent)} bytes", tap.sent)
show(f"<-- {response.status} {response.reason}: the client read {len(tap.received)} bytes", tap.received)
print(" The application got the body:", content)
print()
conn.close() # the server sees the connection close and its thread ends
server.join()
print("Three requests, three responses, one connection.") Output
--> GET /hello: the client sent 71 bytes
GET /hello HTTP/1.1\r\n
Host: notes.example\r\n
Accept-Encoding: identity\r\n
\r\n
<-- 200 OK: the client read 165 bytes
HTTP/1.1 200 OK\r\n
Server: BaseHTTP/0.6 Python/3.14.8\r\n
Date: (the time of the run, left out)\r\n
Content-Type: text/plain; charset=utf-8\r\n
Content-Length: 12\r\n
\r\n
Hello, HTTP\n
The application got the body: b'Hello, HTTP\n'
--> POST /notes: the client sent 144 bytes
POST /notes HTTP/1.1\r\n
Host: notes.example\r\n
Accept-Encoding: identity\r\n
Content-Length: 20\r\n
Content-Type: application/json\r\n
\r\n
{"text": "buy milk"}
<-- 201 Created: the client read 198 bytes
HTTP/1.1 201 Created\r\n
Server: BaseHTTP/0.6 Python/3.14.8\r\n
Date: (the time of the run, left out)\r\n
Location: /notes/7\r\n
Content-Type: application/json\r\n
Content-Length: 29\r\n
\r\n
{"id": 7, "text": "buy milk"}
The application got the body: b'{"id": 7, "text": "buy milk"}'
--> GET /count: the client sent 71 bytes
GET /count HTTP/1.1\r\n
Host: notes.example\r\n
Accept-Encoding: identity\r\n
\r\n
<-- 200 OK: the client read 195 bytes
HTTP/1.1 200 OK\r\n
Server: BaseHTTP/0.6 Python/3.14.8\r\n
Date: (the time of the run, left out)\r\n
Content-Type: text/plain; charset=utf-8\r\n
Transfer-Encoding: chunked\r\n
\r\n
4\r\n
one\n\r\n
4\r\n
two\n\r\n
6\r\n
three\n\r\n
0\r\n
\r\n
The application got the body: b'one\ntwo\nthree\n'
Three requests, three responses, one connection.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 on_the_wire.py
Things to notice in the output:
- The client added
Host: notes.exampleandAccept-Encoding: identityby itself.identitymeans “send the body as it is, not compressed”. - The
POSTcarriesContent-Length: 20because its JSON body is exactly 20 bytes. The server answered201 Createdwith aLocationheader naming the new resource, which is what 201 is for. - The third response had no
Content-Length. It saidTransfer-Encoding: chunkedinstead, and sent the body as chunks, each starting with its size in hexadecimal:4,4,6, then0to mark the end. The client put the chunks back together before the program saw the body. - All three exchanges used the same connection. The
Datevalue is left out of the printout because it changes on every run; a server with a clock must send it on most responses.
Methods: safe, idempotent, cacheable
A method states what the client wants. RFC 9110 gives each method properties that clients, proxies and caches rely on, and those properties matter more in interviews and in production than the list of names.
- Safe methods only read. A crawler may follow every safe link without changing anything on the server.
- Idempotent methods can be sent twice with the same effect as once. If a connection drops before the response arrives, a client may send an idempotent request again automatically, because a duplicate does no harm.
- Cacheable methods have responses that a cache may store and reuse; a later lesson of this module covers how.
Grouped by those properties, the methods of RFC 9110 and RFC 5789 are:
- Safe, so also idempotent:
GETreads a resource;HEADis likeGETbut returns only the status and headers;OPTIONSasks which methods and features a target supports;TRACEechoes the request back, for debugging. Responses toGETandHEADare cacheable. - Idempotent but not safe:
PUTcreates or replaces the resource at this exact address;DELETEremoves it. Both change the server’s state, but a repeat changes nothing more. - Neither:
POSTasks the server to process the body (create something, submit a form, start a job);PATCHchanges part of a resource (RFC 5789);CONNECTopens a tunnel through a proxy, for example for HTTPS. Responses toPOSTandPATCHare cacheable only in rare cases.
Two details trip people up. First, idempotent is about the state of the server, not the response: deleting the
same order twice may return 204 and then 404, yet the order is gone either way. Second, these are promises the
server makes by following the semantics. A GET /cancel-order?id=42 that cancels an order breaks the promise, and
a link prefetcher or crawler that trusts it can cancel orders nobody meant to cancel.
POST is the method to watch. If a payment request times out, the client cannot tell whether the server never saw
it or processed it and lost the reply. Sending it again might charge twice, so clients do not retry a POST
automatically unless they have some other way to know that a repeat is harmless, such as a unique key the server
uses to spot duplicates.
Status codes: five classes
The first digit of a status code gives its class, and a client that does not know a particular code treats it like
the x00 code of its class (RFC 9110, section 15). IANA
keeps the registry of every code.
- 1xx, informational: an interim answer; the real one follows.
100 Continue,101 Switching Protocols,103 Early Hints. - 2xx, success:
200 OK,201 Created,204 No Content,206 Partial Content. - 3xx, redirection: look elsewhere, or use your cached copy.
301,302,303,304 Not Modified,307,308. - 4xx, client error: the request is wrong; do not repeat it unchanged.
400,401,403,404,405,409,412,415,422,429. - 5xx, server error: the request may be fine; the server failed.
500,502,503,504.
The pairs that interviewers ask about:
- 301 or 308, 302 or 307. All four send the client to another address, given in
Location. 301 and 308 mean “moved for good” (clients may update their stored links); 302 and 307 mean “for now”. For historical reasons a client may turn aPOSTinto aGETwhen it follows a 301 or 302. 307 and 308 forbid that, so a form posted to an address that answers 308 is posted again, body and all, to the new one.303 See Otherasks for the opposite: fetch the new address withGET, the usual answer after a form is processed. - 401 or 403.
401 Unauthorizedmeans the request has no valid credentials, and the response must carry aWWW-Authenticateheader that says how to authenticate.403 Forbiddenmeans the server understood the request and refuses it anyway: the credentials it received, if any, do not grant access. A server that does not want to reveal that a forbidden resource exists may answer404instead. - 429 or 503.
429 Too Many Requests(RFC 6585) says this client sent too many requests;503 Service Unavailablesays the server is overloaded or down for maintenance. Both may carryRetry-After, a delay in seconds or a date, and a well-behaved client waits at least that long. - 502 or 504. Both come from a gateway or proxy:
502 Bad Gatewaywhen the server behind it sent something invalid,504 Gateway Timeoutwhen it did not answer in time.
Host, and the rules a server enforces
One machine often serves many sites, so every HTTP/1.1 request must name the site it is for in the Host header.
RFC 9112 is strict here: a client must send exactly one Host, and a server must answer 400 Bad Request to an
HTTP/1.1 request without one or with more than one
(RFC 9112, section 3.2). It also forbids any space
between a field name and its colon, because servers and proxies that handled such spaces differently have been
tricked into treating one request in two ways
(section 5.1).
Not every server applies these rules. The program below sends five hand-written requests to Python’s built-in server and prints what came back:
"""Hand-written requests sent to Python's http.server, and the status line of each answer.
RFC 9112 says an HTTP/1.1 request must carry exactly one Host header, and a server
must answer 400 (Bad Request) when it is missing or repeated. This program sends five
requests over a socket pair (no network) and prints what the server answered, next
to what the RFC asks for.
"""
import http.server
import socket
import threading
class Echo(http.server.BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_GET(self):
hosts = self.headers.get_all("Host") or []
body = f"Host headers seen: {hosts}\n".encode()
self.send_response(200)
self.send_header("Content-Length", str(len(body)))
self.send_header("Connection", "close")
self.end_headers()
self.wfile.write(body)
def log_message(self, *args):
pass
def send(raw):
"""Sends one raw request and returns the status line and the body of the answer."""
client, server_end = socket.socketpair()
worker = threading.Thread(target=Echo, args=(server_end, ("client", 0), None))
worker.start()
client.sendall(raw)
client.shutdown(socket.SHUT_WR) # no more requests on this connection
worker.join() # the handler answers, then stops at the end of the input
server_end.close() # hang up, as a server does after its last response
answer = b""
while chunk := client.recv(4096):
answer += chunk
client.close()
head, _, body = answer.partition(b"\r\n\r\n")
return head.split(b"\r\n")[0].decode(), body.decode().strip()
cases = [
("HTTP/1.1 with one Host", b"GET / HTTP/1.1\r\nHost: shop.example\r\n\r\n", "200"),
("HTTP/1.1 without Host", b"GET / HTTP/1.1\r\n\r\n", "400"),
("HTTP/1.1 with two Hosts", b"GET / HTTP/1.1\r\nHost: shop.example\r\nHost: admin.example\r\n\r\n", "400"),
("HTTP/1.0 without Host", b"GET / HTTP/1.0\r\n\r\n", "200 (Host is optional in 1.0)"),
("Space before the colon", b"GET / HTTP/1.1\r\nHost : shop.example\r\n\r\n", "400"),
]
for name, raw, rfc in cases:
status, body = send(raw)
print(f"{name:24} -> {status:24} RFC 9112 asks for: {rfc}")
print(f"{'':24} {body}") Output
HTTP/1.1 with one Host -> HTTP/1.1 200 OK RFC 9112 asks for: 200
Host headers seen: ['shop.example']
HTTP/1.1 without Host -> HTTP/1.1 200 OK RFC 9112 asks for: 400
Host headers seen: []
HTTP/1.1 with two Hosts -> HTTP/1.1 200 OK RFC 9112 asks for: 400
Host headers seen: ['shop.example', 'admin.example']
HTTP/1.0 without Host -> HTTP/1.1 200 OK RFC 9112 asks for: 200 (Host is optional in 1.0)
Host headers seen: []
Space before the colon -> HTTP/1.1 200 OK RFC 9112 asks for: 400
Host headers seen: []
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 host_rules.py
Python’s server answered 200 OK to all five, including the three that RFC 9112 says must get a 400. For the
request with a space before the colon, its parser did not accept Host : shop.example as a header field and
quietly dropped it, together with any header lines after it, so the program found no Host, while another server
on the same path might have read shop.example. That is not a bug to report: Python’s documentation says http.server is not recommended
for production and implements only basic security checks
(Python docs). It is a reminder that a production server must
check these rules itself, as the exercise below does.
Where the body ends
A connection carries one message after another, so the receiver must find each body’s end exactly. HTTP/1.1 has two ways to say it (RFC 9112, section 6):
Content-Length: n: the body is the nextnbytes. The sender must know the length before it starts.Transfer-Encoding: chunked: the body comes in chunks, each with its size in hexadecimal on a line of its own, and a chunk of size0ends it. A server that generates a page as it goes uses this, as the third response above did.
A request with neither header has no body. A response with neither runs until the server closes the connection,
unless it is one that never has a body, such as the answer to a HEAD request, a 204 or a 304; the close ends
the body and the connection together, so nothing can follow it. A message must never be ambiguous about its length,
and RFC 9112 is strict about the two ways it could be: when both headers arrive, Transfer-Encoding wins and the
message ought to be handled as an error, and when Content-Length arrives with different values the framing is
invalid. A careful server answers 400 Bad Request to such a request and closes the connection, because once two
programs on a connection disagree about where one message ends, they misread every message after it.
Decoding the chunks of the third response above by hand shows how the framing works:
"""Decoding a chunked body by hand: hexadecimal sizes, data, and the zero-size chunk at the end."""
wire = b"4\r\none\n\r\n4\r\ntwo\n\r\n6\r\nthree\n\r\n0\r\n\r\n"
body = b""
rest = wire
while True:
size_line, _, rest = rest.partition(b"\r\n")
size = int(size_line, 16) # the size is written in hexadecimal
print(f"size line {size_line.decode()!r:6} -> {size} bytes")
if size == 0:
break # the last chunk: the body is complete
body += rest[:size]
rest = rest[size + 2:] # skip the data and the CR LF after it
print(f"{len(wire)} bytes on the wire carried a {len(body)}-byte body: {body!r}") Output
size line '4' -> 4 bytes size line '4' -> 4 bytes size line '6' -> 6 bytes size line '0' -> 0 bytes 34 bytes on the wire carried a 14-byte body: b'one\ntwo\nthree\n'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 dechunk.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Each size line is hexadecimal, so a would mean 10 bytes and 1f4 500. The 34 bytes on the wire carried 14 bytes
of body; the rest is framing, the price of not knowing the length in advance.
Exercise · Medium · Python
Parse an HTTP/1.1 request and refuse ambiguous ones
Finish parse_request(data) in request_parser.py. It takes the bytes of one request and returns a dictionary:
"method","target"and"version"from the request line, for example"POST","/notes"and"HTTP/1.1";"headers": field names in lower case, each with its value (the values of a field that appears more than once joined with", ");"body": the body as bytes,b""when there is none.
The starter code already splits the request line and the header lines. Add the rules a careful server applies, and raise ValueError whenever one is broken:
- Only
HTTP/1.1andHTTP/1.0are accepted. - Every header line has a colon, with no space before it (
Host : a.exampleis an error). - An
HTTP/1.1request has exactly oneHostheader.HTTP/1.0may leave it out, but never send two. - A request with both
Content-LengthandTransfer-Encodingis an error: two framing rules could disagree. - With
Transfer-Encoding, the last coding must bechunked. Decode the chunks: each starts with its size in hexadecimal and CR LF (anything after a;on that line is an extension to skip), then the data and CR LF; a chunk of size 0 ends the body. - With
Content-Length, every value must be the same whole number (5twice is fine,5and6is not), and the body is that many bytes.
parse_request(b"POST /notes HTTP/1.1\r\nHost: a.example\r\nContent-Length: 5\r\n\r\nhello")["body"] is b"hello". The six sample tests cover every rule, among them a request whose two length headers disagree.
Starter code · request_parser.py
def parse_request(data):
"""Parse one HTTP/1.1 (or 1.0) request from bytes; raise ValueError for a bad one."""
head, _, rest = data.partition(b"\r\n\r\n")
lines = head.decode("latin-1").split("\r\n")
method, target, version = lines[0].split(" ")
headers = {}
for line in lines[1:]:
name, _, value = line.partition(":")
headers[name.strip().lower()] = value.strip()
# Add the rules of the prompt here, and read the body.
body = b""
return {"method": method, "target": target, "version": version, "headers": headers, "body": body} The sample tests · test_request_parser.py
from request_parser import parse_request
def rejects(data):
"""True when parse_request refuses the request with ValueError."""
try:
parse_request(data)
except ValueError:
return True
return False
def test_request_line_and_headers():
"""splits the request line and keeps header names in lower case"""
r = parse_request(b"GET /docs?page=2 HTTP/1.1\r\nHost: shop.example\r\nAccept: text/html\r\n\r\n")
assert r["method"] == "GET"
assert r["target"] == "/docs?page=2"
assert r["version"] == "HTTP/1.1"
assert r["headers"] == {"host": "shop.example", "accept": "text/html"}
assert r["body"] == b""
def test_content_length_body():
"""reads exactly Content-Length bytes of body"""
r = parse_request(b"POST /notes HTTP/1.1\r\nHost: a.example\r\nContent-Length: 5\r\n\r\nhello")
assert r["body"] == b"hello"
r = parse_request(b"POST /notes HTTP/1.1\r\nHost: a.example\r\nContent-Length: 2\r\n\r\nhello")
assert r["body"] == b"he"
def test_chunked_body():
"""decodes a chunked body, skipping chunk extensions"""
data = b"POST /up HTTP/1.1\r\nHost: a.example\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nnetw\r\n5;note=x\r\norks!\r\n0\r\n\r\n"
assert parse_request(data)["body"] == b"networks!"
data = b"POST /up HTTP/1.1\r\nHost: a.example\r\nTransfer-Encoding: chunked\r\n\r\na\r\n0123456789\r\n0\r\n\r\n"
assert parse_request(data)["body"] == b"0123456789"
def test_host_rules():
"""needs exactly one Host in HTTP/1.1, and never two"""
assert rejects(b"GET / HTTP/1.1\r\n\r\n") is True
assert rejects(b"GET / HTTP/1.1\r\nHost: a.example\r\nHost: b.example\r\n\r\n") is True
assert rejects(b"GET / HTTP/1.0\r\n\r\n") is False
assert rejects(b"GET / HTTP/1.0\r\nHost: a.example\r\nHost: b.example\r\n\r\n") is True
def test_ambiguous_length():
"""refuses a request whose body length could be read two ways"""
both = b"POST / HTTP/1.1\r\nHost: a.example\r\nContent-Length: 5\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n"
assert rejects(both) is True
differ = b"POST / HTTP/1.1\r\nHost: a.example\r\nContent-Length: 5\r\nContent-Length: 6\r\n\r\nhello!"
assert rejects(differ) is True
same = b"POST / HTTP/1.1\r\nHost: a.example\r\nContent-Length: 5\r\nContent-Length: 5\r\n\r\nhello"
assert parse_request(same)["body"] == b"hello"
def test_malformed_requests():
"""refuses broken header lines, versions, lengths and codings"""
assert rejects(b"GET / HTTP/1.1\r\nHost : a.example\r\n\r\n") is True
assert rejects(b"GET / HTTP/1.1\r\nHost: a.example\r\nNoColonHere\r\n\r\n") is True
assert rejects(b"GET / HTTP/2.0\r\nHost: a.example\r\n\r\n") is True
assert rejects(b"POST / HTTP/1.1\r\nHost: a.example\r\nContent-Length: five\r\n\r\nhello") is True
assert rejects(b"POST / HTTP/1.1\r\nHost: a.example\r\nTransfer-Encoding: gzip\r\n\r\nxyz") is True A hint
Collect every header line into a dictionary of lists first: fields.setdefault(name.lower(), []).append(value). Then len(fields.get("host", [])) tells you how many Host headers there were, and fields.get("content-length") and fields.get("transfer-encoding") tell you which framing rule the request asks for, before you read any body bytes.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Connections are reused
In HTTP/1.1 a connection stays open after a response by default
(RFC 9112, section 9.3), so the next request skips the
TCP and TLS handshakes, which cost at least one round trip each, as
the first lesson of this track measured. Either side ends the
connection after a message by sending Connection: close. HTTP/1.0 worked the other way round: one request per
connection, unless both sides agreed to keep it open.
What HTTP/1.1 cannot do well is overlap requests on one connection. A client may send several requests without waiting (this is called pipelining), but the server must send the responses in the same order as the requests, so one slow response holds up every response queued behind it. Clients therefore open a few connections to the same server in parallel instead, and RFC 9112 asks them to keep that number limited. HTTP/2 solves the problem properly by interleaving many requests on a single connection, as a later lesson of this module shows.
Content negotiation
One address can have several representations: HTML or JSON, English or Hindi, compressed or not. The client lists what it accepts, and the server picks one and labels it (RFC 9110, section 12).
- Format: the client sends
Accept, the server labels its choice withContent-Type.Accept: application/jsongetsContent-Type: application/json. - Language:
Accept-LanguageandContent-Language.Accept-Language: hi, en;q=0.8prefers Hindi, then English. - Compression:
Accept-EncodingandContent-Encoding.Accept-Encoding: gzip, brlets the server compress the body.
The q values rank preferences from 0 to 1. When the choice depends on such a request header, the server adds
Vary with that header’s name, for example Vary: Accept-Encoding, so that caches keep the versions apart. A
server that has nothing acceptable may answer 406 Not Acceptable; one that cannot read the body a client sent
answers 415 Unsupported Media Type.
Cookies carry state
HTTP itself remembers nothing between requests. Cookies add memory
(RFC 6265): the server sends Set-Cookie, and the browser sends the
value back in a Cookie header on later requests to the same site.
"""A session cookie: the Set-Cookie header a server sends, and what comes back.
http.cookies builds and parses the headers. The attributes (Path, Max-Age, Secure,
HttpOnly, SameSite) are instructions to the browser and are never sent back: the
browser returns only name=value pairs in the Cookie header.
"""
from http.cookies import SimpleCookie
jar = SimpleCookie()
jar["session"] = "a3f9c2e17b"
jar["session"]["path"] = "/"
jar["session"]["max-age"] = 3600 # seconds: the browser drops it an hour after it arrives
jar["session"]["secure"] = True # only over HTTPS
jar["session"]["httponly"] = True # page scripts cannot read it
jar["session"]["samesite"] = "Lax" # not sent with most cross-site requests
jar["theme"] = "dark"
jar["theme"]["path"] = "/"
print("The server's response carries:")
print(jar.output())
print("\nLater requests from the browser carry one line:")
returned = "session=a3f9c2e17b; theme=dark"
print("Cookie:", returned)
seen = SimpleCookie(returned)
print("\nThe server reads it back as:")
for name, morsel in seen.items():
print(f" {name} = {morsel.value}") Output
The server's response carries: Set-Cookie: session=a3f9c2e17b; HttpOnly; Max-Age=3600; Path=/; SameSite=Lax; Secure Set-Cookie: theme=dark; Path=/ Later requests from the browser carry one line: Cookie: session=a3f9c2e17b; theme=dark The server reads it back as: session = a3f9c2e17b theme = dark
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cookies.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The attributes after the value are instructions to the browser and never come back to the server:
PathandDomainlimit where the browser sends the cookie;Max-Age(in seconds) orExpireslimits how long it keeps it.Securesends it only over HTTPS;HttpOnlyhides it from page scripts, which limits what an injected script can steal.SameSite=Strict,LaxorNonecontrols whether the cookie travels with requests that another site starts, which gives some protection against cross-site request forgery. It is defined in the update of RFC 6265 that the IETF is finishing (draft-ietf-httpbis-rfc6265bis), and that draft has browsers drop aSameSite=Nonecookie that is not alsoSecure.
A related rule is often misunderstood. When a page on one site uses JavaScript to read a response from another
site, the browser checks the response’s Access-Control-Allow-Origin header before it lets the page see the
response. This cross-origin resource sharing (CORS) is defined by the
Fetch Standard and enforced by browsers. It protects the
people using a browser, not the server: curl or any other program ignores it, so a server must still check every
request’s credentials itself.
GATE CS
The GATE 2027 syllabus for Computer Science names exactly two application-layer protocols in its Computer Networks section: DNS and HTTP (syllabus). Methods, status codes, persistent connections and the parts of a message are the HTTP topics to have at your fingertips.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Fresher · What does it mean that PUT is idempotent and POST is not?
An idempotent request can be sent several times with the same effect on the server as sending it once. PUT /users/42 with the same body leaves user 42 in the same state however often it arrives, and DELETE leaves the
resource deleted. That is why a client may repeat them on its own after a dropped connection. POST asks the server
to process the body, and processing it twice can create two orders or two payments, so it is not retried
automatically. The response may differ between repeats (a second DELETE can get 404); idempotence is about the
server’s state, not the response.
Fresher · When does a server answer 401, and when 403?
401 Unauthorized means the request carried no valid credentials: the user is not signed in, or the token has
expired. The response must include a WWW-Authenticate header that says how to authenticate. 403 Forbidden means
the server understood the request and still refuses it: the credentials it received, if any, are not enough, for
example a signed-in user without the admin role. A site that must not reveal that a resource exists may answer 404
instead of 403.
Key takeaways
- Every HTTP/1.1 message is a start line, header fields, an empty line and an optional body, with CR LF line ends. RFC 9110 defines what the parts mean for HTTP/1.1, HTTP/2 and HTTP/3 alike.
- Safe methods only read; idempotent methods (the safe ones plus
PUTandDELETE) can be repeated after a failure;POSTandPATCHare neither, so they are not retried automatically. - The first digit of a status code is its class. Know the pairs: 301 and 308, 302 and 307, 401 and 403, 429 and 503, 502 and 504.
- An HTTP/1.1 request needs exactly one
Hostheader, and a body is framed byContent-Lengthor by chunks, never by both: a receiver that cannot tell where a message ends must refuse it. - HTTP/1.1 reuses connections by default but answers pipelined requests strictly in order, which is why HTTP/2 interleaves them instead.
- Cookies give HTTP memory. Their attributes stay in the browser, and CORS is a browser rule, not a server’s access control.
References
- RFC 9110: HTTP Semantics (RFC Editor (IETF))
- RFC 9112: HTTP/1.1 (RFC Editor (IETF))
- RFC 9113: HTTP/2 (RFC Editor (IETF))
- RFC 9114: HTTP/3 (RFC Editor (IETF))
- RFC 5789: PATCH Method for HTTP (RFC Editor (IETF))
- RFC 6585: Additional HTTP Status Codes (RFC Editor (IETF))
- RFC 6265: HTTP State Management Mechanism (RFC Editor (IETF))
- Cookies: HTTP State Management Mechanism (draft-ietf-httpbis-rfc6265bis) (IETF Datatracker)
- Fetch Standard: CORS protocol (WHATWG)
- Hypertext Transfer Protocol (HTTP) Status Code Registry (IANA)
- http.server: HTTP servers (Python Software Foundation)
- http.client: HTTP protocol client (Python Software Foundation)
- http.cookies: HTTP state management (Python Software Foundation)
- GATE 2027 syllabus: Computer Science and Information Technology (IIT Madras)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress