Understanding HTTP Headers: A Developer's Reference

Understanding HTTP Headers: A Developer's Reference

Request Headers vs Response Headers

Every HTTP transaction involves two sets of headers: the ones your client sends with the request, and the ones the server sends back. They serve different purposes, and knowing which does what saves time when something isn't working.

Request headers give the server context about who's asking and what they want. Response headers tell the client what it got back, how to handle it, and what it's allowed to do with it. Request headers travel up, response headers travel down, and both sides read each other's metadata before processing the body.

Headers are just text: Name: value\r\n, one per line, case-insensitive names by convention (though HTTP/2 lowercases them all). Understanding the key ones is genuinely useful day-to-day, not just for debugging.

The Most Important Request Headers

Content-Type tells the server what format the request body is in. If you're sending JSON, it should be application/json. For form submissions, application/x-www-form-urlencoded or multipart/form-data. Getting this wrong is one of the most common causes of a backend silently receiving empty or malformed data.

POST /api/users HTTP/1.1
Content-Type: application/json

{"name": "Alice", "email": "alice@example.com"}

Accept tells the server what response formats the client can handle. Most APIs accept this gracefully and respond with Content-Type: application/json regardless, but some REST APIs will content-negotiate and return XML if you ask for it.

Authorization carries credentials. The two most common formats are Bearer tokens (JWTs, OAuth access tokens) and Basic auth (base64-encoded username:password). More on both below.

Cookie sends stored cookies back to the server with every request in scope. Session cookies, auth tokens, tracking identifiers — they all ride in this header. The browser manages this automatically; you rarely need to set it manually unless you're writing tests or a non-browser client.

User-Agent identifies the client. Browsers send verbose strings like Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 .... Your app can send whatever makes sense: MyApp/2.1.0, curl/8.4.0, etc. Some servers make decisions based on this — serve different content to bots, throttle certain clients.

Origin is sent on cross-origin requests and is the foundation of CORS. It tells the server where the request originated: Origin: https://yourapp.com. The server uses this to decide whether to allow the request. Unlike Referer, Origin only includes the scheme, host, and port — no path.

The Most Important Response Headers

