HTTP Headers

A user reports that their session keeps logging out on Safari but works fine on Chrome. You inspect the response and find Set-Cookie: session=abc; SameSite=None — no Secure flag. Safari silently drops it. The whole problem was a single missing header attribute. HTTP headers are how clients, proxies, CDNs, and servers negotiate behavior — and tiny header bugs cascade into outages, broken auth, cache poisoning, and CORS failures.

Headers are key-value pairs sent in both requests and responses. Names are case-insensitive. Values are strings. Multiple values can be comma-separated or sent as multiple header lines.

Header-Name: value
Header-Name: value1, value2

Headers are grouped by purpose, not by whether they appear in requests or responses.

Request Headers

Sent by the client to provide context about the request, the client, and what it will accept.

Identity & Routing

HeaderExampleDescription
HostHost: api.example.comTarget host and port. Mandatory in HTTP/1.1. Enables virtual hosting (multiple sites on one IP).
User-AgentUser-Agent: Mozilla/5.0 ...Client software identity. Browser, version, OS.
RefererReferer: https://example.com/pageURL of the page that initiated the request. Note: intentionally misspelled in the spec.
OriginOrigin: https://app.example.comOrigin of the request. Used in CORS. Does not include path.
FromFrom: bot@example.comEmail of the user/bot. Mainly used by crawlers.

Content Negotiation

HeaderExampleDescription
AcceptAccept: application/json, text/html;q=0.9Preferred response MIME types. q values indicate priority (default 1.0).
Accept-EncodingAccept-Encoding: gzip, brAcceptable compression algorithms for the response body.
Accept-LanguageAccept-Language: en-US, en;q=0.8Preferred natural languages for the response.
Accept-CharsetAccept-Charset: utf-8Acceptable character encodings. Mostly obsolete; UTF-8 is assumed.

Authentication & Authorization

HeaderExampleDescription
AuthorizationAuthorization: Bearer eyJhbGc...Credentials for the request. Common schemes: Basic, Bearer, Digest, AWS4-HMAC-SHA256.
CookieCookie: session=abc123; theme=darkCookies previously set by Set-Cookie. Sent automatically by the browser.
Proxy-AuthorizationProxy-Authorization: Basic dXNlcjpwYXNzCredentials for an authenticating proxy.

Conditional Requests

Used with caching and to prevent lost-update problems.

HeaderExampleDescription
If-None-MatchIf-None-Match: "abc123"Returns 304 Not Modified if the resource ETag hasn’t changed.
If-Modified-SinceIf-Modified-Since: Tue, 01 Jan 2025 00:00:00 GMTReturns 304 if the resource hasn’t changed since the given date.
If-MatchIf-Match: "abc123"Proceeds only if resource ETag matches. Used for safe updates (optimistic concurrency). Returns 412 on mismatch.
If-Unmodified-SinceIf-Unmodified-Since: Tue, 01 Jan 2025 00:00:00 GMTProceeds only if resource hasn’t changed since the date. Returns 412 otherwise.

Connection & Transmission

HeaderExampleDescription
ConnectionConnection: keep-aliveControl options for the current connection. keep-alive or close.
Keep-AliveKeep-Alive: timeout=5, max=100Parameters for persistent connection. Works with Connection: keep-alive.
Content-LengthContent-Length: 348Byte length of the request body. Required when sending a body without chunked encoding.
Content-TypeContent-Type: application/jsonMIME type of the request body. Required when sending a body.
Transfer-EncodingTransfer-Encoding: chunkedHow the body is encoded for transfer. chunked is most common.
ExpectExpect: 100-continueAsks the server to acknowledge readiness to receive body before the client sends it. Useful for large payloads.
UpgradeUpgrade: websocketRequest to switch to a different protocol (e.g., WebSocket).

Range Requests

HeaderExampleDescription
RangeRange: bytes=0-1023Requests a specific byte range of the resource. Server responds with 206 Partial Content.
If-RangeIf-Range: "abc123"Combined with Range; only applies the range request if the resource hasn’t changed.

Forwarding & Proxy Headers

