HTTP Request/Response Lifecycle
You are debugging a failing API call. You run
curl -v https://api.example.com/usersand the terminal fills with lines starting with>,<, and*. Headers, status codes, connection info — a wall of text.Most engineers skim this output or pipe it to
/dev/null. But every line tells you something specific: which HTTP method was used, what the server accepted, how the response was encoded, whether caching is involved, and where the connection came from.This lesson decodes the full HTTP request/response lifecycle. By the end, you will be able to read
curl -voutput like a book and immediately spot misconfigurations, missing headers, and protocol issues.
Part 1: The HTTP Conversation Model
HTTP is a request/response protocol. The client sends a request, the server sends a response. That is it. No server-initiated messages (in HTTP/1.1), no push notifications, no bidirectional streaming. One request in, one response out.
Every HTTP request has three parts:
- Request line — method, path, HTTP version
- Headers — metadata about the request
- Body (optional) — data being sent to the server
Every HTTP response has three parts:
- Status line — HTTP version, status code, reason phrase
- Headers — metadata about the response
- Body (optional) — data being sent back to the client
HTTP Request/Response Lifecycle
Click each step to explore
For a single HTTPS request, the client must complete DNS resolution, TCP handshake, and TLS handshake before any HTTP data flows. That is 3-5 round trips of latency before the first byte of your API response. This is why HTTP keep-alive (reusing connections) and HTTP/2 (multiplexing) exist — they amortize these setup costs across many requests.
Part 2: HTTP Methods — What Each One Means
HTTP defines several methods (also called "verbs"). Each has specific semantics that matter for caching, idempotency, and API design.
The Methods You Use Daily
| Method | Purpose | Has body? | Idempotent? | Safe? |
|---|---|---|---|---|
| GET | Retrieve a resource | No (technically allowed, but avoided) | Yes | Yes |
| POST | Create a resource or trigger an action | Yes | No | No |
| PUT | Replace a resource entirely | Yes | Yes | No |
| PATCH | Update part of a resource | Yes | Not guaranteed | No |
| DELETE | Remove a resource | Rarely | Yes | No |
| HEAD | Same as GET but response has no body | No | Yes | Yes |
| OPTIONS | Ask what methods are allowed (used by CORS) | No | Yes | Yes |
Idempotent means: calling it multiple times has the same effect as calling it once. PUT /users/123 {"name": "Alice"} always results in the user being named Alice, no matter how many times you call it. POST /users {"name": "Alice"} might create a new Alice each time.
Safe means: the method does not modify server state. GET and HEAD should never cause side effects.
# GET — retrieve users
curl -X GET https://api.example.com/users
# POST — create a user
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "alice@example.com"}'
# PUT — replace user 123 entirely
curl -X PUT https://api.example.com/users/123 \
-H "Content-Type: application/json" \
-d '{"name": "Alice Updated", "email": "alice@example.com", "role": "admin"}'
# PATCH — update one field of user 123
curl -X PATCH https://api.example.com/users/123 \
-H "Content-Type: application/json" \
-d '{"role": "admin"}'
# DELETE — remove user 123
curl -X DELETE https://api.example.com/users/123
# HEAD — check if resource exists (no body returned)
curl -I https://api.example.com/users/123
# OPTIONS — what methods are supported? (CORS preflight)
curl -X OPTIONS https://api.example.com/users -v
Idempotency matters for retry logic. If a POST request times out, you do not know whether the server processed it — retrying might create a duplicate. If a PUT request times out, retrying is safe because the result is the same. Design your APIs and retry strategies around this distinction. Many production incidents are caused by retrying non-idempotent requests.
Part 3: Request Headers — What You Send
Headers are key-value pairs that provide metadata about the request. Some are set automatically by the HTTP client; others you set explicitly.
Essential Request Headers
# The full curl -v output for a typical API request:
curl -v -H "Authorization: Bearer eyJhbG..." \
-H "Accept: application/json" \
https://api.example.com/users
# > GET /users HTTP/2
# > Host: api.example.com
# > User-Agent: curl/8.4.0
# > Accept: application/json
# > Authorization: Bearer eyJhbG...
| Header | Purpose | Example |
|---|---|---|
Host | Which domain the request is for (required in HTTP/1.1) | api.example.com |
Authorization | Authentication credentials | Bearer eyJhbG... or Basic dXNlcjpwYXNz |
Content-Type | MIME type of the request body | application/json, multipart/form-data |
Accept | What response format the client wants | application/json, text/html |
User-Agent | Client software identification | curl/8.4.0, Mozilla/5.0... |
X-Request-ID | Unique ID for distributed tracing | 550e8400-e29b-41d4-a716-446655440000 |
Cache-Control | Caching directives from the client | no-cache, max-age=0 |
Cookie | Session cookies from previous responses | session_id=abc123 |
The Host header is how virtual hosting works. A single IP address (and thus a single Kubernetes Ingress) can serve traffic for dozens of domains. The Host header tells the server (or ingress controller) which domain the client wants. Without it, the server would not know which backend to route to. In HTTP/2, this is replaced by the :authority pseudo-header.
Content-Type — The Most Mismatched Header
Content-Type mismatches are one of the most common causes of 400 Bad Request and 415 Unsupported Media Type errors:
# WRONG: sending JSON body with no Content-Type
curl -X POST https://api.example.com/users \
-d '{"name": "Alice"}'
# Server may interpret this as form-urlencoded, not JSON → 400 Bad Request
# CORRECT: explicitly set Content-Type
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice"}'
Common Content-Type values:
| Content-Type | Used for |
|---|---|
application/json | JSON APIs (most modern APIs) |
application/x-www-form-urlencoded | HTML form submissions |
multipart/form-data | File uploads |
text/plain | Plain text |
application/octet-stream | Binary data |
application/grpc | gRPC over HTTP/2 |
A team spent two days debugging why their webhook integration was failing. The upstream service was sending JSON payloads but with Content-Type: text/plain. Their ingress controller had a WAF rule that only allowed JSON parsing for application/json content type. The webhook body was being treated as opaque text, not parsed, and the backend received an empty object. The fix was adding text/plain to the WAF's JSON content type list. Always log the Content-Type header when debugging request parsing issues.
Part 4: Response Headers — What You Get Back
Response headers control caching, security, rate limiting, and more.
Essential Response Headers
# Typical response headers from curl -v:
# < HTTP/2 200
# < content-type: application/json; charset=utf-8
# < content-length: 1234
# < cache-control: public, max-age=300
# < x-request-id: 550e8400-e29b-41d4-a716-446655440000
# < x-ratelimit-limit: 100
# < x-ratelimit-remaining: 97
# < x-ratelimit-reset: 1699900800
# < set-cookie: session_id=abc123; Path=/; HttpOnly; Secure
# < strict-transport-security: max-age=31536000; includeSubDomains
| Header | Purpose | Why it matters |
|---|---|---|
Content-Type | MIME type of the response body | Client needs this to parse the response correctly |
Content-Length | Size of the response in bytes | Enables progress bars, detects truncated responses |
Cache-Control | How long the response can be cached | max-age=300 means cache for 5 minutes |
Set-Cookie | Send cookies to the client | Session management, authentication tokens |
X-RateLimit-* | Rate limiting information | How many requests remain before throttling |
Strict-Transport-Security | Force HTTPS for future requests | Prevents downgrade attacks |
X-Request-ID | Trace ID for this request | Critical for distributed tracing across microservices |
Always propagate X-Request-ID (or your tracing header of choice) through your entire microservice chain. When a request passes through ingress, API gateway, service A, service B, and a database — having a single ID that appears in every log makes debugging production issues orders of magnitude faster. If the incoming request does not have one, generate a UUID and attach it.
Part 5: Decoding curl -v Output Line by Line
The curl -v command shows the complete HTTP conversation. Here is how to read every line:
curl -v https://api.example.com/users/123
* Trying 203.0.113.50:443... # DNS resolved, attempting TCP connection
* Connected to api.example.com # TCP handshake complete
* ALPN: offers h2,http/1.1 # Client offers HTTP/2 and HTTP/1.1
* TLSv1.3 (OUT), TLS handshake # TLS negotiation begins
* TLSv1.3 (IN), TLS handshake # Server responds with certificate
* SSL certificate verify ok # Certificate chain valid
* using HTTP/2 # HTTP/2 negotiated via ALPN
> GET /users/123 HTTP/2 # REQUEST: method, path, version
> Host: api.example.com # REQUEST: virtual host header
> User-Agent: curl/8.4.0 # REQUEST: client identification
> Accept: */* # REQUEST: accept any content type
< HTTP/2 200 # RESPONSE: status code
< content-type: application/json # RESPONSE: body is JSON
< content-length: 89 # RESPONSE: body is 89 bytes
< cache-control: private, max-age=60 # RESPONSE: cache for 60s, private only
{"id": 123, "name": "Alice", "email": "alice@example.com", "role": "admin"}
The prefixes tell you what is happening:
*— curl internal info (connection, TLS, protocol negotiation)>— data sent FROM client TO server (your request)<— data sent FROM server TO client (the response)- No prefix — the response body
When debugging, always use curl -v (or curl -vvv for even more detail). The response body alone does not tell you the full story. Headers reveal caching behavior, rate limits, redirects, content encoding, and authentication state. A 200 response with Cache-Control: public, max-age=3600 might explain why your users see stale data — they are getting a cached response, not a fresh one.
Part 6: Cookies, Sessions, and Statelessness
HTTP is a stateless protocol. Each request is independent — the server does not inherently remember previous requests from the same client. Sessions and cookies are hacks to add statefulness on top of a stateless protocol.
How Cookies Work
- Server sends
Set-Cookie: session_id=abc123; Path=/; HttpOnly; Securein a response - Client stores the cookie
- Client includes
Cookie: session_id=abc123in every subsequent request to that domain - Server looks up
abc123in its session store to identify the user
# See cookies being set and sent
curl -v -c cookies.txt https://api.example.com/login \
-H "Content-Type: application/json" \
-d '{"email": "alice@example.com"}'
# < Set-Cookie: session_id=abc123; Path=/; HttpOnly; Secure
# Use the cookie in the next request
curl -v -b cookies.txt https://api.example.com/users/me
# > Cookie: session_id=abc123
Cookie Security Flags
| Flag | Purpose | When to use |
|---|---|---|
HttpOnly | Cookie not accessible via JavaScript | Always for session cookies (prevents XSS theft) |
Secure | Only sent over HTTPS | Always in production |
SameSite=Strict | Not sent on cross-origin requests | Prevents CSRF attacks |
SameSite=Lax | Sent on top-level navigations but not AJAX | Default in modern browsers |
Path=/ | Cookie sent for all paths | Usually what you want |
Domain=.example.com | Cookie sent to all subdomains | Only if needed |
In Kubernetes microservice architectures, session cookies become problematic because requests may hit different pods. Either use sticky sessions (ingress affinity annotations), a shared session store (Redis), or better yet — switch to stateless authentication with JWTs or API tokens. Stateless auth works naturally with horizontal scaling because no session state needs to be shared between pods.
Part 7: CORS — Cross-Origin Resource Sharing
CORS is the mechanism by which a browser allows (or blocks) JavaScript on one domain from making requests to a different domain. It is enforced by the browser, not the server.
The CORS Flow
- Your frontend is at
https://app.example.com - It tries to call
https://api.example.com/users(different origin) - For "non-simple" requests (anything with custom headers, JSON body, or non-GET methods), the browser first sends an OPTIONS preflight request
- The server responds with CORS headers telling the browser what is allowed
- If the CORS headers allow it, the browser sends the actual request
# The preflight request (sent automatically by the browser)
# > OPTIONS /users HTTP/2
# > Host: api.example.com
# > Origin: https://app.example.com
# > Access-Control-Request-Method: POST
# > Access-Control-Request-Headers: Content-Type, Authorization
# The server's response (must include CORS headers)
# < HTTP/2 204
# < Access-Control-Allow-Origin: https://app.example.com
# < Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
# < Access-Control-Allow-Headers: Content-Type, Authorization
# < Access-Control-Max-Age: 86400
| Header | Direction | Purpose |
|---|---|---|
Origin | Request | Browser sends the origin of the calling page |
Access-Control-Allow-Origin | Response | Server says which origins are allowed |
Access-Control-Allow-Methods | Response | Which HTTP methods are allowed |
Access-Control-Allow-Headers | Response | Which request headers are allowed |
Access-Control-Max-Age | Response | How long the browser should cache the preflight result |
Access-Control-Allow-Credentials | Response | Whether cookies are allowed in cross-origin requests |
A deployment went out where the API server's CORS configuration was set to Access-Control-Allow-Origin: * (allow everything) alongside Access-Control-Allow-Credentials: true (allow cookies). This combination is explicitly forbidden by the CORS spec — browsers reject it. The result: every authenticated API call from the frontend silently failed. The browser console showed "CORS error" but no one checked it because the API worked fine from curl (which does not enforce CORS). The fix was replacing the wildcard with the explicit frontend origin. Always test CORS from a browser, not from curl.
CORS is a browser-only security mechanism. curl, wget, Postman, and any non-browser HTTP client completely ignore CORS headers. If your API "works from curl but not from the browser," CORS is almost certainly the issue. Check the browser console for CORS errors and ensure your API responds to OPTIONS preflight requests with the correct headers.
Key Concepts Summary
- HTTP is a request/response protocol — client sends a request (method, headers, body), server sends a response (status, headers, body)
- The first HTTPS request requires DNS + TCP + TLS before any HTTP data flows — 3-5 round trips of latency
- HTTP methods have semantic meaning: GET reads, POST creates, PUT replaces, PATCH updates, DELETE removes
- Idempotency determines retry safety: PUT and DELETE are safe to retry, POST is not
- Content-Type mismatches cause 400/415 errors — always set it explicitly when sending request bodies
- curl -v shows the full conversation:
*is connection info,>is what you sent,<is what you received - HTTP is stateless — cookies and sessions add statefulness on top, but create scaling challenges
- CORS is browser-enforced — it does not affect curl or server-to-server calls. Test from a real browser.
- X-Request-ID should propagate through every service for distributed tracing
Common Mistakes
- Not setting
Content-Type: application/jsonwhen sending JSON bodies — the server may misinterpret the data - Confusing PUT (full replacement) with PATCH (partial update) — sending a partial object to a PUT endpoint may erase fields
- Retrying POST requests after a timeout without idempotency keys — this creates duplicate resources
- Testing CORS with curl and assuming it works — curl does not enforce CORS, only browsers do
- Using
Access-Control-Allow-Origin: *with credentials — the spec forbids this combination - Not propagating trace IDs (X-Request-ID) across microservices — makes debugging production issues nearly impossible
- Caching authenticated responses with
Cache-Control: public— other users may see private data from shared caches
Your frontend at https://app.example.com calls your API at https://api.example.com. The request works from curl but fails in the browser with a CORS error. What is the most likely cause?