Networking Fundamentals for DevOps Engineers

HTTP Request/Response Lifecycle

You are debugging a failing API call. You run curl -v https://api.example.com/users and 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 -v output 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:

  1. Request line — method, path, HTTP version
  2. Headers — metadata about the request
  3. Body (optional) — data being sent to the server

Every HTTP response has three parts:

  1. Status line — HTTP version, status code, reason phrase
  2. Headers — metadata about the response
  3. Body (optional) — data being sent back to the client

HTTP Request/Response Lifecycle

Click each step to explore

KEY CONCEPT

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

MethodPurposeHas body?Idempotent?Safe?
GETRetrieve a resourceNo (technically allowed, but avoided)YesYes
POSTCreate a resource or trigger an actionYesNoNo
PUTReplace a resource entirelyYesYesNo
PATCHUpdate part of a resourceYesNot guaranteedNo
DELETERemove a resourceRarelyYesNo
HEADSame as GET but response has no bodyNoYesYes
OPTIONSAsk what methods are allowed (used by CORS)NoYesYes

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
PRO TIP

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...
HeaderPurposeExample
HostWhich domain the request is for (required in HTTP/1.1)api.example.com
AuthorizationAuthentication credentialsBearer eyJhbG... or Basic dXNlcjpwYXNz
Content-TypeMIME type of the request bodyapplication/json, multipart/form-data
AcceptWhat response format the client wantsapplication/json, text/html
User-AgentClient software identificationcurl/8.4.0, Mozilla/5.0...
X-Request-IDUnique ID for distributed tracing550e8400-e29b-41d4-a716-446655440000
Cache-ControlCaching directives from the clientno-cache, max-age=0
CookieSession cookies from previous responsessession_id=abc123
KEY CONCEPT

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-TypeUsed for
application/jsonJSON APIs (most modern APIs)
application/x-www-form-urlencodedHTML form submissions
multipart/form-dataFile uploads
text/plainPlain text
application/octet-streamBinary data
application/grpcgRPC over HTTP/2
WAR STORY

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
HeaderPurposeWhy it matters
Content-TypeMIME type of the response bodyClient needs this to parse the response correctly
Content-LengthSize of the response in bytesEnables progress bars, detects truncated responses
Cache-ControlHow long the response can be cachedmax-age=300 means cache for 5 minutes
Set-CookieSend cookies to the clientSession management, authentication tokens
X-RateLimit-*Rate limiting informationHow many requests remain before throttling
Strict-Transport-SecurityForce HTTPS for future requestsPrevents downgrade attacks
X-Request-IDTrace ID for this requestCritical for distributed tracing across microservices
PRO TIP

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
WARNING

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

  1. Server sends Set-Cookie: session_id=abc123; Path=/; HttpOnly; Secure in a response
  2. Client stores the cookie
  3. Client includes Cookie: session_id=abc123 in every subsequent request to that domain
  4. Server looks up abc123 in 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
FlagPurposeWhen to use
HttpOnlyCookie not accessible via JavaScriptAlways for session cookies (prevents XSS theft)
SecureOnly sent over HTTPSAlways in production
SameSite=StrictNot sent on cross-origin requestsPrevents CSRF attacks
SameSite=LaxSent on top-level navigations but not AJAXDefault in modern browsers
Path=/Cookie sent for all pathsUsually what you want
Domain=.example.comCookie sent to all subdomainsOnly if needed
PRO TIP

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

  1. Your frontend is at https://app.example.com
  2. It tries to call https://api.example.com/users (different origin)
  3. For "non-simple" requests (anything with custom headers, JSON body, or non-GET methods), the browser first sends an OPTIONS preflight request
  4. The server responds with CORS headers telling the browser what is allowed
  5. 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
HeaderDirectionPurpose
OriginRequestBrowser sends the origin of the calling page
Access-Control-Allow-OriginResponseServer says which origins are allowed
Access-Control-Allow-MethodsResponseWhich HTTP methods are allowed
Access-Control-Allow-HeadersResponseWhich request headers are allowed
Access-Control-Max-AgeResponseHow long the browser should cache the preflight result
Access-Control-Allow-CredentialsResponseWhether cookies are allowed in cross-origin requests
WAR STORY

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.

WARNING

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/json when 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

KNOWLEDGE CHECK

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?