HeaderExampleDescription
X-Forwarded-ForX-Forwarded-For: 203.0.113.5, 198.51.100.1Original client IP, added by each proxy in the chain. Rightmost IP is the most recently added.
X-Forwarded-ProtoX-Forwarded-Proto: httpsOriginal protocol (HTTP or HTTPS) used by the client. Set by load balancers/proxies when terminating TLS.
X-Forwarded-HostX-Forwarded-Host: api.example.comOriginal Host header, preserved by the proxy.
ForwardedForwarded: for=203.0.113.5;proto=httpsStandard RFC 7239 replacement for X-Forwarded-* headers.

Response Headers

Sent by the server to describe the response, control caching, and set client-side state.

Content Description

HeaderExampleDescription
Content-TypeContent-Type: application/json; charset=utf-8MIME type and optional character encoding of the response body.
Content-LengthContent-Length: 1234Byte length of the response body. Absent when Transfer-Encoding: chunked.
Content-EncodingContent-Encoding: gzipCompression applied to the body. Client decompresses before processing.
Content-LanguageContent-Language: en-USNatural language of the response body.
Content-LocationContent-Location: /api/users/42Alternate URI for the returned resource.
Content-RangeContent-Range: bytes 0-1023/5120Range of bytes returned in a 206 response. Format: unit start-end/total.
Transfer-EncodingTransfer-Encoding: chunkedHow the body is transmitted. Mutually exclusive with Content-Length.

Resource Identity

HeaderExampleDescription
ETagETag: "abc123"Opaque identifier for a specific version of a resource. Used in conditional requests (If-None-Match, If-Match).
Last-ModifiedLast-Modified: Mon, 20 Jan 2025 10:00:00 GMTTimestamp of last modification. Used in If-Modified-Since checks.
LocationLocation: /api/users/42URI of a new or moved resource. Used with 201 Created and all 3xx redirects.
VaryVary: Accept-EncodingLists request headers that affect the response. Tells caches to store separate versions per header value.

Connection Control

HeaderExampleDescription
ConnectionConnection: keep-aliveDirects the client on connection handling after the response.
Keep-AliveKeep-Alive: timeout=5, max=100Parameters for how long the server will keep the connection open.
UpgradeUpgrade: websocketServer agrees to switch protocols (response to a client Upgrade request).

Authentication Challenges

HeaderExampleDescription
WWW-AuthenticateWWW-Authenticate: Bearer realm="api"Required on 401 responses. Tells the client what auth scheme to use.
Proxy-AuthenticateProxy-Authenticate: Basic realm="proxy"Required on 407 Proxy Auth Required.

Cookies

HeaderExampleDescription
Set-CookieSet-Cookie: session=abc; Path=/; HttpOnly; Secure; SameSite=LaxSets a cookie in the browser. Can be repeated for multiple cookies.

Cookie attributes:

AttributeDescription
Path=Scope of the cookie by URL path
Domain=Scope by domain (and subdomains if leading dot)
Expires= / Max-Age=Cookie lifetime. Max-Age takes precedence. Session cookie if omitted.
HttpOnlyPrevents JavaScript access (document.cookie). Mitigates XSS cookie theft.
SecureCookie only sent over HTTPS.
SameSite=StrictCookie not sent on cross-site requests.
SameSite=LaxCookie not sent on cross-site sub-resource requests, but sent on top-level navigation. Default in modern browsers.
SameSite=NoneCookie sent on all cross-site requests. Requires Secure.
⚠️

SameSite=None requires the Secure attribute. Browsers silently reject SameSite=None cookies that are not also marked Secure.

Rate Limiting (Common Conventions)

Not standardized in HTTP/1.1; widely adopted by APIs.

HeaderExampleDescription
X-RateLimit-LimitX-RateLimit-Limit: 1000Max requests allowed in the window.
X-RateLimit-RemainingX-RateLimit-Remaining: 42Requests remaining in the current window.
X-RateLimit-ResetX-RateLimit-Reset: 1706745600Unix timestamp when the window resets.
Retry-AfterRetry-After: 30Seconds to wait before retrying. Used with 429 and 503. Can be a date instead of seconds.

Caching Headers

Caching headers control how responses are stored and reused by browsers, CDNs, and proxies.

