HTTP in Depth: Methods, Status Codes, Headers, Caching, Cookies, and CORS
HTTP is the protocol almost every web request rides on, but most applications only expose a small part of it. The difficult bugs usually appear at the edges: a retry creates a duplicate order, a cache serves the wrong representation, or a browser sends a request that the API processes but the frontend cannot read.
This is Part 2 of the How the Web Works series. Part 1 mapped the journey from the address bar to the screen. This article focuses on the contract between client and server: methods, status codes, headers, caching, cookies, CORS, and the places where a technically valid request can still produce a surprising result.
Quick Answer
HTTP is a request-response protocol. A client sends a method, a path, headers, and optionally a body. A server replies with a status code, headers, and optionally a body. Caching, cookies, CORS, and content negotiation are mostly driven by headers.
Client Server
| |
| GET /blog/engineering HTTP/1.1|
| Host: suriyaprakash.in |
| Accept: text/html |
|------------------------------->|
| |
| HTTP/1.1 200 OK |
| Content-Type: text/html |
| Cache-Control: max-age=300 |
| <html>...</html> |
|<-------------------------------|
HTTP request semantics are stateless: each request contains the information needed to interpret it, and the protocol does not require conversational state between requests. Applications can still keep server-side state and correlate requests using cookies, sessions, or tokens.
HTTP Methods and When to Use Them
Methods describe intent, not just action. Using the wrong one breaks caching, retries, and tooling that assume standard behavior.
| Method | Purpose | Safe? | Idempotent? |
|---|---|---|---|
| GET | Retrieve a resource | Yes | Yes |
| HEAD | Same as GET, headers only | Yes | Yes |
| POST | Create a resource / trigger an action | No | No |
| PUT | Replace a resource entirely | No | Yes |
| PATCH | Partially update a resource | No | No (usually) |
| DELETE | Remove a resource | No | Yes |
| OPTIONS | Ask what's allowed (used by CORS preflight) | Yes | Yes |
Safe means the client is not asking for a state change; incidental effects such as logging are still allowed. Browsers, crawlers, and proxies may invoke safe methods speculatively. Idempotent means repeated identical requests have the same intended effect as one request, although individual responses can differ. Retrying an idempotent operation is generally safer, but clients still need to consider concurrency and partial failures. Retrying a POST blindly can create duplicate orders.
A common production bug: treating POST as idempotent when a client retries a network timeout, resulting in duplicate charges or duplicate rows. The fix is usually an idempotency key sent by the client, not relying on the method semantics alone.
Anatomy of an HTTP Request and Response
A raw HTTP/1.1 request looks like this:
POST /api/orders HTTP/1.1
Host: api.suriyaprakash.in
Content-Type: application/json
Authorization: Bearer eyJhbGciOi...
Content-Length: 42
{"item":"widget","quantity":2}
And the response:
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/orders/8231
Cache-Control: no-store
{"id":8231,"item":"widget","quantity":2}
Four parts matter on each side:
- Start line: method + path + version (request), or version + status (response)
- Headers: metadata as key-value pairs
- Blank line: separates headers from body
- Body: optional payload (JSON, HTML, form data, binary)
In HTTP/2 and HTTP/3 this is no longer literally sent as plain text. It is split into frames, and headers are compressed with HPACK for HTTP/2 or QPACK for HTTP/3. The logical model above still holds and is what your application code usually sees.
Status Codes: What Each Range Actually Means
Status codes are grouped by their first digit. The class gives a broad outcome, but it does not always identify who is "at fault."
| Range | Meaning | Example |
|---|---|---|
| 1xx | Informational, request still in progress | 100 Continue |
| 2xx | Success | 200 OK, 201 Created, 204 No Content |
| 3xx | Redirection | 301 Moved Permanently, 302 Found, 304 Not Modified |
| 4xx | The request cannot be fulfilled because of a client-side condition | 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 429 Too Many Requests |
| 5xx | The server or an upstream component failed while handling the request | 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout |
A few distinctions that trip people up in real systems:
- 401 vs 403:
401means the request lacks valid authentication credentials and normally includes an authentication challenge where applicable.403means the server understood the request but refuses to authorize it; re-authenticating with the same identity may not help. - 301/308 vs 302/307:
301and308describe permanent redirects;302and307describe temporary redirects.307and308explicitly preserve the method and body, while user agents historically rewrite somePOSTrequests toGETafter301or302. Temporary redirects can still be cached when explicit caching information permits it, so "temporary" does not mean "never cached." - 502 vs 503 vs 504:
502means a gateway or proxy received an invalid upstream response.503means the service is currently unavailable, for example because of overload or maintenance, and can includeRetry-After.504means a gateway or proxy did not receive a timely upstream response. Rate-limit rejection is normally429, not503.
Headers That Matter Most
Headers are where most of HTTP's real behavior lives. A few worth knowing well:
Content-Type: application/json; charset=utf-8
Tells the receiver how to parse the body. A wrong Content-Type is a classic cause of "the API works in Postman but not in my frontend."
Authorization: Bearer <token>
Carries credentials such as a JWT, API key, or session token. Never log this header in plaintext.
Cache-Control: max-age=300, public
Controls caching behavior (covered in depth below).
Vary: Accept-Encoding, Accept-Language
Tells caches that the selected representation depends on those request headers. For user-specific responses, do not rely on Vary: Authorization as the primary safety control; use an appropriate policy such as Cache-Control: private or no-store unless shared caching of authenticated responses has been designed deliberately.
ETag: "33a64df551"
A fingerprint of the response body, used for conditional requests (see below).
Caching: Browser Cache, CDN Cache, and Cache-Control
Caching is one of the biggest performance levers in a web stack, and also one of the easiest things to get subtly wrong.
The Cache-Control directives that matter most
| Directive | Meaning |
|---|---|
no-store | Caches must not store the response |
no-cache | A stored response must be successfully revalidated before reuse |
private | Shared caches must not store it; a private cache such as the browser may |
public | Explicitly permits shared caching, subject to the other caching rules |
max-age=N | The response is fresh for N seconds |
s-maxage=N | Freshness lifetime for shared caches; it overrides max-age there |
immutable | While the response is fresh, the client need not revalidate it; use with versioned URLs |
A confusing but important detail: no-cache does not mean "don't cache." It means "cache it, but check with the server before serving it again." That check happens via conditional requests.
Conditional requests (revalidation)
GET /style.css HTTP/1.1
If-None-Match: "33a64df551"
If the resource hasn't changed, the server replies:
HTTP/1.1 304 Not Modified
with no response content. The client reuses its stored representation. A 304 often means revalidation is working and avoided a full transfer, although a fresh response can be reused without any network request at all.
Cache layers in a real system
Browser HTTP cache (memory or disk)
Service worker Cache API (if the worker chooses to use it)
CDN or shared-proxy cache
Reverse-proxy cache
Application cache (Redis, in-process cache, etc.)
Origin application and database
This is a set of possible layers, not one universal fixed order: a service worker can intercept a fetch and decide whether to use its own cache, the browser HTTP cache, or the network. When a change does not appear, identify which layer served the response by inspecting DevTools and cache-related response headers.
Cookies: Session State Over a Stateless Protocol
HTTP does not require conversational memory between requests. Cookies are one mechanism for carrying an identifier or small piece of state across requests.
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax; Max-Age=3600; Path=/
| Attribute | Meaning |
|---|---|
HttpOnly | Prevents JavaScript APIs from reading the cookie; XSS can still perform authenticated actions |
Secure | Sent only over secure transport, with limited localhost exceptions in some browsers |
SameSite=Strict | Withheld on cross-site requests |
SameSite=Lax | Generally sent on same-site requests and cross-site top-level navigations using safe methods |
SameSite=None | Allows cross-site sending and requires Secure, but browser privacy policy can still block or partition it |
Max-Age / Expires | How long the cookie persists |
Path / Domain | Which URLs the cookie is sent to |
SameSite affected many embedded widgets and cross-site authentication flows when browsers began treating an omitted value as Lax in common cases. A conventional unpartitioned cookie intended for a cross-site iframe generally needs SameSite=None; Secure, but that is not a guarantee: third-party-cookie restrictions, cookie partitioning, or APIs such as the Storage Access API may also determine whether it is available.
Cookies are also why CORS requests distinguish between "with credentials" and "without." Sending cookies cross-origin requires the client to opt in with credentials: 'include' and the server to explicitly allow it.
CORS: Why Cross-Origin Requests Get Blocked
CORS (Cross-Origin Resource Sharing) is a browser security feature, not a server or network limitation. The browser blocks the response from reaching your JavaScript, but the request often still happens.
A request is cross-origin when the scheme, host, or port differs from the page making it. https://app.example.com calling https://api.example.com is cross-origin even though they're "the same company."
Simple requests vs preflighted requests
Some requests, such as basic GET/POST with safelisted headers, go straight through. More complex requests, such as custom headers, Content-Type: application/json, or methods like PUT and DELETE, trigger a preflight. The browser first sends an OPTIONS request asking permission.
OPTIONS /api/orders HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
The server must respond with explicit permission:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Credentials: true
If a preflight response does not grant permission, the browser does not send the actual request. For a CORS-safelisted request, or for an actual request sent after a successful preflight, the server may process the request even if the final response lacks the headers JavaScript needs to read it. That is how a POST can change data while the frontend reports a CORS failure.
Do: use a specific allowed origin rather than * when credentials mode is enabled; browsers reject the wildcard combination with credentialed CORS. Also send Vary: Origin when the allowed origin is selected dynamically and the response can pass through a cache.
Don't: hide the symptom with a browser extension or mode: 'no-cors'. Fix the server policy, use a same-origin backend proxy, or redesign the request so it is not cross-origin.
Content Negotiation
The client advertises preferences through request headers, and the server selects a representation and describes it with response headers.
Accept: application/json
Accept-Language: en-IN, en;q=0.9
Accept-Encoding: gzip, br
The server responds with whichever format it picked:
Content-Type: application/json
Content-Language: en
Content-Encoding: br
Accept-Encoding is important for performance: compressible text responses are usually much smaller with Brotli or gzip. When representation selection depends on request headers such as Accept-Encoding or Accept-Language, the response should normally include the corresponding Vary fields so shared caches do not mix variants.
HTTP/1.1 vs HTTP/2 vs HTTP/3
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| Transport | TCP | TCP | QUIC (UDP) |
| Multiplexing | No practical multiplexing in browsers; several connections are commonly used | Yes, many streams share one connection | Yes, many streams share one QUIC connection |
| Header compression | None in the protocol | HPACK | QPACK |
| Head-of-line blocking | Requests on a connection serialize; pipelining was rarely usable | Removed between HTTP streams, but TCP loss can stall the whole connection | Independent stream ordering avoids TCP-style cross-stream blocking, though congestion and connection-level limits still affect all streams |
| Deployment role | Compatibility baseline | Widely deployed | Widely supported, with deployment depending on network and server support |
The practical takeaway: HTTP/2 multiplexing is why "reduce the number of requests" matters less than it used to for HTTP/2+ origins. Bundling every asset into one giant file can hurt on modern connections; smaller chunks often let HTTP/2/3 work better.
Practical Debugging Checklist
When something HTTP-related is misbehaving:
1. Check the actual request and response
curl -v https://api.suriyaprakash.in/orders
Look at: method, status code, all response headers, and whether the body matches expectations.
2. Is it a caching issue?
- Check
Cache-Control,ETag, and whether you're getting304s when you expect200s (or vice versa) - Check
Vary: is the cache correctly keyed per user/format? - Hard refresh (bypass browser cache) to rule out the browser layer specifically
3. Is it a cookie issue?
- DevTools → Application → Cookies: check
SameSite,Secure,HttpOnly, and expiry - Confirm the request is same-site if
SameSite=Lax/Strictis set
4. Is it CORS?
- DevTools console will name the exact missing/mismatched header
- Check the Network tab for the
OPTIONSpreflight response, not just the main request
5. Is it a status code mismatch?
- Use 4xx vs 5xx as a starting clue, not a complete blame assignment; proxies, authentication layers, and application policy can complicate the source
- Check whether a
3xxredirect is looping (redirect loops usually show up as the browser giving up after ~20 hops)
The Parts That Usually Cause Confusion
A short explanation is not enough when a request fails in a real application:
HTTP is a request-response protocol with methods, headers, and status codes.
The details that matter most in practice are:
- Idempotency and safety guarantees per method, and why they matter for retries
- The difference between
no-cacheandno-store, and why304is a good sign SameSitecookie behavior and its interaction with iframes and CORS- Why a CORS error can appear even after the server successfully processed the request
- The performance implications of HTTP/2 multiplexing on bundling strategy
FAQ
What's the difference between PUT and PATCH?
PUT replaces the entire resource with what you send. PATCH applies a partial update. Sending a PUT with only some fields typically means the missing fields get wiped out or reset to defaults, depending on the server implementation.
Why does my request work in Postman but fail in the browser?
API clients such as Postman do not enforce the browser's CORS model and do not automatically reproduce browser cookie rules. CORS and cookie attributes are common explanations, but also compare redirects, proxy settings, TLS trust, automatically added headers, and the exact request body.
Does a CORS error mean the server didn't receive the request?
It depends. A CORS-safelisted request is usually sent and its response may then be hidden from JavaScript. A request that requires preflight is not sent when the preflight fails. Check the Network panel and server logs rather than inferring this from the console message alone.
What does no-cache mean?
It means "cache this, but revalidate with the server on every use before serving it," not "don't cache it." no-store is the directive that means don't cache at all.
Why is 401 different from 403?
401 Unauthorized means the request lacks valid authentication. 403 Forbidden means the server recognizes who's asking, but that identity isn't allowed to perform the action.
Is HTTP/2 always faster than HTTP/1.1?
Usually, because of multiplexing over a single connection, but not always. On very lossy networks, a single lost packet can stall every multiplexed stream in HTTP/2 due to TCP-level head-of-line blocking. This is one reason HTTP/3 exists.
Why do some requests trigger a preflight and others don't?
CORS-safelisted requests use GET, HEAD, or POST and must satisfy restrictions on author-controlled headers and, where present, Content-Type. Methods such as PUT, DELETE, and PATCH, non-safelisted headers such as Authorization, and application/json normally trigger an OPTIONS preflight.
Glossary
| Term | Simple meaning |
|---|---|
| Idempotent | Calling it once or many times has the same effect |
| Status code | Three-digit outcome code returned with every HTTP response |
| Header | Key-value metadata sent with a request or response |
| Cache-Control | Header controlling how and for how long a response may be cached |
| ETag | A fingerprint of a response body used for conditional requests |
| Cookie | Small piece of state a server asks the browser to store and resend |
| SameSite | Cookie attribute restricting when it's sent on cross-site requests |
| CORS | Browser mechanism controlling whether cross-origin JS can read a response |
| Preflight | An OPTIONS request asking permission before a "non-simple" cross-origin request |
| Content negotiation | Client and server agreeing on format, language, and encoding |
| HPACK / QPACK | Header compression schemes used by HTTP/2 and HTTP/3 |
Final Mental Model
Method + headers + body
↓
Sent over HTTP/1.1, HTTP/2, or HTTP/3
↓
Server applies auth, CORS, and routing rules
↓
Status code + headers + body returned
↓
Browser applies caching, cookie, and CORS rules
↓
Your application code finally sees the result
Many "weird" HTTP bugs come from a mismatch between server policy, intermediary behavior, and browser enforcement. Once you identify the responsible layer, a vague CORS or caching complaint becomes a specific check of origin policy, credentials mode, freshness, cache scope, and variant selection.
Series: How the Web Works
Part 2 of 7