Content-Type in a response tells the client how to interpret the body. application/json, text/html; charset=utf-8, image/webp, application/pdf. The ;charset=utf-8 suffix matters. Without it, some browsers default to ISO-8859-1 for text/* types, which mangles anything outside the ASCII range.

Cache-Control controls caching at every level (browser, CDN, reverse proxy). The values compose:

Cache-Control: public, max-age=31536000, immutable  # long-lived static asset
Cache-Control: no-cache                              # revalidate every time
Cache-Control: no-store                              # never cache (auth pages)
Cache-Control: private, max-age=300                 # browser only, 5 min

Set-Cookie creates or updates a cookie. The flags on this header are critical for security: HttpOnly prevents JavaScript from reading it (protects against XSS), Secure restricts it to HTTPS, and SameSite=Lax or SameSite=Strict prevents it from being sent on cross-site requests (protects against CSRF).

Set-Cookie: session=abc123; Path=/; HttpOnly; Secure; SameSite=Lax

Strict-Transport-Security (HSTS) tells browsers to only connect over HTTPS for a specified duration. Once a browser has seen this header, it won't allow HTTP connections to the host — not even if the user types http://. The includeSubDomains flag extends this to all subdomains.

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

X-Content-Type-Options: nosniff tells the browser not to MIME-sniff the response — trust the Content-Type header exactly. Without it, browsers try to auto-detect content type, which can lead to script injection if an attacker uploads a file that looks like JavaScript.

CORS headers are the response side of cross-origin resource sharing. For requests that trigger a preflight (non-simple methods or custom headers), the browser first sends an OPTIONS request and checks these headers before sending the actual request:

Access-Control-Allow-Origin: https://yourapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

Access-Control-Allow-Origin: * means any origin can access the resource — fine for public APIs, wrong for anything that uses cookies or per-user data.

Authorization Header Formats

Three patterns you'll encounter regularly:

Basic auth encodes username:password in Base64 and sends it on every request. Simple, stateless, and fine over HTTPS. Never over plain HTTP.

Authorization: Basic dXNlcjpwYXNzd29yZA==

The encoded value is just user:password through Base64 — not encrypted, just encoded. You can verify or generate these with the Base64 Encoder.

Bearer tokens are the modern standard. The token is opaque to the client — it might be a JWT, an OAuth access token, or a random string that maps to a session in a database.

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

API keys are often sent in a custom header rather than Authorization, though there's no universal convention:

X-API-Key: my-secret-api-key
Authorization: ApiKey my-secret-api-key

When you're working with raw JSON responses from APIs and need to inspect authorization payloads or token structures, JSON Formatter makes decoded JWT payloads much easier to read.

Inspecting Headers in DevTools

Chrome and Firefox both make this easy. Open DevTools with F12 or Cmd+Option+I, go to the Network tab, click any request, and look at the Headers panel.

The Headers tab shows both request and response headers. The Preview and Response tabs show the body. For XHR / fetch requests specifically, filter by Fetch/XHR to reduce noise.

For APIs and scripting, curl -v prints full headers:

curl -v https://api.example.com/users \
  -H "Authorization: Bearer my-token" \
  -H "Accept: application/json"

The lines prefixed with > are request headers; < are response headers. Or use curl -I for a HEAD request to see response headers without downloading the body.

If you're decoding URL-encoded header values — authentication challenges, redirect locations, encoded cookie values — URL Encoder handles both encoding and decoding in the browser.

Common Header Debugging Scenarios

CORS errors almost always surface in the browser console as blocked by CORS policy. Check Access-Control-Allow-Origin in the response (or its absence) and the Origin in the request. If the server doesn't return the right Allow-Origin value, the browser blocks the response even if it received HTTP 200.

401 Unauthorized means the request lacked valid credentials — check your Authorization header format and token validity. 403 Forbidden means credentials were valid but the user doesn't have permission. It's an authorization problem, not an authentication one. They're not interchangeable.

Missing Content-Type on POST requests causes a large percentage of "my API is receiving empty data" bugs. If you're sending a JSON body, you need Content-Type: application/json on the request — full stop.

For a deeper look at the caching side, see How HTTP Caching Works. For the broader HTTP status code reference, HTTP Status Codes: A Developer's Guide covers what each range means and when to use which code.

The MDN HTTP headers reference is the most complete resource for anything beyond what's covered here — every header has a full specification page with browser compatibility tables and security notes.

FAQ

What's the difference between Origin and Referer?

Origin includes only the scheme, host, and port (e.g., https://example.com). Referer includes the full URL including path and query string. Origin is sent on cross-origin requests and POSTs; Referer is sent more broadly (subject to the Referrer-Policy header). For CORS decisions and CSRF checks, Origin is the right one to validate.

Are HTTP header names case-sensitive?

No. HTTP/1.1 headers are case-insensitive by spec, so Content-Type and content-type are equivalent. HTTP/2 and HTTP/3 normalize all header names to lowercase on the wire. Don't rely on a specific casing in your code; always compare case-insensitively.

Why are some headers prefixed with `X-`?

Historically, X- indicated non-standard or experimental headers (X-Forwarded-For, X-Custom-Header). RFC 6648 deprecated this convention in 2012 — new headers shouldn't use the prefix. But existing X-headers (X-Content-Type-Options, X-Frame-Options) are too entrenched to rename, so you'll see them indefinitely.

Should I send Cache-Control or Expires?

Cache-Control. It's more flexible, supports modern directives like immutable, stale-while-revalidate, and must-revalidate, and overrides Expires when both are present. Expires is from HTTP/1.0 and uses absolute timestamps that get awkward with clock skew. Set Cache-Control as the primary header; let Expires die.

What does `SameSite=None` do?

It explicitly opts a cookie out of the cross-site protection that browsers default to. Required for cookies that need to flow across sites (third-party embeds, OAuth callbacks). Must be paired with Secure (HTTPS-only) — Chrome and Firefox reject SameSite=None without Secure. Use this only when you genuinely need cross-site cookies.

Why is X-Content-Type-Options: nosniff important?

Without it, browsers may MIME-sniff the response body and decide it's JavaScript even when the server said it's text. An attacker who can upload a file that "looks like" JS can get it executed cross-origin. Setting nosniff forces the browser to trust the Content-Type header exactly. It's a one-line security win — always set it.

What's the maximum size of an HTTP header?

The HTTP spec doesn't define a hard limit, but servers and proxies enforce their own. Nginx defaults to 8KB per header, 32KB total. Apache defaults to 8KB. Cloudflare allows 32KB total. Browsers accept up to ~256KB. If a request fails with 431 (Request Header Fields Too Large), check your Authorization or Cookie size — JWTs and large session cookies are common culprits.

Should I rely on the User-Agent string?

For analytics, sure. For feature detection or routing, no. User-Agent strings are unreliable, can be spoofed, and Chrome's User-Agent Client Hints (Sec-CH-UA) is gradually deprecating the old format. For "is this a mobile browser?" use feature detection (CSS media queries, JS feature checks) instead of UA parsing.