Type a URL in the browser and a chunk of text flows across the wire — one request line + headers + blank line + optional body. It looks simple, but it hides content negotiation, conditional requests, cookies, preflight, and a stack of underrated headers. This guide opens the request, names the parts, and clarifies the conversations behind GET and POST.
One request on the wire
GET /api/users?page=2 HTTP/1.1
Host: example.com
Accept: application/json
User-Agent: Mozilla/5.0 (...)
Accept-Encoding: gzip, deflate, br
Cookie: session=abc123; theme=dark
Authorization: Bearer eyJhbGciOiJIUzI1Ni...
(blank line — no body)A POST request adds a body:
POST /api/users HTTP/1.1
Host: example.com
Content-Type: application/json
Content-Length: 42
Authorization: Bearer eyJhbGciOiJIUzI1Ni...
{"name":"Alice","email":"a@b.c"}Three parts:
- Request line — method + path + HTTP version
- Headers — name: value lines, terminated by a blank line
- Body — optional. Length set via Content-Length or chunked transfer
Methods — GET, POST, and the rest
| Method | Meaning | Body | Idempotent | Cacheable |
|---|---|---|---|---|
| GET | Retrieve a resource | No | Yes | Yes |
| POST | Create resource / arbitrary action | Yes | No | No |
| PUT | Replace a resource fully | Yes | Yes | No |
| PATCH | Modify part of a resource | Yes | Implementation-dependent | No |
| DELETE | Delete a resource | Optional | Yes | No |
| HEAD | GET's headers only | No | Yes | Yes |
| OPTIONS | Supported methods / CORS preflight | No | Yes | No |
Idempotent — calling N times has the same effect as calling once. That's the safety criterion for network retries.
Never use GET to mutate server state (HTTP/1.1 spec). Search crawlers will hit every GET URL — patterns like ?delete=1 cause real incidents.
Headers — too many to list, but here's the core
General / standard
Host— virtual hosting. Multiple domains on one IP. Required in HTTP/1.1.User-Agent— client identifier (see the User-Agent Parser guide).Accept— MIME types the client will take.application/json, text/html;q=0.9(q = preference).Accept-Language—ko-KR, ko;q=0.9, en;q=0.8. Server picks the locale.Accept-Encoding—gzip, deflate, br, zstd. Server picks the compression.Authorization—Bearer .../Basic .../Digest ....Cookie—name=value; name2=value2(Cookie Parser).Referer— previous page URL (typo from the original spec). Used for analytics and CSRF checks.Origin— the requesting origin. Core of CORS.
Body-related
Content-Type— body MIME:application/json,multipart/form-data,application/x-www-form-urlencoded.Content-Length— body byte count.Transfer-Encoding: chunked— body size unknown ahead of time (streaming).Content-Encoding— gzip / br / zstd applied to body (matching Accept-Encoding).
Conditional / caching
If-None-Match: "etag"— return 304 if unchangedIf-Modified-Since: Date— return 304 if unchanged sinceCache-Control: max-age=3600— caching policy
Modern security / observability
Sec-CH-UAfamily — the User-Agent successor (Client Hints)Sec-Fetch-*— request context (Mode / Dest / Site / User)X-Request-ID— distributed tracing (Datadog, Honeycomb)
Content negotiation — why Accept-* matters
Client → Server:
GET /article/42
Accept: application/json, text/html;q=0.9
Accept-Language: ko-KR, ko;q=0.9
Accept-Encoding: gzip
Server picks:
- /article/42.json (Korean) if available and client prefers JSON
- Compresses with gzip
- Sets Vary: Accept, Accept-Language for cache awarenessq values (quality, 0.0-1.0) express preference. Omitted = 1.0. Server picks the highest-q acceptable representation.
Conditional GET — caching's secret sauce
First request:
GET /image.png
→ 200 OK
ETag: "abc123"
Cache-Control: max-age=86400
After browser cache expires:
GET /image.png
If-None-Match: "abc123"
→ 304 Not Modified (zero-byte body!)304 = "what you have is still current." Saves bandwidth and speeds up page loads. Core CDN mechanic.
POST body — four common encodings
application/json
POST /api/users HTTP/1.1
Content-Type: application/json
{"name":"Alice","age":30}application/x-www-form-urlencoded
POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
name=Alice&password=secret&remember=trueHTML form default. URL-encoded (non-ASCII becomes %xx).
multipart/form-data
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="user"
Alice
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="avatar"; filename="me.jpg"
Content-Type: image/jpeg
(binary image data)
------WebKitFormBoundary7MA4YWxkTrZu0gW--For file uploads. The boundary separates parts.
raw
text/plain / application/xml / any binary. SOAP uses XML.
CORS preflight — what OPTIONS actually does
Cross-origin browser requests with custom headers or non-simple methods trigger a preflight before the real request:
1. Preflight:
OPTIONS /api/data HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: x-custom-header
2. Server response:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: x-custom-header
Access-Control-Max-Age: 86400
3. Real request (after preflight succeeds):
PUT /api/data HTTP/1.1
Origin: https://app.example.com
x-custom-header: value
...See the cors-explained guide for the full story. Quick rules:
- Simple requests (GET/HEAD with standard headers) skip preflight
- Custom headers or PUT/DELETE/PATCH require preflight
- Max-Age caches the preflight result (usually a day)
HTTP/1.1 vs HTTP/2 vs HTTP/3
- HTTP/1.1 (1997) — text-based, one request per connection (Keep-Alive enables sequential pipelining). This guide's wire format.
- HTTP/2 (2015) — binary frames, multiplexing (many requests over one connection). Header compression (HPACK).
- HTTP/3 (2022) — runs over QUIC (UDP). Integrated TLS. Lower connection-setup RTT. Robust to mobile packet loss.
Semantics (methods, headers, bodies) are identical across the three. Only the wire format changes. This guide uses 1.1 for clarity.
Seeing the wire with curl
curl -v https://example.com/api/data
# -v shows the wire trace
# > lines are the request
# < lines are the response
curl -X POST https://example.com/api/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer abc123" \
-d '{"name":"Alice"}'cURL Builder turns a URL + method + headers + body into a curl command — handy when sharing requests in tickets.
Common pitfalls
1. Missing Content-Type
POST with a body but no Content-Type → server rejects (415 Unsupported Media Type) or parses it wrong.
2. Cookie's SameSite / Secure missing
Without SameSite=None; Secure, cross-site requests don't carry cookies. Independent of CORS.
3. Authorization case sensitivity
Authorization: Bearer abc123 ← standard
Authorization: bearer abc123 ← some servers reject
authorization: Bearer abc123 ← header NAMES are case-insensitive4. GET query string length
Safe range is 2,000-8,000 bytes. Some servers and proxies truncate beyond that. Large parameters belong in a POST body.
5. Server ignores Accept
Sent Accept: application/json but got HTML back → client parse error. Verify the server contract.
6. CORS that only checks Origin
An attacker on the same Origin's different path slips through. Origin + path + custom header in combination.
References
- RFC 9110 (HTTP semantics) — datatracker
- MDN HTTP headers — MDN
- CORS spec — WHATWG
- curl manual — curl.se
Summary
- HTTP request = request line + headers + blank line + optional body. Text-based in HTTP/1.1.
- Method properties differ — idempotent / cacheable. Never use GET to mutate state.
- Content negotiation — Accept / Accept-Language / Accept-Encoding with q-value preference.
- Conditional GET — ETag / If-None-Match → 304 Not Modified. The CDN superpower.
- POST bodies use four common encodings — JSON, form-urlencoded, multipart, raw.
- CORS preflight (OPTIONS) for anything beyond simple requests.
- HTTP/2 and 3 keep semantics identical, only the wire changes.
- Try it: cURL Builder / Cookie Parser / User-Agent Parser / HTTP Status Codes.