HeaderDirectionDescription
Cache-ControlBothPrimary cache directive. See directives below.
ExpiresResponseAbsolute expiry date. Overridden by Cache-Control: max-age. Legacy.
ETagResponseVersion identifier. Client sends back in If-None-Match.
Last-ModifiedResponseModification timestamp. Client sends back in If-Modified-Since.
PragmaBothLegacy no-cache directive for HTTP/1.0 proxies. Effectively replaced by Cache-Control.

Cache-Control Directives

ℹ️

Cache-Control: no-cache does not mean “don’t cache.” It means “store it, but revalidate with the server before every use.” Use no-store to actually prevent caching.

Common Cache Patterns

Use CaseCache-Control Value
Content-hashed static assets (JS, CSS)public, max-age=31536000, immutable
HTML pagesno-cache (revalidate every time)
Private user dataprivate, no-store
API responses (short TTL)private, max-age=60
CDN-cached API responsespublic, s-maxage=300, max-age=0

Security Headers

Set by the server to instruct the browser on security policies.

HeaderExampleDescription
Strict-Transport-Security (HSTS)max-age=31536000; includeSubDomains; preloadForces browser to use HTTPS for the domain for max-age seconds. preload submits to browser preload lists.
Content-Security-Policy (CSP)default-src 'self'; script-src 'self' cdn.example.comControls which sources the browser may load resources from. Primary defense against XSS.
X-Content-Type-OptionsnosniffPrevents browser from MIME-sniffing. Forces use of declared Content-Type.
X-Frame-OptionsDENY or SAMEORIGINControls iframe embedding. Replaced by CSP frame-ancestors but still widely used.
Referrer-Policystrict-origin-when-cross-originControls how much of the Referer URL is sent on navigation.
Permissions-Policycamera=(), microphone=()Controls which browser APIs the page may use. Replaces Feature-Policy.
Cross-Origin-Opener-Policy (COOP)same-originIsolates browsing context from cross-origin windows. Required to enable SharedArrayBuffer.
Cross-Origin-Embedder-Policy (COEP)require-corpRequires all sub-resources to opt in to cross-origin loading. Also required for SharedArrayBuffer.
Cross-Origin-Resource-Policy (CORP)same-siteRestricts who can load the resource.

CORS Headers

Cross-Origin Resource Sharing — allows controlled cross-origin requests from browsers.

Response headers (set by server):

