C.W.K.
Stream
Lesson 02 of 07 · published

Request & Response Anatomy — The Four Parts That Never Change

~11 min · foundations, anatomy, request, response

Level 0HTTP Newbie
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"Every HTTP message — request or response, 1990 or 2026, plain or encrypted — has the same four-part shape. Memorize the shape once; the rest is just filling it in."

One Shape, Forever

An HTTP message has exactly four parts, in this order:

  1. Start line — one line that says what kind of message this is.
  2. Headers — zero or more key: value pairs.
  3. Blank line — a literal CRLF that says headers are done, body begins next.
  4. Body — optional payload. Can be empty, JSON, form data, an image, a stream of events, anything.

That's it. Every HTTP/1.1 message has those four parts. HTTP/2 and HTTP/3 frame the same content as binary blocks for speed, but the parsed shape your code sees is identical. Learn the shape; everything else is variation on a theme.

Start Line — The Only Place Request and Response Differ

The first line is the only place a request and a response look structurally different.

Request start line has three tokens: METHOD SP URI SP HTTP-VERSION. Example: POST /api/chat HTTP/1.1. That says "I want to POST to /api/chat using HTTP/1.1."

Response start line has three different tokens: HTTP-VERSION SP STATUS-CODE SP REASON-PHRASE. Example: HTTP/1.1 201 Created. That says "Here's an HTTP/1.1 response, status 201, which means Created." The reason phrase is human-text only — clients should never branch on it; branch on the code.

Headers — Metadata About the Message

Headers describe the message and how to handle it. Keys are case-insensitive on the wire (Content-Type, content-type, CONTENT-TYPE all mean the same thing). Values can be anything ASCII; commas can pack multiple values into one header.

Two categories matter most early on:

  • Representation metadata (formerly "entity headers") — what's in the body. Content-Type, Content-Length, Content-Encoding. These can appear on requests OR responses, anywhere there's a body.
  • Control headers — how to handle the request itself. Host, Authorization, Accept, User-Agent on requests. Server, Set-Cookie, Cache-Control on responses.

The Blank Line — Stupid but Mandatory

One single CRLF (Carriage Return + Line Feed) separates the headers from the body. Miss it and the server thinks your body is another header line. Most modern clients write it for you, but if you're hand-rolling a request over a TCP socket, forgetting the blank line is the #1 newbie bug.

Headers say what; body shows what. If your client crashes parsing the body, your first debug move is to print the Content-Type response header. The header is the truth about the body's format; trying to response.json() a text/html body is the most common cause of "the API gave me garbage."

Body — Optional, Described by Headers

A body is optional. GET requests typically have none. DELETE often has none. But every body that exists is described by at least one of two headers: Content-Length (fixed-size body, in bytes) or Transfer-Encoding: chunked (streaming body, length not known up front). cwkPippa's SSE responses use Transfer-Encoding: chunked — same four-part anatomy, just a body that streams event-by-event over a long-lived connection.

The Whole Thing Visualized

Read these byte-by-byte and you've read every HTTP message you'll ever encounter.

Code

Annotated request — four parts, top to bottom·http
POST /api/chat HTTP/1.1                       <- start line
Host: localhost:8000                          <- header
Content-Type: application/json                <- header
Content-Length: 47                            <- header
Authorization: Bearer abc.def.ghi             <- header
                                              <- blank line (CRLF)
{"conversation_id":"xyz","message":"Hi"}     <- body
Annotated response — same four parts, different start line·http
HTTP/1.1 201 Created                          <- start line
Content-Type: application/json                <- header (about body)
Content-Length: 89                            <- header (about body)
Location: /api/chat/messages/m_42             <- header (control)
Date: Sun, 25 May 2026 03:46:50 GMT           <- header (control)
                                              <- blank line (CRLF)
{"id":"m_42","role":"assistant","content":"Hi 아빠."}  <- body
Same anatomy, accessed through Python httpx·python
import httpx

resp = httpx.post(
    'http://localhost:8000/api/chat',
    json={'conversation_id': 'xyz', 'message': 'Hi'},
    headers={'Authorization': 'Bearer abc.def.ghi'},
)

# Start line pieces
print(resp.http_version)      # 'HTTP/1.1'
print(resp.status_code)       # 201
print(resp.reason_phrase)     # 'Created' (don't branch on this)

# Headers — case-insensitive dict
print(resp.headers['content-type'])  # 'application/json'
print(resp.headers['Content-Length'])  # also works

# Body — the headers tell us how to parse it
if resp.headers.get('content-type', '').startswith('application/json'):
    print(resp.json())
else:
    print(resp.text)  # fall back to raw text
Streaming body: same anatomy, chunked transfer·python
# Streaming body — cwkPippa's SSE response uses chunked transfer.
# Same four-part anatomy; body just arrives in chunks over time.
with httpx.stream('POST', 'http://localhost:8000/api/chat',
                  json={'message': 'Tell me a story'}) as resp:
    print(resp.headers.get('content-type'))  # 'text/event-stream'
    print(resp.headers.get('transfer-encoding'))  # 'chunked'
    for chunk in resp.iter_text():
        print(chunk, end='', flush=True)
    # Body has no Content-Length — the server doesn't know up front

External links

Exercise

Pick any API you've used (your own, cwkPippa, GitHub's, anything). Capture one request with curl -v or DevTools. Label the four parts of BOTH the request and the response by hand — write them out in a comment block. Then deliberately send a malformed request: keep the JSON body but change Content-Type to text/plain. What status code comes back? Why?
Hint
The malformed Content-Type usually triggers a 415 Unsupported Media Type or a 422 — depends on the server's strictness. FastAPI is strict; an Express server without body-parser middleware might just ignore the body silently and return 200 with empty fields. That difference is itself a lesson in why the header matters.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.