Loading content...
Every interaction on the web—every page load, API call, image download, and form submission—consists of HTTP messages flowing between clients and servers. Understanding the precise structure of these messages is fundamental to debugging network issues, building APIs, and comprehending how the web actually works at the protocol level.
HTTP messages are simple text-based protocols (in HTTP/1.x) or binary-framed (in HTTP/2 and HTTP/3), but they share a common conceptual structure:
┌──────────────────────────────┐
│ Start Line │ ← Request line or Status line
├──────────────────────────────┤
│ Headers │ ← Key: Value pairs (metadata)
│ Headers │
│ Headers │
├──────────────────────────────┤
│ │
│ Body │ ← Payload data (optional)
│ (Optional) │
│ │
└──────────────────────────────┘
This page provides a comprehensive, authoritative examination of HTTP message format—how requests and responses are structured, encoded, and transmitted across the network.
By the end of this page, you will: • Understand the complete structure of HTTP requests and responses • Parse and construct HTTP messages manually • Recognize differences between HTTP/1.1, HTTP/2, and HTTP/3 message formats • Debug HTTP issues using raw message analysis • Apply best practices for message construction in APIs
An HTTP request is a message sent from client to server, asking for a resource or action. In HTTP/1.1 (the most human-readable version), a request has this structure:
Method SP Request-Target SP HTTP-Version CRLF
Header-Field CRLF
Header-Field CRLF
...
CRLF
[message-body]
Where:
1234567891011
POST /api/users HTTP/1.1Host: api.example.comContent-Type: application/jsonContent-Length: 89Accept: application/jsonAccept-Encoding: gzip, deflate, brAuthorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...User-Agent: MyApp/1.0 (Windows NT 10.0; Win64; x64)X-Request-ID: 7a3b2c1d-4e5f-6789-abcd-ef0123456789 {"username": "johndoe", "email": "john@example.com", "preferences": {"theme": "dark"}}The first line of every HTTP request contains three components:
POST /api/users HTTP/1.1
└──┘ └─────────┘ └───────┘
│ │ └── HTTP Version
│ └── Request Target (path + query)
└── Method (verb)
Method: The action to perform (GET, POST, PUT, DELETE, etc.)
Request-Target: Usually the path component of the URL:
/path/to/resource?query=value (most common)http://example.com/path (used with proxies)example.com:443 (CONNECT method only)* (OPTIONS for the server itself)HTTP-Version: HTTP/1.0, HTTP/1.1, or HTTP/2 (though HTTP/2 is binary, tools often display it this way)
Headers follow the request line, each on its own line:
Header-Name: Header-Value CRLF
Key rules:
Content-Type = content-type)Minimum required headers (HTTP/1.1):
GET / HTTP/1.1
Host: example.com
The Host header is required in HTTP/1.1 because:
Host, the server wouldn't know which site you wantAfter the blank line, the optional body contains the payload:
Content-Type: application/json
Content-Length: 45
{"username": "john", "password": "secret123"}
Body rules:
Content-Length specifies exact byte countTransfer-Encoding: chunked allows streaming without knowing total sizeYou can send raw HTTP requests using tools like netcat or telnet:
echo -e "GET / HTTP/1.1\r
Host: example.com\r
\r
" | nc example.com 80
Or use curl --verbose to see the exact request being sent:
curl -v https://api.example.com/users
An HTTP response follows a similar structure to requests, with the first line being the status line instead of the request line:
HTTP-Version SP Status-Code SP Reason-Phrase CRLF
Header-Field CRLF
Header-Field CRLF
...
CRLF
[message-body]
1234567891011121314151617
HTTP/1.1 200 OKDate: Sat, 18 Jan 2025 12:00:00 GMTServer: nginx/1.24.0Content-Type: application/json; charset=utf-8Content-Length: 156Cache-Control: private, max-age=3600ETag: "abc123def456"X-Request-ID: 7a3b2c1d-4e5f-6789-abcd-ef0123456789Connection: keep-alive { "id": 12345, "username": "johndoe", "email": "john@example.com", "createdAt": "2025-01-18T12:00:00Z", "status": "active"}HTTP/1.1 200 OK
└───────┘ └─┘ └┘
│ │ └── Reason Phrase (human-readable, optional)
│ └── Status Code (3 digits)
└── HTTP Version
HTTP-Version: Must match the highest version the server supports for that request
Status-Code: The 3-digit result code (200, 404, 500, etc.)
Reason-Phrase: Human-readable explanation
Response headers provide metadata about the response:
Date: Sat, 18 Jan 2025 12:00:00 GMT # When response was generated
Server: nginx/1.24.0 # Server software (optional)
Content-Type: application/json; charset=utf-8 # Body format
Content-Length: 156 # Body size in bytes
Cache-Control: private, max-age=3600 # Caching instructions
ETag: "abc123def456" # Resource version identifier
Date header: Required for responses with caching implications. Format: RFC 5322 date (e.g., Sat, 18 Jan 2025 12:00:00 GMT).
The body appears after the blank line, with size determined by:
Content-Length header (fixed size)Transfer-Encoding: chunked (streaming)| Status Code | Body Allowed? | Notes |
|---|---|---|
| 1xx Informational | No | Never contains a body |
| 2xx Success | Usually | 204 No Content has no body |
| 3xx Redirection | Usually not | May contain redirect message |
| 4xx Client Error | Usually | Error details in body |
| 5xx Server Error | Usually | Error details in body |
| 204 No Content | Never | Must not have body |
| 304 Not Modified | Never | Client uses cached body |
The message body can be transmitted in several ways, each with different characteristics and use cases.
The simplest approach—declare the exact byte count:
Content-Length: 156
{"id": 12345, "name": "John Doe", ...}
Pros:
Cons:
For dynamic content where size isn't known in advance:
Transfer-Encoding: chunked
1a
This is the first chunk.
15
This is chunk two.
0
Format:
chunk-size (hex) CRLF
chunk-data CRLF
...
0 CRLF
CRLF
12345678910111213141516171819202122232425
HTTP/1.1 200 OKContent-Type: text/plainTransfer-Encoding: chunked 7\r Hello, \r 6\r World!\r 0\r \r # Decoded result: "Hello, World!" # Chunk breakdown:# "7" = 7 bytes in hex (7 in decimal)# "Hello, " = 7 bytes# "6" = 6 bytes in hex (6 in decimal) # "World!" = 6 bytes# "0" = Final chunk (0 bytes = end of body)Separate from transfer encoding, content encoding compresses the body:
Content-Type: application/json
Content-Encoding: gzip
Content-Length: 312 # Compressed size
(gzip-compressed JSON data)
Common encodings:
gzip — GNU zip, widely supporteddeflate — zlib compression (less common)br — Brotli, best compression ratio for textidentity — No compression (default)Combining with chunked:
Transfer-Encoding: chunked
Content-Encoding: gzip
(chunked, gzip-compressed data)
When determining body length, HTTP clients follow this priority:
Important: Transfer-Encoding takes precedence over Content-Length. If both present, Content-Length should be ignored.
Disagreements between how different servers interpret Content-Length and Transfer-Encoding can lead to HTTP Request Smuggling attacks.
If a frontend proxy sees:
Content-Length: 44
Transfer-Encoding: chunked
And interprets Content-Length, while the backend interprets chunked, an attacker can "smuggle" a second request inside the first.
Prevention: Reject requests with both headers, or ensure consistent handling across all servers in the chain.
Let's examine complete HTTP conversations for common scenarios.
1234567891011121314151617181920212223242526
# RequestGET /api/products/42 HTTP/1.1Host: api.store.comAccept: application/jsonAccept-Encoding: gzip, deflate, brUser-Agent: Mozilla/5.0 Chrome/120.0.0.0Authorization: Bearer token123If-None-Match: "v1-etag-abc" # Response (Cache Hit - Not Modified)HTTP/1.1 304 Not ModifiedDate: Sat, 18 Jan 2025 12:00:00 GMTETag: "v1-etag-abc"Cache-Control: private, max-age=3600 # Response (Cache Miss - Full Response)HTTP/1.1 200 OKDate: Sat, 18 Jan 2025 12:00:00 GMTContent-Type: application/json; charset=utf-8Content-Encoding: gzipContent-Length: 412ETag: "v2-etag-xyz"Cache-Control: private, max-age=3600Vary: Accept-Encoding (gzip-compressed JSON body)123456789101112131415161718192021222324252627282930313233343536
# RequestPOST /api/orders HTTP/1.1Host: api.store.comContent-Type: application/jsonContent-Length: 156Accept: application/jsonAuthorization: Bearer token123Idempotency-Key: order-7a3b2c1d-unique { "productId": 42, "quantity": 2, "shippingAddress": { "city": "San Francisco", "zip": "94102" }} # Response (Created)HTTP/1.1 201 CreatedDate: Sat, 18 Jan 2025 12:00:00 GMTContent-Type: application/jsonContent-Length: 234Location: /api/orders/78901X-Request-ID: req-abc123 { "orderId": 78901, "status": "pending", "total": 59.98, "createdAt": "2025-01-18T12:00:00Z", "links": { "self": "/api/orders/78901", "payment": "/api/orders/78901/pay" }}12345678910111213141516171819202122232425262728293031323334
# RequestPOST /api/documents HTTP/1.1Host: api.example.comContent-Type: multipart/form-data; boundary=----Boundary123Content-Length: 2456Authorization: Bearer token123 ------Boundary123Content-Disposition: form-data; name="title" Annual Report 2024------Boundary123Content-Disposition: form-data; name="file"; filename="report.pdf"Content-Type: application/pdf %PDF-1.7(binary PDF content...)%%EOF------Boundary123-- # ResponseHTTP/1.1 201 CreatedDate: Sat, 18 Jan 2025 12:00:00 GMTContent-Type: application/jsonLocation: /api/documents/doc-xyz789 { "documentId": "doc-xyz789", "title": "Annual Report 2024", "filename": "report.pdf", "size": 2048576, "mimeType": "application/pdf", "uploadedAt": "2025-01-18T12:00:00Z"}12345678910111213141516171819202122232425
# RequestPOST /api/users HTTP/1.1Host: api.example.comContent-Type: application/jsonContent-Length: 45 {"email": "invalid", "password": "123"} # Response (Validation Error)HTTP/1.1 422 Unprocessable EntityDate: Sat, 18 Jan 2025 12:00:00 GMTContent-Type: application/problem+jsonContent-Length: 312 { "type": "https://api.example.com/errors/validation", "title": "Validation Failed", "status": 422, "detail": "Request body contains invalid data", "instance": "/api/users", "errors": [ {"field": "email", "code": "invalid_format", "message": "Invalid email format"}, {"field": "password", "code": "too_short", "message": "Password must be at least 8 characters"} ]}While HTTP/1.1 uses text-based messages, HTTP/2 and HTTP/3 use binary framing for efficiency. The semantic meaning remains the same, but the wire format is fundamentally different.
| Aspect | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| Format | Text-based | Binary frames | Binary frames (QUIC) |
| Headers | Plaintext, each request | HPACK compressed | QPACK compressed |
| Multiplexing | None (pipelining failed) | Streams over single TCP | Streams over QUIC |
| Request line | GET /path HTTP/1.1 | :method: GET, :path: /path | :method: GET, :path: /path |
| Status line | HTTP/1.1 200 OK | :status: 200 | :status: 200 |
| Connection | Per-request or keep-alive | Single connection, many streams | QUIC connection, many streams |
HTTP/2 replaces the request line and status line with pseudo-headers (prefixed with :):
Request pseudo-headers:
:method: GET
:scheme: https
:authority: api.example.com
:path: /api/users?id=123
Response pseudo-headers:
:status: 200
Note: The reason phrase ("OK", "Not Found") is not transmitted in HTTP/2+.
HTTP/2 divides messages into frames:
+-----------------------------------------------+
| Frame Header (9 bytes) |
+-----------------------------------------------+
| Length (24) | Type (8) | Flags (8) | |
+-----------------------------------------------+
| Stream Identifier (32) |
+-----------------------------------------------+
| Frame Payload |
+-----------------------------------------------+
Frame types:
DATA: Request/response bodyHEADERS: HTTP headersPRIORITY: Stream prioritySETTINGS: Connection settingsPUSH_PROMISE: Server pushGOAWAY: Graceful shutdownWINDOW_UPDATE: Flow controlCONTINUATION: Large header continuation12345678910111213141516171819202122
# HTTP/2 Request (as shown by debugging tools):method: POST:scheme: https:authority: api.example.com:path: /api/userscontent-type: application/jsonaccept: application/jsonauthorization: Bearer token123 {"username": "johndoe", "email": "john@example.com"} # HTTP/2 Response:status: 201content-type: application/jsonlocation: /api/users/12345 {"id": 12345, "username": "johndoe", "createdAt": "2025-01-18T12:00:00Z"} # What actually goes on the wire:# - HEADERS frame with HPACK-compressed pseudo-headers and headers# - DATA frame with the body# - Both use stream ID to associate request/responseHTTP/1.1: Headers sent as plain text on every request, often repeating (Host, User-Agent, Cookie, etc.)
HTTP/2 (HPACK): Headers are:
HTTP/3 (QPACK): Similar to HPACK but designed to work with QUIC's independent streams (avoids head-of-line blocking on header compression).
# HTTP/1.1: Sequential requests
Connection ─────┬─── Request 1 ─── Response 1
├─── Request 2 ─── Response 2 (must wait)
└─── Request 3 ─── Response 3 (must wait)
# HTTP/2: Multiplexed streams over single connection
Connection ─────┬─── Stream 1: Request 1 ────────────
├─── Stream 3: Request 2 ─────────
├─── Stream 5: Request 3 ──────────
└─── (responses interleaved)
Each stream is independent; a slow response doesn't block others.
Since HTTP/2+ is binary, you can't use telnet to debug. Use:
• Browser DevTools: Network tab shows HTTP/2 as normal headers
• curl: curl --http2 -v https://example.com
• Wireshark: Decodes HTTP/2 frames (requires decryption for HTTPS)
• nghttp: HTTP/2 command-line tool
• h2load: HTTP/2 benchmarking tool
Understanding how HTTP connections work is essential for performance optimization and debugging.
Original HTTP opened a new TCP connection for each request:
1. TCP handshake (3 packets)
2. Send HTTP request
3. Receive HTTP response
4. TCP teardown (4 packets)
5. Repeat for next request
Problem: TCP handshake and slow-start made this extremely inefficient for pages with many resources.
HTTP/1.1 made persistent connections the default:
Connection: keep-alive # Default in HTTP/1.1
Multiple requests can use the same connection:
1. TCP handshake
2. Request 1 → Response 1
3. Request 2 → Response 2
4. Request 3 → Response 3
...
N. Connection idle → close
Connection header values:
keep-alive: Maintain connection (default)close: Close after this responseHTTP/1.1 also defined pipelining—sending multiple requests without waiting for responses:
Client: Request 1
Client: Request 2 (without waiting)
Client: Request 3 (without waiting)
Server: Response 1
Server: Response 2
Server: Response 3
Problem: Responses must come in order. A slow Response 1 blocks Response 2 and 3 (head-of-line blocking). Most browsers disabled pipelining.
| Version | Connections | Multiplexing | Head-of-Line Blocking |
|---|---|---|---|
| HTTP/1.0 | One per request | No | N/A |
| HTTP/1.1 | Persistent, 6-8 per domain | No (pipelining failed) | Yes, at HTTP level |
| HTTP/2 | Single per domain | Yes, streams | Yes, at TCP level |
| HTTP/3 | Single (QUIC) | Yes, independent streams | No (per-stream) |
Browsers limit concurrent connections per domain:
| Browser | Connections per domain | Total connections |
|---|---|---|
| Chrome | 6 | 256 |
| Firefox | 6 | 256 |
| Safari | 6 | Varies |
Workarounds (HTTP/1.1 era):
static1.example.com, static2.example.comHTTP/2 solution: Single connection, unlimited multiplexed streams (typically 100-256 concurrent).
To explicitly close after a response:
# Request
GET /final-request HTTP/1.1
Host: example.com
Connection: close
# Response
HTTP/1.1 200 OK
Connection: close
Content-Length: 42
(body)
(connection closed after response)
Mastering debugging tools is essential for working with HTTP at the protocol level.
12345678910111213141516171819202122
$ curl -v https://api.example.com/users/123 * Trying 93.184.216.34:443...* Connected to api.example.com (93.184.216.34) port 443* TLS 1.3 connection using TLS_AES_256_GCM_SHA384* Server certificate: api.example.com> GET /users/123 HTTP/2> Host: api.example.com> User-Agent: curl/8.4.0> Accept: */*>< HTTP/2 200 < date: Sat, 18 Jan 2025 12:00:00 GMT< content-type: application/json< content-length: 89<{"id": 123, "name": "John Doe", "email": "john@example.com"} # Key:# > = Request (client to server)# < = Response (server to client)# * = Connection info| Tool | Use Case | Key Features |
|---|---|---|
| Browser DevTools | Web debugging | Network tab, timing, headers, preview |
| curl | Command-line requests | -v verbose, --trace raw bytes, -i show headers |
| httpie | Human-friendly CLI | Colored output, JSON by default |
| Postman/Insomnia | API testing GUI | Collections, environments, history |
| Wireshark | Packet-level analysis | Full protocol decode, filtering |
| mitmproxy | HTTP proxy/intercept | Modify requests in-flight |
| Charles Proxy | macOS proxy | SSL proxying, throttling |
| Fiddler | Windows proxy | Full HTTP debugging, scripting |
"Why isn't my request working?"
"Why isn't caching working?"
"Why is my request slow?"
To see the exact bytes sent/received:
curl --trace - https://example.com
This shows hexadecimal dumps of the actual wire format, useful for debugging encoding issues or binary protocols.
Understanding HTTP message format at the protocol level enables effective debugging, API design, and performance optimization.
Congratulations! You have completed the HTTP Methods and Status module. You now have a comprehensive understanding of:
• HTTP methods (GET, POST, PUT, DELETE) and their semantic properties • HTTP headers for caching, security, content negotiation, and CORS • Status codes across all five categories (1xx-5xx) • Content types and MIME type structure • HTTP request and response message format
This foundation prepares you for advanced topics like HTTP/1.1, HTTP/2, HTTP/3, and HTTPS covered in subsequent modules.