HeaderExampleDescription
Access-Control-Allow-Origin* or https://app.example.comWhich origins may access the resource. * disallows credentials.
Access-Control-Allow-MethodsGET, POST, PUTAllowed HTTP methods for cross-origin requests.
Access-Control-Allow-HeadersContent-Type, AuthorizationAllowed request headers.
Access-Control-Allow-CredentialstrueWhether cookies/auth headers are included. Cannot be used with * origin.
Access-Control-Max-Age86400Seconds the preflight response may be cached.
Access-Control-Expose-HeadersX-Request-IdResponse headers the browser JS may read beyond the safe defaults.
⚠️

Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true are mutually exclusive. Browsers will block the response if both are set. Use an explicit origin (e.g., https://app.example.com) when credentials are required.

Preflight flow (for non-simple requests):

sequenceDiagram
    participant B as Browser
    participant S as Server

    B->>S: OPTIONS /api/data (preflight)
    Note right of B: Origin: https://app.example.com
    S->>B: 204 No Content
    Note left of S: Access-Control-Allow-Origin: https://app.example.com
    Note left of S: Access-Control-Allow-Methods: POST
    B->>S: POST /api/data (actual request)
    S->>B: 200 OK

Custom / Vendor Headers

ℹ️

The X- prefix convention was deprecated by RFC 6648 in 2012. New custom headers should not use X-. Existing ones (X-Forwarded-For, X-Request-Id, etc.) remain in widespread use and are not going away.

HeaderDescription
X-Request-IdUnique identifier for the request. Used for distributed tracing and log correlation.
X-Correlation-IdSimilar to X-Request-Id. Common in microservice architectures.
X-Real-IPOriginal client IP as set by Nginx. Simpler alternative to X-Forwarded-For.
X-Api-VersionAPI version indicator. Some APIs use this instead of URL versioning.
ℹ️

Interview tip: Anchor on high-leverage headers: “Caching: Cache-Control: public, max-age=31536000, immutable on content-hashed assets, private, no-store for user-specific data — no-cache means revalidate, not don’t cache. Security: HSTS with preload, strict CSP, nosniff. Client IP: never trust leftmost X-Forwarded-For — read the IP from your first trusted proxy. CORS: Allow-Origin: * and Allow-Credentials: true are mutually exclusive. Cookies: always HttpOnly, Secure, SameSite=Lax.”

Test Your Understanding

Your API sets Cache-Control: no-cache on all responses. A developer complains that ‘caching is disabled’ and wants you to switch to no-store. Are they right? What’s the actual difference?

They’re wrong about what no-cache does, but may be right about the desired behavior.

  • no-cache = store it, but revalidate with the server every time before using it. The browser sends a conditional request (If-None-Match with ETag), and the server returns 304 Not Modified (no body) if nothing changed. The response IS cached — it’s just always validated.
  • no-store = don’t store anything at all. No caching, no conditional requests, full response body every time.

If the goal is to prevent sensitive data from being stored on disk (e.g., credit card data, personal health info), no-store is correct. If the goal is “always fresh but avoid transferring unchanged data,” no-cache is more efficient because 304 responses have no body.

A browser sends a CORS preflight (OPTIONS) to your API. The server returns Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true. The actual request fails silently. Why?

These two headers are mutually exclusive. The spec requires that when Allow-Credentials: true, the Allow-Origin must be a specific origin (e.g., https://app.example.com), not *. Browsers silently block the response when both are set.

Why: Allow-Origin: * with credentials would let any site send authenticated requests to your API and read the response — a massive security hole (effectively disabling the Same-Origin Policy).

Fix: Set Access-Control-Allow-Origin to the specific requesting origin (read it from the Origin request header, validate it against an allowlist, then echo it back).

Your Set-Cookie header is: Set-Cookie: session=abc; SameSite=None. It works in Chrome but is silently dropped in Safari. What’s missing?

SameSite=None requires the Secure attribute. Without Secure, modern browsers (Safari first, then Chrome and Firefox) silently reject the cookie. The correct header is:

Set-Cookie: session=abc; SameSite=None; Secure; HttpOnly; Path=/

SameSite=None means “send this cookie on cross-site requests” — the browser requires Secure (HTTPS only) as a safety gate, because sending cookies cross-site over HTTP would be trivially interceptable.

You set Strict-Transport-Security: max-age=31536000; includeSubDomains; preload on your production domain. A developer sets up a staging environment on staging.example.com over HTTP. Users can’t access it. Why?

includeSubDomains applies HSTS to all subdomains, including staging.example.com. Browsers that visited the production site now refuse to connect to any subdomain over HTTP — they upgrade all requests to HTTPS. Since staging doesn’t have a TLS certificate, the connection fails.

preload makes this permanent: The domain is submitted to browser preload lists, which are hardcoded into Chrome, Firefox, Safari, and Edge. Even users who’ve never visited your site enforce HSTS. Removing preload from the header doesn’t remove you from the list — you must submit a removal request to each browser vendor, which takes weeks to months.

Fix: Don’t use includeSubDomains if you have non-HTTPS subdomains. Or get TLS certs for all subdomains (Let’s Encrypt wildcard certs make this easy).

An attacker discovers your API returns user data with Vary: Cookie. They craft a URL that a victim clicks, and the CDN caches the victim’s personalized response. How does this attack work?

This is a web cache poisoning / cache deception attack. The attack flow:

  1. Attacker crafts a URL: https://api.example.com/account?cb=random123
  2. Victim clicks the link (via phishing, social engineering)
  3. The CDN receives the request with the victim’s Cookie header
  4. The CDN caches the personalized response keyed on URL + Cookie value
  5. If the CDN also caches based on URL alone (misconfigured), or if Vary: Cookie is not properly implemented, the attacker can request the same URL and receive the victim’s cached response

The real issue: Vary: Cookie creates a separate cache entry per unique cookie value. If the cache key implementation is flawed (e.g., CDN ignores Vary for certain response codes, or the cache key doesn’t include the full cookie), this breaks.

Fix: User-specific responses should use Cache-Control: private, no-store — never cache them at shared caches (CDN/proxy). Vary: Cookie is almost never correct for CDN caching because cookies are per-user by definition.