HTTP in Depth: Methods, Status Codes, Headers, Caching, Cookies, and CORS

16 min read

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.

MethodPurposeSafe?Idempotent?
GETRetrieve a resourceYesYes
HEADSame as GET, headers onlyYesYes
POSTCreate a resource / trigger an actionNoNo
PUTReplace a resource entirelyNoYes
PATCHPartially update a resourceNoNo (usually)
DELETERemove a resourceNoYes
OPTIONSAsk what's allowed (used by CORS preflight)YesYes

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."

RangeMeaningExample
1xxInformational, request still in progress100 Continue
2xxSuccess200 OK, 201 Created, 204 No Content
3xxRedirection301 Moved Permanently, 302 Found, 304 Not Modified
4xxThe request cannot be fulfilled because of a client-side condition400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 429 Too Many Requests
5xxThe server or an upstream component failed while handling the request500 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: 401 means the request lacks valid authentication credentials and normally includes an authentication challenge where applicable. 403 means the server understood the request but refuses to authorize it; re-authenticating with the same identity may not help.
  • 301/308 vs 302/307: 301 and 308 describe permanent redirects; 302 and 307 describe temporary redirects. 307 and 308 explicitly preserve the method and body, while user agents historically rewrite some POST requests to GET after 301 or 302. Temporary redirects can still be cached when explicit caching information permits it, so "temporary" does not mean "never cached."
  • 502 vs 503 vs 504: 502 means a gateway or proxy received an invalid upstream response. 503 means the service is currently unavailable, for example because of overload or maintenance, and can include Retry-After. 504 means a gateway or proxy did not receive a timely upstream response. Rate-limit rejection is normally 429, not 503.

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

DirectiveMeaning
no-storeCaches must not store the response
no-cacheA stored response must be successfully revalidated before reuse
privateShared caches must not store it; a private cache such as the browser may
publicExplicitly permits shared caching, subject to the other caching rules
max-age=NThe response is fresh for N seconds
s-maxage=NFreshness lifetime for shared caches; it overrides max-age there
immutableWhile 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=/
AttributeMeaning
HttpOnlyPrevents JavaScript APIs from reading the cookie; XSS can still perform authenticated actions
SecureSent only over secure transport, with limited localhost exceptions in some browsers
SameSite=StrictWithheld on cross-site requests
SameSite=LaxGenerally sent on same-site requests and cross-site top-level navigations using safe methods
SameSite=NoneAllows cross-site sending and requires Secure, but browser privacy policy can still block or partition it
Max-Age / ExpiresHow long the cookie persists
Path / DomainWhich 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.1HTTP/2HTTP/3
TransportTCPTCPQUIC (UDP)
MultiplexingNo practical multiplexing in browsers; several connections are commonly usedYes, many streams share one connectionYes, many streams share one QUIC connection
Header compressionNone in the protocolHPACKQPACK
Head-of-line blockingRequests on a connection serialize; pipelining was rarely usableRemoved between HTTP streams, but TCP loss can stall the whole connectionIndependent stream ordering avoids TCP-style cross-stream blocking, though congestion and connection-level limits still affect all streams
Deployment roleCompatibility baselineWidely deployedWidely 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 getting 304s when you expect 200s (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
  • DevTools → Application → Cookies: check SameSite, Secure, HttpOnly, and expiry
  • Confirm the request is same-site if SameSite=Lax/Strict is set

4. Is it CORS?

  • DevTools console will name the exact missing/mismatched header
  • Check the Network tab for the OPTIONS preflight 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 3xx redirect 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-cache and no-store, and why 304 is a good sign
  • SameSite cookie 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

TermSimple meaning
IdempotentCalling it once or many times has the same effect
Status codeThree-digit outcome code returned with every HTTP response
HeaderKey-value metadata sent with a request or response
Cache-ControlHeader controlling how and for how long a response may be cached
ETagA fingerprint of a response body used for conditional requests
CookieSmall piece of state a server asks the browser to store and resend
SameSiteCookie attribute restricting when it's sent on cross-site requests
CORSBrowser mechanism controlling whether cross-origin JS can read a response
PreflightAn OPTIONS request asking permission before a "non-simple" cross-origin request
Content negotiationClient and server agreeing on format, language, and encoding
HPACK / QPACKHeader 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.


About the author

Suriyaprakash Somu is a full-stack developer from Erode, Tamil Nadu, building production-ready business applications with React, Node.js, Fastify, PostgreSQL and MySQL. He focuses on Access Control, schema-based forms, and reliable backend workflows.