Getting Started
HTTP Request & Response
HTTP is a request-response protocol. Clients send requests (method, path, headers, optional body), servers respond with status, headers, and body. HTTP/1.1 is text-based, HTTP/2 and HTTP/3 use binary framing.
# HTTP Request
GET /api/users HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0
Accept: application/json
# HTTP Response
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 45
{"id": 1, "name": "Alice", "email": "[email protected]"}URL Structure
URLs identify resources. The scheme selects the protocol. The host is resolved via DNS. The default port is implied when omitted. The fragment (#) is client-side only and stripped before sending to the server. Query strings are commonly used for filtering/pagination.
# Generic URL format
scheme://userinfo@host:port/path?query#fragment
# Example
https://user:[email protected]:8443/api/v1/users?page=2#section
# Components
# scheme - https (protocol)
# userinfo - user:pass (rare, deprecated)
# host - example.com (domain or IP)
# port - 8443 (default: 80 for http, 443 for https)
# path - /api/v1/users (resource location)
# query - page=2 (key=value pairs, & separated)
# fragment - section (client-only, never sent to server)HTTP Versions Overview
HTTP/0.9 was a minimal prototype. HTTP/1.0 added headers and content types but opened a new connection per request. HTTP/1.1 introduced persistent connections, the mandatory Host header (enabling virtual hosts), and pipelining. HTTP/2 added binary multiplexing. HTTP/3 runs over QUIC, eliminating head-of-line blocking at the transport layer.
# HTTP/0.9 (1991) - single GET, no headers
GET /page.html
# HTTP/1.0 (1996) - headers, status codes, multiple types
GET /page.html HTTP/1.0
# HTTP/1.1 (1997) - persistent connections, Host header, pipelining
GET /page.html HTTP/1.1
Host: example.com
# HTTP/2 (2015) - binary, multiplexing, header compression
# (negotiated via ALPN or h2c upgrade)
# HTTP/3 (2022) - over QUIC (UDP-based)
# (negotiated via Alt-Svc header)Connection Lifecycle
HTTP/1.1 keeps TCP connections open by default (keep-alive), avoiding the cost of repeated handshakes. Multiple requests can be sent over one connection. The Connection: close header signals the last request. HTTP/2 multiplexes many streams over a single connection. Keep-Alive timeout and max are configurable on the server.
# HTTP/1.1 persistent connection (keep-alive)
GET /page1 HTTP/1.1
Host: example.com
Connection: keep-alive
# (server responds, connection stays open)
HTTP/1.1 200 OK
Content-Length: 128
Connection: keep-alive
# Client reuses same TCP connection for next request
GET /page2 HTTP/1.1
Host: example.com
# Closing the connection
GET /final HTTP/1.1
Host: example.com
Connection: closeMessage Format
HTTP messages start with a start-line (request-line or status-line), followed by headers, a blank line, and an optional body. Lines use CRLF line endings. Header names are case-insensitive. The blank line is mandatory and signals the start of the body. Bodies can be of any length (chunked) or fixed (Content-Length).
# Request format
<METHOD> <PATH> <VERSION>
<Header-Name>: <value>
...
<optional body>
# Response format
<VERSION> <STATUS> <REASON>
<Header-Name>: <value>
...
<optional body>
# Lines end with CRLF (
). Headers are case-insensitive.
# A blank line (
) separates headers from body.Telnet / Raw HTTP Debugging
You can speak HTTP raw over TCP using nc or telnet (port 80) or openssl s_client (HTTPS). This is invaluable for debugging. You must include the Host header on HTTP/1.1+. Two blank lines terminate the request. curl -v shows the wire-format conversation including headers.
# Manual HTTP request via netcat/nc
nc example.com 80
GET / HTTP/1.1
Host: example.com
# (press Enter twice to send blank line)
# Using openssl for HTTPS
openssl s_client -connect example.com:443
GET / HTTP/1.1
Host: example.com
# curl with --http1.1 to force version
curl -v --http1.1 https://example.com
curl -v --http2 https://example.com
curl -v --http3 https://example.comHTTP Methods
GET — Retrieve Resource
GET retrieves a representation of a resource. It must be safe (no side effects) and idempotent (repeatable). GET requests are cacheable and bookmarkable. Although the spec allows a body, it is discouraged — proxies and CDNs may strip it. Use query parameters for filtering instead.
# Basic GET
GET /api/users/42 HTTP/1.1
Host: api.example.com
Accept: application/json
# GET with query parameters
GET /api/users?role=admin&active=true HTTP/1.1
Host: api.example.com
# GET should be safe & idempotent
# - safe: no server state change
# - idempotent: repeating yields same result
# - cacheable: yes
# - body allowed but discouraged (some clients/servers reject)POST — Create Resource
POST submits data to be processed. It is used to create new resources where the server assigns the URI (e.g., /api/users → /api/users/43). POST is neither safe nor idempotent — submitting twice may create two resources. The 201 Created response should include a Location header pointing to the new resource.
# POST creating a new resource
POST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Content-Length: 52
{"name": "Bob", "email": "[email protected]", "age": 30}
# Response typically 201 Created with Location header
HTTP/1.1 201 Created
Location: /api/users/43
Content-Type: application/json
{"id": 43, "name": "Bob"}
# POST is neither safe nor idempotent.
# Repeating may create duplicate resources.PUT — Replace Resource
PUT replaces the entire resource at the target URI with the request body. It is idempotent — repeating the same PUT produces the same end state. PUT targets a specific URI (e.g., /users/43), unlike POST which targets a collection. If a field is omitted in PUT, it is typically removed (full replacement).
# PUT replaces the entire resource at the given URI
PUT /api/users/43 HTTP/1.1
Host: api.example.com
Content-Type: application/json
Content-Length: 60
{"id": 43, "name": "Robert", "email": "[email protected]", "age": 31}
# Idempotent: sending the same PUT again yields the same state.
# If the resource doesn't exist, PUT may create it (server-dependent).
# Response
HTTP/1.1 200 OK
# or 204 No Content if no body returnedPATCH — Partial Update
PATCH applies partial updates to a resource, unlike PUT which replaces it entirely. The simplest form is JSON Merge Patch (RFC 7396): send only the fields to change. JSON Patch (RFC 6902) uses an array of operations (add, remove, replace, move, copy, test). PATCH is NOT idempotent by default, though merge-patch is.
# PATCH applies partial modifications
PATCH /api/users/43 HTTP/1.1
Host: api.example.com
Content-Type: application/merge-patch+json
Content-Length: 22
{"email": "[email protected]"}
# Only the email field is updated; other fields remain.
# JSON Patch (RFC 6902) - structured operations
PATCH /api/users/43 HTTP/1.1
Content-Type: application/json-patch+json
[
{"op": "replace", "path": "/email", "value": "[email protected]"},
{"op": "remove", "path": "/age"}
]DELETE — Remove Resource
DELETE removes the target resource. It is idempotent — deleting the same resource twice should leave the server in the same state. Common success codes are 204 (no body), 200 (with body confirming), or 202 (async deletion). Most APIs require authentication. Whether to return 404 on a missing resource is debated — 204 is safer for idempotency.
# Delete a resource
DELETE /api/users/43 HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
# Successful responses:
HTTP/1.1 204 No Content # most common
HTTP/1.1 200 OK # if body returned
HTTP/1.1 202 Accepted # async deletion queued
# Idempotent: deleting an already-deleted resource
# should still return 204 (or 404 — debatable).HEAD & OPTIONS
HEAD is identical to GET but the server returns only headers (no body) — useful for checking Content-Length, Content-Type, or Last-Modified without downloading the body. OPTIONS asks which methods are allowed (Allow header) and is used by browsers for CORS preflight. Both are safe and idempotent.
# HEAD: like GET but returns headers only (no body)
HEAD /api/users/42 HTTP/1.1
Host: api.example.com
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 128
# (no body)
# OPTIONS: describe communication options
OPTIONS /api/users HTTP/1.1
Host: api.example.com
HTTP/1.1 200 OK
Allow: GET, POST, HEAD, OPTIONS
Access-Control-Allow-Methods: GET, POSTStatus Codes
1xx Informational
1xx codes are informational, indicating the request was received and processing continues. 100 Continue lets a client check whether to send a large body (paired with the Expect: 100-continue header). 101 Switching Protocols enables WebSocket. 103 Early Hints lets the server hint the browser to preload assets before the final response is ready.
# 100 Continue - server received headers, client may send body
POST /upload HTTP/1.1
Host: example.com
Expect: 100-continue
Content-Length: 1048576
# Server replies:
HTTP/1.1 100 Continue
# (client now sends the request body)
# 101 Switching Protocols - used for WebSocket upgrade
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
# 103 Early Hints - preload resources before final response
HTTP/1.1 103 Early Hints
Link: </style.css>; rel=preload; as=style2xx Success
2xx codes indicate success. 200 OK is the generic success. 201 Created indicates a new resource was made (with a Location header). 202 Accepted means the request is queued for async processing (e.g., a long job). 204 No Content signals success without a body — common for DELETE/PUT. 206 Partial Content returns part of a resource (Range request).
# 200 OK - standard success
HTTP/1.1 200 OK
# 201 Created - new resource created (POST/PUT)
HTTP/1.1 201 Created
Location: /api/users/43
# 202 Accepted - request queued for async processing
HTTP/1.1 202 Accepted
# 204 No Content - success, no body (DELETE, PUT)
HTTP/1.1 204 No Content
# 206 Partial Content - Range request result
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1023/20483xx Redirection
3xx codes indicate redirection. 301/308 are permanent (cacheable, SEO link juice passes); 302/307 are temporary. The crucial difference: 301 and 302 historically allowed POST→GET conversion (causing data loss), while 307 and 308 strictly preserve the method. 304 Not Modified is returned on conditional requests when the cache is valid — no body sent.
# 301 Moved Permanently - cacheable, change bookmarks
HTTP/1.1 301 Moved Permanently
Location: https://newsite.com/page
# 302 Found - temporary redirect (don't update bookmarks)
HTTP/1.1 302 Found
Location: /login
# 304 Not Modified - cache is still valid (conditional GET)
HTTP/1.1 304 Not Modified
ETag: "abc123"
# 307 Temporary Redirect - preserves method (POST stays POST)
HTTP/1.1 307 Temporary Redirect
Location: /new-endpoint
# 308 Permanent Redirect - permanent, preserves method
HTTP/1.1 308 Permanent Redirect
Location: https://newsite.com/api4xx Client Errors
4xx codes mean the client made an error. 400 Bad Request is malformed input. 401 means no/invalid authentication (must include WWW-Authenticate). 403 means authenticated but lacking permission. 404 is a missing resource. 429 indicates rate limiting (include Retry-After). Other common ones: 405 Method Not Allowed, 409 Conflict, 422 Unprocessable Entity.
# 400 Bad Request - malformed syntax
HTTP/1.1 400 Bad Request
Content-Type: application/json
{"error": "Invalid JSON in request body"}
# 401 Unauthorized - authentication required
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api"
# 403 Forbidden - authenticated but not allowed
HTTP/1.1 403 Forbidden
# 404 Not Found - resource doesn't exist
HTTP/1.1 404 Not Found
# 429 Too Many Requests - rate limit hit
HTTP/1.1 429 Too Many Requests
Retry-After: 605xx Server Errors
5xx codes mean the server failed. 500 is a catch-all for unhandled errors (check server logs). 501 means the server doesn't implement the method. 502 means a proxy/gateway got a bad response from upstream. 503 means temporary overload (use Retry-After). 504 means an upstream timeout. Clients may retry 502/503/504 with backoff.
# 500 Internal Server Error - generic server failure
HTTP/1.1 500 Internal Server Error
# 501 Not Implemented - server doesn't support the method
HTTP/1.1 501 Not Implemented
# 502 Bad Gateway - upstream server returned invalid response
HTTP/1.1 502 Bad Gateway
# 503 Service Unavailable - temporary overload or maintenance
HTTP/1.1 503 Service Unavailable
Retry-After: 300
# 504 Gateway Timeout - upstream didn't respond in time
HTTP/1.1 504 Gateway TimeoutStatus Code Patterns
A consistent status code mapping makes APIs predictable. Use 2xx for success with semantic precision (201 for create, 204 for no body). 4xx for client errors with the most specific code (409 for conflicts, 422 for validation). 401 vs 403 is the most common confusion: 401 = 'who are you?', 403 = 'I know who you are, but you can't do this'.
# REST API common mapping
GET /users -> 200
POST /users -> 201 (Created)
GET /users/42 -> 200 | 404
PUT /users/42 -> 200 | 201 (if created) | 404
PATCH /users/42 -> 200 | 404
DELETE /users/42 -> 204 | 404
# Validation errors
400 -> malformed JSON, missing required field
409 -> duplicate resource / version conflict
422 -> semantic validation failure (some APIs)
# Auth flow
401 -> no/invalid token (login required)
403 -> logged in but lacking permission
404 -> hide existence from unauthorized users
# Use 418 I'm a teapot for fun (RFC 2324 joke).Request Headers
Common Request Headers
Common request headers convey client identity (User-Agent), desired response format (Accept family), connection control (Connection), authentication (Authorization), state (Cookie), and origin (Referer/Origin). Host is mandatory in HTTP/1.1+ for virtual hosting. Content-Type and Content-Length describe any request body.
GET /api/data HTTP/1.1
Host: api.example.com # target host (mandatory in HTTP/1.1)
User-Agent: Mozilla/5.0 # client identity
Accept: application/json # desired response format
Accept-Language: en-US,en;q=0.9
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Authorization: Bearer <token>
Cookie: session=abc123; theme=dark
Referer: https://example.com/
Origin: https://example.com
Content-Type: application/json
Content-Length: 42Accept Headers (Content Negotiation)
The Accept family drives content negotiation. Quality values (q, 0-1) express preference — higher is more preferred. */* is a wildcard catch-all. The server picks the best match from what it supports; if none match, it returns 406 Not Acceptable (though most servers fall back to a default rather than 406). Brotli (br) and zstd offer better compression than gzip.
# Accept: desired media types with quality (q) values
Accept: text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8
# Accept-Language: preferred languages
Accept-Language: en-US,en;q=0.9, zh-CN;q=0.8, fr;q=0.7
# Accept-Encoding: accepted compression algorithms
Accept-Encoding: gzip, deflate, br, zstd;q=0.9
# q=1.0 is default. Higher q = more preferred.
# */* matches any type.
# Server responds with 406 Not Acceptable if it can't satisfy.Authorization & Auth Headers
Authorization carries credentials. Basic auth sends base64-encoded username:password on every request (always use HTTPS). Bearer tokens are opaque strings (often JWTs) used in OAuth 2.0. Custom auth schemes (X-API-Key) are common for API gateways. Basic auth's base64 is encoding, not encryption — it offers no security without TLS.
# Basic Authentication (base64 of user:pass)
Authorization: Basic dXNlcjpwYXNz
# dXNlcjpwYXNz = base64("user:pass")
# Bearer Token (OAuth 2.0 / JWT)
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
# API Key in header (custom)
X-API-Key: abc123def456
# Proxy authentication
Proxy-Authorization: Basic dXNlcjpwYXNz
# WARNING: Basic auth sends credentials on every request.
# Always use HTTPS to prevent eavesdropping.User-Agent & Referer
User-Agent identifies the client, but most browsers fake Mozilla/5.0 for backward compatibility (historical sniffing). Referer (misspelled in the spec, never corrected) tells the server which page linked to the request — used for analytics and hotlink protection. Origin is the scheme+host+port, sent by browsers on CORS requests and POSTs.
# User-Agent identifies the client software
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
# Common browsers prepend "Mozilla/5.0" for historical compatibility
# (servers used to serve different content to different browsers)
# Referer: page that linked to this request
Referer: https://example.com/search?q=hello
# Origin: scheme + host + port (used by CORS)
Origin: https://example.com
# Note: "Referer" is the official spelling (misspelled in the original spec)Conditional Request Headers
Conditional headers enable efficient caching and concurrency control. If-None-Match (with ETag) and If-Modified-Since (with Last-Modified) make GET conditional — server returns 304 if the cache is valid. If-Match and If-Unmodified-Since guard PUT/PATCH against lost updates (optimistic concurrency). If-Range enables atomic range requests.
# Conditional GET - only fetch if changed
If-Modified-Since: Wed, 21 Oct 2023 07:28:00 GMT
If-None-Match: "abc123"
# If-Match / If-Unmodified-Since - for safe PUT/PATCH
If-Match: "abc123"
If-Unmodified-Since: Wed, 21 Oct 2023 07:28:00 GMT
# If-Range - for range requests (all-or-nothing)
If-Range: "abc123"
Range: bytes=0-1023
# Typical flow:
# 1. Server sends ETag/Last-Modified with response
# 2. Client sends If-None-Match/If-Modified-Since on next request
# 3. Server replies 304 if unchanged, 200 if changedCustom & X-Headers
Custom headers historically used the X- prefix, but RFC 6648 deprecated this convention in 2012. New headers should drop X- (e.g., Request-ID not X-Request-ID). X-Forwarded-For and X-Forwarded-Proto carry client info through proxies (note: spoofable). The standardized alternative is Forwarded (RFC 7239). Traceparent/Tracestate enable distributed tracing (W3C).
# X- prefixed headers (deprecated convention, but widely used)
X-Request-ID: 550e8400-e29b-41d4-a716-446655440000
X-Correlation-ID: abc-123
X-Forwarded-For: 203.0.113.1, 70.41.3.18
X-Forwarded-Proto: https
X-Real-IP: 203.0.113.1
X-RateLimit-Remaining: 42
# Modern recommendation: avoid X- prefix (RFC 6648)
# Use a namespace instead:
Request-ID: 550e8400-e29b-41d4-a716-446655440000
Traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
# Forwarded (RFC 7239) - standardized proxy info
Forwarded: for=203.0.113.1; proto=https; host=example.comResponse Headers
Common Response Headers
Common response headers describe the server (Server), the response body (Content-Type, Content-Length), connection control (Connection), caching (Cache-Control, ETag, Last-Modified), content negotiation (Vary), state (Set-Cookie), and CORS (Access-Control-*). Date is mandatory and uses the RFC 1123 format in GMT.
HTTP/1.1 200 OK
Server: nginx/1.25.0
Date: Wed, 21 Oct 2023 07:28:00 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 128
Connection: keep-alive
Cache-Control: public, max-age=3600
ETag: "abc123"
Last-Modified: Mon, 20 Oct 2023 10:00:00 GMT
Vary: Accept-Encoding
Set-Cookie: session=xyz; HttpOnly; Secure
Access-Control-Allow-Origin: https://example.comContent Headers
Content-* headers describe the body. Content-Type is the MIME type and is critical. Content-Length gives the exact byte count (required unless using chunked encoding). Content-Encoding indicates compression (gzip, br). Content-Disposition forces download with a filename. Content-Range accompanies 206 Partial Content responses.
# Body metadata
Content-Type: application/json; charset=utf-8
Content-Length: 128 # exact byte count
Content-Encoding: gzip # applied encoding
Content-Language: en-US
Content-Location: /api/users/42.json
Content-Disposition: attachment; filename="report.pdf"
Content-Range: bytes 0-1023/2048 # for partial content
# For chunked transfer (no Content-Length known upfront)
Transfer-Encoding: chunked
# MIME type format: type/subtype; parameter=value
# e.g., text/html; charset=UTF-8Security Headers
Security headers harden browsers against common attacks. HSTS forces HTTPS and prevents SSL stripping. X-Content-Type-Options: nosniff prevents MIME sniffing. X-Frame-Options (or CSP frame-ancestors) blocks clickjacking. CSP is the most powerful — it controls which resources can load and execute. Referrer-Policy limits leaked referrer info. Set these on every response.
# Force HTTPS for 6 months (incl. subdomains)
Strict-Transport-Security: max-age=15768000; includeSubDomains; preload
# Prevent MIME-type sniffing
X-Content-Type-Options: nosniff
# Prevent clickjacking
X-Frame-Options: DENY
# Or modern CSP frame-ancestors:
Content-Security-Policy: frame-ancestors 'none'
# Referrer policy
Referrer-Policy: strict-origin-when-cross-origin
# Permissions policy (feature lockdown)
Permissions-Policy: geolocation=(), camera=()
# Full Content-Security-Policy
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.comCORS Response Headers
CORS (Cross-Origin Resource Sharing) headers let a server opt-in to cross-origin requests. Access-Control-Allow-Origin is required for any cross-origin read; either a specific origin or *. If credentials are involved, Allow-Credentials: true AND a specific origin (no wildcard). Preflight (OPTIONS) responses include Allow-Methods/Headers. Expose-Headers lets JS read non-default response headers.
# Allow a specific origin
Access-Control-Allow-Origin: https://example.com
# Or wildcard (but credentials won't work with *)
Access-Control-Allow-Origin: *
# Allow credentials (cookies, Authorization)
Access-Control-Allow-Credentials: true
# Allowed methods (preflight response)
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
# Allowed headers (preflight response)
Access-Control-Allow-Headers: Content-Type, Authorization
# How long browser can cache preflight (seconds)
Access-Control-Max-Age: 86400
# Headers the JS can read from the response
Access-Control-Expose-Headers: X-Request-ID, X-Total-CountCache Headers
Cache-Control is the modern (HTTP/1.1) caching header with directives: public/private (who may cache), max-age (freshness in seconds), no-cache (must revalidate), no-store (never cache), immutable (URL never changes). ETag and Last-Modified are validators used with conditional requests. Expires (HTTP/1.0) is a fallback. Age tells how long a response has been cached.
# Cache-Control directives (HTTP/1.1 - preferred)
Cache-Control: public, max-age=3600
Cache-Control: private, max-age=600
Cache-Control: no-cache # revalidate before use
Cache-Control: no-store # never cache
Cache-Control: must-revalidate
Cache-Control: immutable
# Validators
ETag: "abc123" # opaque version tag
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
# HTTP/1.0 fallbacks (deprecated)
Expires: Wed, 21 Oct 2023 08:28:00 GMT
Pragma: no-cache
# Age: time a response has been in a cache (seconds)
Age: 120Authentication
Basic Authentication
Basic auth sends base64-encoded credentials on every request. It is simple and universally supported but offers zero security without HTTPS — base64 is trivially decodable. The WWW-Authenticate header challenges the client (browsers show a native login dialog). Basic auth is rare in modern web apps but common for API tokens and internal tooling.
# Basic auth: base64(user:pass) sent on every request
GET /api/data HTTP/1.1
Host: example.com
Authorization: Basic dXNlcjpwYXNz
# dXNlcjpwYXNz = base64("user:pass")
# In curl: curl -u user:pass https://example.com/api/data
# Server challenge (401 with WWW-Authenticate)
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Restricted Area", charset="UTF-8"
# WARNING: base64 is encoding, NOT encryption.
# Always pair Basic auth with HTTPS.Bearer Token (OAuth 2.0)
Bearer tokens are opaque strings presented as proof of authorization. The server just needs to validate them — they are stateless if self-contained (JWT) or looked up in a store. OAuth 2.0 defines flows to obtain tokens (authorization code, client credentials, refresh token). Always send Bearer tokens over HTTPS. Tokens have finite lifetimes; use refresh tokens to get new ones.
# Bearer token: opaque string proving authorization
GET /api/profile HTTP/1.1
Host: api.example.com
Authorization: Bearer SlAV32hkKG...
# Token obtained from OAuth 2.0 flow:
POST /oauth/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=alice&password=secret&client_id=...
# Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "SlAV32hkKG...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "GmR..."
}JWT Structure
A JWT has three base64url-encoded parts separated by dots: header (algorithm), payload (claims), and signature. The signature proves the token wasn't tampered with. Common claims: sub (subject), iat (issued at), exp (expiration), iss (issuer), aud (audience). The payload is NOT encrypted — never put secrets there. Signature algorithms: HS256 (HMAC), RS256 (RSA), ES256 (ECDSA).
# JWT = header.payload.signature (base64url-encoded)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
# Decoded:
# Header
{"alg": "HS256", "typ": "JWT"}
# Payload (claims)
{
"sub": "1234567890", # subject (user id)
"name": "Alice",
"iat": 1516239022, # issued at
"exp": 1516242622, # expiration
"iss": "example.com", # issuer
"aud": "api.example.com" # audience
}
# Signature (HMAC-SHA256 of header.payload with secret)
HMAC-SHA256(secret, "eyJhbGc...")JWT Validation
JWT validation: verify the signature (catches tampering), then check claims (exp, iss, aud). exp is the most important — reject expired tokens with 401. JWTs are stateless: once issued, they can't be revoked until they expire. For revocation, use short-lived access tokens (5-15 min) + refresh tokens stored server-side, or maintain a blacklist (defeats statelessness).
# Server-side JWT validation steps:
# 1. Parse three parts (header.payload.signature)
# 2. Verify signature using secret (HS256) or public key (RS256)
# 3. Check claims:
# - exp (must be in future)
# - iat (must be in past, optional)
# - iss (must match expected issuer)
# - aud (must match expected audience)
# 4. Check revocation list (if using short-lived + refresh)
# Common errors:
# 401 - invalid signature, expired, wrong issuer
# 401 - "Bearer" prefix missing
# 403 - valid token but lacking permission (scope)
# Token expired response
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="The token has expired"OAuth 2.0 Authorization Code Flow
The Authorization Code flow is the standard for server-side web apps. Step 1 redirects the user to the auth server. Step 2 returns a short-lived code to the redirect URI. Step 3 exchanges the code (plus client_secret) for tokens server-to-server. PKCE (Proof Key for Code Exchange) replaces the secret with a code challenge/verifier pair — essential for SPAs and mobile apps where the secret can't be safely stored.
# Step 1: Redirect user to authorization endpoint
GET /authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=https://app.com/callback&scope=read&state=random123 HTTP/1.1
Host: auth.example.com
# Step 2: User logs in & grants consent, redirected back with code
HTTP/1.1 302 Found
Location: https://app.com/callback?code=AUTH_CODE&state=random123
# Step 3: Server exchanges code for token (server-to-server)
POST /oauth/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)
grant_type=authorization_code&code=AUTH_CODE&redirect_uri=https://app.com/callback
# Step 4: Token response
{"access_token": "...", "refresh_token": "...", "expires_in": 3600}
# Use PKCE for SPAs/mobile (no client_secret)
# code_challenge = base64url(SHA256(code_verifier))API Keys
API keys are simple opaque secrets identifying a client. They are sent in headers (preferred) or query strings (avoid — logged in URLs and proxy logs). API keys are stateful (server looks them up) and revocable. Unlike JWTs, they don't carry claims — the server stores permissions. Best practice: hash keys at rest, scope them tightly, rotate them, and provide a self-service rotation UI.
# API key in header (most common)
GET /api/data HTTP/1.1
X-API-Key: abc123def456
# API key as Bearer token
Authorization: Bearer abc123def456
# API key in query string (less secure - logged in URLs)
GET /api/data?api_key=abc123def456 HTTP/1.1
# API key in custom header with prefix
Authorization: ApiKey abc123def456
# Best practices:
# - Rotate keys periodically
# - Scope keys to specific permissions
# - Store keys hashed at rest (like passwords)
# - Never commit keys to source controlContent Negotiation
Accept Header
The Accept header lists media types the client can handle, with quality values (q) expressing preference. Wildcards (text/*, */*) broaden the match. The server picks the best match and responds with the chosen Content-Type. If no media type matches, the server ideally returns 406, but in practice many APIs fall back to a default format.
# Single type
Accept: application/json
# Multiple types with preferences (q values, 0-1)
Accept: text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8
# Wildcard media types
Accept: text/* # any text/* subtype
Accept: */* # anything
Accept: image/png, image/*;q=0.8
# Parameters
Accept: text/html;charset=utf-8
# Server picks best match and returns it in Content-Type.
# If no match, server returns 406 Not Acceptable (or a default).Accept-Language
Accept-Language tells the server the user's preferred languages, with quality values for ranking. RFC 5646 defines language tags (language[-script][-region]). The server responds with Content-Language. Browsers set this based on the user's OS/locale settings. For i18n, fall back gracefully: if the requested language is unavailable, return the default. CDNs sometimes vary caches by Accept-Language — use the Vary header.
# Single language
Accept-Language: en-US
# Multiple languages with preferences
Accept-Language: en-US,en;q=0.9, zh-CN;q=0.8, fr;q=0.7, de;q=0.5
# Wildcard
Accept-Language: *
# Server responds with the selected language:
Content-Language: en-US
# RFC 5646 language tags:
# en - language
# en-US - language + region
# zh-Hans - language + script (Simplified Chinese)
# es-419 - language + region (Latin America)Accept-Encoding
Accept-Encoding lists compression algorithms the client can decode. The server picks one and applies it, returning Content-Encoding. Brotli (br) compresses text better than gzip and is supported in all modern browsers. zstd is emerging with even better speed/ratio. identity means no compression. Always enable compression for text responses (HTML, CSS, JS, JSON) — major bandwidth savings, but skip already-compressed formats (images, video).
# Accepted compression algorithms
Accept-Encoding: gzip, deflate, br, zstd
# With preferences
Accept-Encoding: br;q=1.0, gzip;q=0.8, identity;q=0.5, *;q=0
# identity = no encoding (raw)
# * = any encoding not listed
# Server response
Content-Encoding: br
# Common algorithms:
# gzip - widespread, RFC 1952
# deflate - zlib, RFC 1950
# br - Brotli, better for text (modern browsers)
# zstd - Zstandard, fast + good ratio (newer)
# identity - no compressionVary Header
The Vary header tells caches (browser, CDN, proxy) which request headers affect the response. If a server returns different content based on Accept-Encoding, it must Vary: Accept-Encoding so the cache doesn't serve a gzip response to a client that only accepts br. Over-Varying hurts cache hit rate; under-Varying causes cache poisoning (wrong content served). Vary: * effectively disables caching.
# Server response telling caches to vary by Accept-Encoding
HTTP/1.1 200 OK
Content-Type: text/html
Content-Encoding: gzip
Vary: Accept-Encoding
# Vary on multiple headers
Vary: Accept-Encoding, Accept-Language
# Vary on Accept (full content negotiation)
Vary: Accept
# Vary: * means "varies on everything" (effectively no-cache)
Vary: *
# CDN/cache key includes the Vary header values.
# A gzip+en response is cached separately from a br+fr response.Quality Values (q)
Quality values (q, 0-1) express preference strength in Accept-family headers. q=1.0 is the default (most preferred); q=0 means 'unacceptable'. The server lists what it can produce, matches each against the client's list, picks the highest-q match. Specificity breaks ties: application/json beats */* even at the same q. Three decimal places are allowed but rarely needed.
# q values appear in Accept, Accept-Language, Accept-Encoding, Accept-Charset
# Format: ;q=0.000 to ;q=1.000 (default is 1.0)
Accept: text/html;q=1.0, application/json;q=0.9, */*;q=0.1
# Meaning:
# q=1.0 - strongly preferred
# q=0.5 - acceptable
# q=0 - NOT acceptable (refuse)
# Server algorithm (RFC 7231):
# 1. List media types the server can produce
# 2. Match each against the client's Accept
# 3. Pick the match with the highest q value
# 4. Ties broken by specificity (concrete > wildcard)
# Example: client prefers JSON over XML
Accept: application/json, application/xml;q=0.5Caching
Cache-Control Directives
Cache-Control is the modern caching header with directives. public/private controls who may cache; max-age sets freshness in seconds; no-cache forces revalidation; no-store forbids caching; must-revalidate forbids stale serving; s-maxage sets a separate TTL for shared caches (CDNs). immutable tells browsers the resource never changes — skip revalidation on reload. Stale-while-revalidate and stale-if-error enable graceful degradation.
# Response directives (server -> cache)
Cache-Control: public, max-age=3600 # cacheable by all, fresh 1h
Cache-Control: private, max-age=600 # browser only, fresh 10min
Cache-Control: no-cache # revalidate before use
Cache-Control: no-store # never cache (sensitive data)
Cache-Control: must-revalidate # never serve stale
Cache-Control: proxy-revalidate # proxies must revalidate
Cache-Control: immutable # URL never changes
Cache-Control: max-age=0, must-revalidate # always revalidate
Cache-Control: s-maxage=3600, max-age=600 # CDN caches 1h, browser 10min
# Request directives (client -> cache)
Cache-Control: no-cache # send request to origin
Cache-Control: no-store # don't store the response
Cache-Control: only-if-cached # only from cacheETag & If-None-Match
ETag is an opaque version tag (often a hash of the content) attached to a response. On the next request, the client sends If-None-Match with the saved ETag. If the resource hasn't changed, the server returns 304 Not Modified with no body — saving bandwidth. Strong ETags guarantee byte-identical content; weak ETags (W/) allow semantically-equivalent responses (e.g., whitespace differences).
# Server tags a response with an ETag
HTTP/1.1 200 OK
ETag: "abc123"
Content-Type: application/json
{"data": "..."}
# Client sends conditional request on next fetch
GET /api/data HTTP/1.1
If-None-Match: "abc123"
# Server compares:
# - If matches: 304 Not Modified (no body)
HTTP/1.1 304 Not Modified
ETag: "abc123"
# - If changed: 200 OK with new body + new ETag
HTTP/1.1 200 OK
ETag: "def456"
# Strong vs weak ETags:
# "abc123" - strong (byte-identical)
# W/"abc123" - weak (semantically equivalent)Last-Modified & If-Modified-Since
Last-Modified is a timestamp-based validator. It has 1-second precision — sub-second changes can be missed. If-Modified-Since triggers a 304 if unchanged. If-Unmodified-Since guards against lost updates: if another client modified the resource, the server returns 412 Precondition Failed. ETag is preferred when available (more precise), but Last-Modified is a useful fallback.
# Server includes Last-Modified
HTTP/1.1 200 OK
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
# Client sends conditional request
GET /api/data HTTP/1.1
If-Modified-Since: Wed, 21 Oct 2023 07:28:00 GMT
# Server: if not modified since that time -> 304
HTTP/1.1 304 Not Modified
# Server: if modified since -> 200 with new content
HTTP/1.1 200 OK
Last-Modified: Thu, 22 Oct 2023 09:00:00 GMT
# If-Unmodified-Since (for safe updates - optimistic locking)
PUT /api/users/42 HTTP/1.1
If-Unmodified-Since: Wed, 21 Oct 2023 07:28:00 GMT
# 412 Precondition Failed if modified elsewhereCache Validation Flow
Cache flow: fresh (within max-age) → served from cache, no network. Stale (after max-age) → conditional request with validators. 304 refreshes the cache timer without re-downloading the body. 200 replaces the cached entry. This drastically cuts bandwidth and origin load. For data that changes often but you can tolerate slight staleness, use stale-while-revalidate.
# 1. First request - server sends validators
GET /api/users HTTP/1.1
HTTP/1.1 200 OK
Cache-Control: max-age=60
ETag: "v1"
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
# 2. Within max-age (60s): cache serves directly, no network
GET /api/users (served from cache)
# 3. After max-age: stale - revalidate (conditional GET)
GET /api/users HTTP/1.1
If-None-Match: "v1"
If-Modified-Since: Wed, 21 Oct 2023 07:28:00 GMT
# 4a. Unchanged -> 304 (revalidate resets the cache timer)
HTTP/1.1 304 Not Modified
Cache-Control: max-age=60
ETag: "v1"
# 4b. Changed -> 200 with new body
HTTP/1.1 200 OK
Cache-Control: max-age=60
ETag: "v2"Cache Busting & Invalidation
Cache busting: give immutable assets (JS, CSS, images) versioned URLs (app.v1a2b3c.js) and cache them for a year with immutable. When you deploy, change the URL — browsers fetch the new file. The HTML entry point uses no-cache so users always revalidate and pick up new asset URLs. This pattern gives both instant updates and long-lived caches. Avoid query-string versioning (?v=123) — some proxies don't cache it.
# Immutable assets with long cache + versioned URLs
GET /assets/app.v1a2b3c.js HTTP/1.1
HTTP/1.1 200 OK
Cache-Control: public, max-age=31536000, immutable
# When deploying a new version, the URL changes:
GET /assets/app.v4d5e6f.js HTTP/1.1
# (browser fetches new file; old one stays cached but unused)
# HTML entry point: short cache, must revalidate
GET /index.html HTTP/1.1
HTTP/1.1 200 OK
Cache-Control: no-cache
# (revalidate every time, but 304 if unchanged)
# Anti-pattern: caching HTML forever (users see stale entry)
# Anti-pattern: querying JS with ?ts=123 (defeats CDN caching)
# Manual cache purge via CDN API (vendor-specific)CORS (Cross-Origin)
Same-Origin Policy & CORS
Same-Origin Policy (SOP) is a browser security model that blocks web pages from reading cross-origin responses by default. CORS (Cross-Origin Resource Sharing) is the opt-out: the server declares which origins may read its responses. Origin is defined as scheme+host+port — any of these differing means cross-origin. CORS only matters for browser-based requests; server-to-server calls are unrestricted.
# Same-Origin Policy (SOP): browsers block cross-origin reads by default
# Origin = scheme + host + port
# https://app.com vs https://api.com -> DIFFERENT origins
# http://app.com vs https://app.com -> DIFFERENT (scheme)
# https://app.com vs https://app.com:8443 -> DIFFERENT (port)
# CORS lets a server OPT IN to cross-origin requests
# via Access-Control-Allow-Origin response header.
# Simple cross-origin GET (no preflight)
GET /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
# Server allows the origin
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.comSimple Requests (No Preflight)
Simple requests skip the preflight step and go straight to the server. The restrictions are conservative: only GET/HEAD/POST, only safelisted headers, and only simple Content-Types. Any deviation (e.g., Content-Type: application/json, custom headers, PUT/DELETE) makes the request 'non-simple' and triggers a preflight. The server still must include CORS headers on the response or the browser blocks reading it.
# A "simple" request is sent directly (no preflight):
# - Method: GET, HEAD, or POST
# - Only CORS-safelisted headers:
# Accept, Accept-Language, Content-Language, Content-Type
# - Content-Type limited to:
# application/x-www-form-urlencoded
# multipart/form-data
# text/plain
# - No event listeners on upload
# - No ReadableStream
# Example: simple cross-origin POST form submit
POST /api/login HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/x-www-form-urlencoded
user=alice&pass=secret
# Server must respond with Allow-Origin for browser to expose responsePreflight Requests (OPTIONS)
A preflight is an OPTIONS request the browser sends before a non-simple cross-origin request. It checks whether the actual method and headers are allowed. The server responds with Allow-Methods and Allow-Headers. Access-Control-Max-Age caches the preflight result (default 5s, browsers cap at 24h). Preflights add latency — avoid them for hot paths by sticking to simple requests where possible.
# Browser sends preflight OPTIONS before non-simple requests
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
# Server preflight response
HTTP/1.1 204 No Content
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 # cache preflight for 24h
# Only then does the browser send the actual request
PUT /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json
Authorization: Bearer ...
{"key": "value"}CORS with Credentials
By default, cross-origin fetches don't send cookies. To include them, the client sets credentials: 'include'. The server must respond with Access-Control-Allow-Credentials: true AND echo the specific origin (not the wildcard *). Always include Vary: Origin so caches don't serve a response negotiated for one origin to another. Misconfigured CORS+credentials is a security hole — restrict origins to a known allowlist.
# Client: opt in to sending cookies/auth
fetch('https://api.example.com/data', {
credentials: 'include' # send cookies cross-origin
});
# Browser sends:
GET /data HTTP/1.1
Origin: https://app.example.com
Cookie: session=abc123
# Server response MUST:
# 1. Echo the SPECIFIC origin (NOT *)
# 2. Set Allow-Credentials: true
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin # important: caches must vary by Origin
# Wildcard origin + credentials = browser blocks response
# (Access-Control-Allow-Origin: * with credentials fails)Common CORS Errors
CORS errors surface in the browser console. The most common are missing Allow-Origin (server didn't opt in), wildcard-with-credentials (forbidden combo), and missing Allow-Headers/Methods on preflight responses. CORS is enforced by the browser, not the server — the request still reaches the server, but the browser hides the response from JS. Check the Network tab: if the OPTIONS preflight fails, the actual request is never sent.
# Error: No 'Access-Control-Allow-Origin' header
# Cause: server didn't send CORS header, or origin not allowed
# Fix: add Access-Control-Allow-Origin on the server
# Error: Credentials flag is true, but Allow-Origin is *
# Cause: wildcard not allowed with credentials
# Fix: echo the specific request Origin instead of *
# Error: preflight missing Allow-Methods / Allow-Headers
# Cause: server didn't allow the method/header
# Fix: add them to Access-Control-Allow-Methods/Headers
# Error: Allow-Origin doesn't match Origin
# Cause: server echoed a different origin (or scheme mismatch)
# Fix: dynamically echo req.headers.origin if allowlisted
# Debug:
# - Check browser DevTools Console for exact error
# - Check Network tab -> see the OPTIONS preflight response
# - Verify headers in the responseHTTPS & SSL/TLS
TLS Handshake (HTTPS)
HTTPS wraps HTTP in TLS encryption. The TLS handshake authenticates the server (certificate), negotiates encryption, and exchanges keys. TLS 1.2 takes 2 round trips; TLS 1.3 takes 1 (or 0 with resumption). SNI (Server Name Indication) sends the target hostname in the clear so the server can pick the right certificate — this leaks the hostname to eavesdroppers; ECH (Encrypted Client Hello) encrypts it.
# HTTPS = HTTP over TLS
# 1. TCP connection (port 443)
# 2. TLS handshake:
ClientHello -> server
- supported TLS versions, cipher suites, extensions
- SNI: hostname (example.com)
ServerHello + Certificate + ServerHelloDone <- server
- selected cipher, certificate (public key)
ClientKeyExchange + ChangeCipherSpec -> server
- encrypted pre-master secret (RSA) or ECDHE params
ChangeCipherSpec + Finished <- server
# 3. Encrypted HTTP exchange begins
GET /page HTTP/1.1
Host: example.com
# TLS 1.3 collapses this to 1 round trip (1-RTT)
# 0-RTT resumption for returning connectionsHTTPS Connection
HTTPS is HTTP over TLS, typically on port 443. The HTTP messages themselves are unchanged — TLS just encrypts the underlying TCP traffic. To enforce HTTPS, redirect HTTP→HTTPS (301) and set the HSTS header so browsers remember to use HTTPS for the specified duration. HSTS preload lists (maintained by browsers) hardcode this behavior so even the first request goes HTTPS.
# HTTP on port 80 (plaintext)
GET /page HTTP/1.1
Host: example.com
# HTTPS on port 443 (TLS-encrypted)
# Same HTTP messages, but the wire traffic is encrypted.
GET /page HTTP/1.1
Host: example.com
# Force HTTPS via redirect
HTTP/1.1 301 Moved Permanently
Location: https://example.com/page
# Force HTTPS via HSTS (browser remembers)
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
# Modern browsers default to https:// for bare domains
# (some even refuse plain HTTP for sensitive endpoints)Certificates
X.509 certificates bind a public key to a domain, signed by a Certificate Authority (CA). The chain of trust goes from the server cert up through intermediates to a root CA pre-installed in the browser. Browsers verify the chain, the expiration date, and that the SAN (Subject Alternative Name) matches the requested hostname. Wildcard certs (*.example.com) and multi-domain certs (multiple SANs) cover multiple hostnames.
# X.509 certificate contains:
# - Subject: example.com (CN is deprecated; use SAN)
# - Subject Alternative Name (SAN): example.com, www.example.com
# - Issuer: Let's Encrypt / DigiCert (CA)
# - Validity: Not Before / Not After
# - Public key (RSA or ECDSA)
# - Signature by CA
# Chain of trust:
# Root CA (self-signed, in browser trust store)
# -> Intermediate CA
# -> Server cert (your domain)
# Server sends leaf + intermediate during handshake.
# Browser verifies: signature chain back to a trusted root.
# Inspect a cert:
openssl s_client -connect example.com:443 -showcerts
openssl x509 -in cert.pem -text -nooutHSTS (HTTP Strict Transport Security)
HSTS tells browsers to always use HTTPS for a domain, preventing SSL-stripping attacks. max-age is in seconds (1 year = 31536000). includeSubDomains extends it to all subdomains. preload submits the domain to a hardcoded browser list so even the first visit is HTTPS — strong protection but hard to undo (removal takes months). Always redirect HTTP→HTTPS first, then add HSTS, then optionally preload.
# Tell browser to use HTTPS for 6 months
Strict-Transport-Security: max-age=15768000
# Include subdomains
Strict-Transport-Security: max-age=15768000; includeSubDomains
# Submit to preload list (browsers hardcode the domain)
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
# Effects:
# - http://example.com -> browser rewrites to https://
# - Self-signed cert -> browser blocks (no override)
# - prevents SSL stripping attacks (downgrade to HTTP)
# Preload list: https://hstspreload.org
# Once preloaded, removal is slow and difficult.TLS Versions & Ciphers
Disable SSLv2/v3 and TLS 1.0/1.1 — they have known vulnerabilities (POODLE, BEAST). TLS 1.3 (2018) is the modern standard: faster handshake, removes weak ciphers, mandates forward secrecy. TLS 1.2 with ECDHE + AES-GCM is acceptable. Use tools like SSL Labs to grade your config. Forward secrecy (ECDHE) ensures past traffic stays encrypted even if the private key leaks later.
# TLS versions (deprecate old ones!)
# SSLv2, SSLv3 - BROKEN (POODLE), disable
# TLS 1.0, 1.1 - deprecated (2020), disable
# TLS 1.2 - widely supported, OK
# TLS 1.3 - fastest, most secure (preferred)
# Modern cipher suites (TLS 1.3):
# TLS_AES_256_GCM_SHA384
# TLS_CHACHA20_POLY1305_SHA256
# TLS_AES_128_GCM_SHA256
# TLS 1.2 recommended:
# ECDHE-ECDSA-AES256-GCM-SHA384 (forward secrecy + AEAD)
# ECDHE-RSA-AES256-GCM-SHA384
# Server config (nginx example)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
# Test: https://www.ssllabs.com/ssltest/HTTP/2
HTTP/2 Features
HTTP/2 keeps HTTP semantics (methods, status codes, headers) but changes the wire format to binary frames. Key wins: multiplexing many requests over one TCP connection (no head-of-line blocking at HTTP level), HPACK header compression, and stream prioritization. Negotiated via ALPN over TLS (most common) or h2c upgrade over plaintext. Server Push was deprecated in 2022 due to poor real-world performance.
# HTTP/2 (RFC 9113, 2015) - binary, multiplexed
# - Binary framing (not text)
# - Multiplexing: many streams over 1 TCP connection
# - Header compression (HPACK)
# - Server Push (deprecated in 2022)
# - Stream prioritization
# - Single connection per origin
# Negotiation:
# - Over TLS: ALPN extension advertises "h2"
# - Over plaintext: h2c upgrade (rare in practice)
#curl with HTTP/2:
curl -v --http2 https://example.com
# (Connection header shows HTTP/2)
# Backward compatible: same methods, status codes, headers, semantics
# Only the wire format changes.Multiplexing
Multiplexing is HTTP/2's headline feature. Many streams share one TCP connection, with frames interleaved — no more 6-connection-per-origin limit, no head-of-line blocking at the HTTP layer. This eliminates HTTP/1.1 hacks like domain sharding, file concatenation, and image inlining. Note: TCP-level head-of-line blocking still exists (a lost packet stalls all streams until retransmitted) — that's what HTTP/3 fixes with QUIC.
# HTTP/1.1: 6 parallel connections per origin, each request blocks its connection
# (head-of-line blocking at the HTTP level)
# HTTP/2: 1 connection, many parallel streams
# Each request/response is a stream with a unique ID.
# Frames carry data for multiple streams interleaved:
# Stream 1: HEADERS, DATA (request 1)
# Stream 3: HEADERS, DATA (request 2)
# Stream 5: HEADERS, DATA (request 3)
# Server responds with interleaved frames on each stream.
# Browser opens one TCP+TLS connection per origin,
# then sends all requests as concurrent streams.
# Eliminates: domain sharding, concatenation, inlining
# (those were HTTP/1.1 optimization hacks)Server Push (Deprecated)
Server Push let the server send resources before the client asked. In theory, this saves a round trip for critical assets. In practice it was deprecated in 2022: the server can't know the client's cache state, so it over-pushes, often wasting bandwidth. Browsers now ignore PUSH_PROMISE frames. Modern alternatives: <link rel=preload> in HTML, or 103 Early Hints (the server sends preload hints before the final response).
# Server proactively sends resources the client will need
GET /index.html HTTP/2
Host: example.com
HTTP/2 200 OK
Content-Type: text/html
Link: </style.css>; rel=preload; as=style
# Server then pushes style.css on a new stream
PUSH_PROMISE (stream 2, /style.css)
<headers + body of /style.css>
# Then the actual /index.html body follows.
# DEPRECATED (2022): browsers ignore it because:
# - Client cache might already have the asset
# - Server can't know what client has cached
# - Net performance negative in real deployments
# Modern alternative: <link rel="preload" href="...">
# or 103 Early HintsStream Prioritization
HTTP/2 let clients express stream priority via a dependency tree with weights. The server used this to allocate bandwidth: e.g., CSS before images. RFC 9113 (2022) removed the explicit PRIORITY frame, leaving scheduling to the server — many implemented their own. The Extensible Prioritization Scheme (RFC 9218) is an optional modern alternative. Prioritization matters most on slow connections.
# HTTP/2 streams have priority (weight + dependency tree)
# Originally: client sends PRIORITY frames
# Client says: "CSS (stream 3) is more important than image (stream 5)"
# PRIORITY frame (original model):
# - stream dependency (parent)
# - weight (1-256)
# RFC 9113 (2022) removed explicit prioritization
# - Leaves it to the server
# - Many servers implement their own scheduler
# Practical effect: server allocates bandwidth
# - HTML, CSS, JS -> high priority
# - Images, fonts -> lower priority
# - Critical render path optimized
# Clients can still signal via Extensible Prioritization Scheme
# (RFC 9218) - optional.HPACK Header Compression
HPACK compresses HTTP headers using a static table of common headers, a per-connection dynamic table that learns recently sent values, and Huffman coding. This drastically cuts overhead — a 1KB cookie sent on every request becomes a tiny indexed reference. HPACK is carefully designed to avoid the CRIME/BREACH compression oracle attacks that plagued earlier TLS compression. HTTP/3 uses a similar scheme called QPACK.
# HTTP/1.1: headers sent as plaintext on every request (redundant)
# Cookies alone can exceed 1KB per request.
# HTTP/2: HPACK compresses headers
# - Static table: 61 common headers (e.g., :method GET)
# - Dynamic table: per-connection, learns recently sent headers
# - Huffman coding for strings
# Example: first request sends full User-Agent (200 bytes)
# Subsequent requests reference the dynamic table entry (2 bytes)
# Indexed header representation:
# 1 bit set + index -> "use entry N"
# Literal with incremental indexing:
# "add this to the dynamic table for future reuse"
# Security: CRIME attack exploited compression oracles.
# HPACK mitigates by never compressing sensitive + attacker-controlled
# data together.HTTP/3 & QUIC
QUIC Protocol
QUIC is a transport protocol over UDP that integrates TLS 1.3 and provides reliable, multiplexed streams. Moving to UDP lets QUIC evolve faster than kernel TCP. Key advantages: no TCP-level head-of-line blocking (lost packet stalls only one stream), 0-RTT resumption for returning connections, and connection migration (a phone switching from Wi-Fi to cellular keeps the connection). HTTP/3 runs over QUIC.
# QUIC = Quick UDP Internet Connections (Google, then IETF)
# - Transport protocol over UDP (not TCP)
# - Built-in TLS 1.3 encryption
# - Multiple streams, independent per-stream delivery
# - 0-RTT connection resumption
# - Connection migration (survives IP changes)
# - Better loss recovery than TCP
# Why UDP? TCP is in the kernel and hard to evolve.
# QUIC moves reliability + congestion control to user space.
# Wire format: UDP packets containing QUIC packets
# - Initial, Handshake, 1-RTT, 0-RTT packets
# - Frames carry stream data, acks, flow control
# Negotiation:
# Server advertises HTTP/3 via Alt-Svc header on HTTP/2 response:
Alt-Svc: h3=":443"; ma=86400
# Browser caches this and uses HTTP/3 next time.HTTP/3 Features
HTTP/3 preserves HTTP semantics but runs over QUIC instead of TCP. The big win: no head-of-line blocking. In HTTP/2 a single lost TCP packet stalls all multiplexed streams until retransmission; in HTTP/3 only the affected stream waits. Plus 0-RTT resumption and connection migration. The cost: more CPU (QUIC in user space) and UDP sometimes blocked/throttled by networks. Adoption is growing — most major sites support it.
# HTTP/3 (RFC 9114, 2022) - HTTP semantics over QUIC
# Same methods, status codes, headers as HTTP/2
# - Binary framing (similar to HTTP/2)
# - Multiplexing over QUIC streams (no HoL blocking!)
# - QPACK header compression (HPACK variant)
# - 0-RTT for returning clients
# - Connection migration
# Loss recovery: per-stream, not per-connection
# A lost packet stalls only that stream, others continue.
# Head-of-line blocking eliminated at the transport layer:
# HTTP/2: lost TCP packet blocks ALL streams (kernel retransmit)
# HTTP/3: lost QUIC packet blocks only that stream
# curl with HTTP/3 (requires special build):
curl -v --http3 https://example.com0-RTT Connection Resumption
0-RTT lets returning clients send data in their first flight (no round trip before the request). This shaves tens to hundreds of milliseconds off repeat visits. The catch: the early data could be replayed by an attacker, so it must be limited to idempotent operations (GET, HEAD, PUT/DELETE if idempotent). Servers should reject 0-RTT for POST and other state-changing methods. Anti-replay mechanisms add complexity.
# TLS 1.3 + QUIC: 0-RTT resumption for returning clients
# First visit: full handshake (1-RTT in TLS 1.3)
Client -> Server: ClientHello + key share
Server -> Client: ServerHello + key share + Finished
Client -> Server: Finished + first HTTP request
# Returning visit (0-RTT):
Client -> Server: ClientHello + early data (HTTP request)
# Server can respond immediately, no round trip!
# Trade-offs:
# + Saves 1 RTT on subsequent visits (faster page loads)
# - Vulnerable to replay attacks on early data
# - Only safe for idempotent methods (GET, HEAD)
# - Server should reject 0-RTT for non-idempotent requests
# Configure carefully:
# - Limit early data to safe methods
# - Use anti-replay mechanismsConnection Migration
QUIC connections are identified by a connection ID rather than the IP/port 4-tuple. This means a connection survives IP changes — a phone switching from Wi-Fi to cellular, or a laptop moving between networks, keeps the connection alive. The server validates the new path with PATH_CHALLENGE/PATH_RESPONSE. TCP can't do this because the kernel identifies connections by the 4-tuple. Big win for mobile.
# TCP connection identified by 4-tuple:
# (src IP, src port, dst IP, dst port)
# If your phone switches Wi-Fi -> cellular, IP changes ->
# the TCP connection dies. Browser must reconnect.
# QUIC uses a connection ID (random, in every packet).
# The 4-tuple can change mid-connection; the connection ID
# identifies the connection.
# Scenario:
# 1. Phone on Wi-Fi, downloading a large file over HTTP/3
# 2. Phone walks out, switches to cellular
# 3. Packets now arrive from a new IP, same connection ID
# 4. Server continues serving the same connection!
# 5. No reconnect, no re-handshake, no failed download.
# Validation: server sends PATH_CHALLENGE, client replies PATH_RESPONSE.HTTP/1.1 vs HTTP/2 vs HTTP/3
HTTP/1.1 is text over TCP with 6 parallel connections. HTTP/2 is binary over TCP with multiplexing (but TCP-level HoL blocking). HTTP/3 is binary over QUIC with no HoL blocking, mandatory encryption, 0-RTT, and connection migration. Adoption is incremental: servers advertise HTTP/3 via the Alt-Svc header on HTTP/2 responses, and browsers upgrade on the next visit. All three share HTTP semantics.
# Feature comparison
# Property | HTTP/1.1 | HTTP/2 | HTTP/3
# -----------------+--------------+--------------+--------------
# Transport | TCP | TCP | QUIC (UDP)
# Format | Text | Binary | Binary
# Multiplexing | No (6 conns) | Yes | Yes
# HoL blocking | HTTP-level | TCP-level | None
# Header compress | No | HPACK | QPACK
# Server Push | No | Yes (deprecated) | No
# Encryption | Optional | Optional | Mandatory (TLS 1.3)
# 0-RTT | No | No | Yes
# Connection migration | No | No | Yes
# Handshake RTT | 1+ | 1+ | 0-1 (with resumption)
# Most sites today: HTTP/2 over TLS, with HTTP/3 available via Alt-SvcRedirection (3xx)
301 vs 302
301 means permanent: browsers cache the redirect, bookmarks update, and SEO link equity passes to the new URL. 302 means temporary: nothing is cached, SEO stays with the original. Historically, both converted POST→GET (a spec violation that became de facto). To strictly preserve the method, use 307 (temporary) or 308 (permanent). For SEO migrations use 301; for login redirects use 302 or 307.
# 301 Moved Permanently
HTTP/1.1 301 Moved Permanently
Location: https://newsite.com/page
# - Permanent; browser & SEO caches it
# - Link juice transfers to new URL (SEO)
# - Bookmark updates
# - HTTP/1.1 spec: should preserve method, but historically
# POST -> GET (causing data loss)
# 302 Found
HTTP/1.1 302 Found
Location: /login
# - Temporary
# - Don't update bookmarks
# - SEO link juice stays with original URL
# - HTTP/1.1 spec: should preserve method, but historically
# POST -> GET
# Use 301 for permanent moves (site migrations).
# Use 302 for temporary redirects (login flows, A/B tests).307 vs 308
307 and 308 are the method-preserving counterparts of 302 and 301. They fix the historical POST→GET conversion bug. 307 is temporary, 308 is permanent — both keep the original method and body. Use them when redirecting non-GET requests, especially in APIs where a POST→GET conversion would silently drop the body. 308 is the modern choice for permanent API endpoint moves.
# 307 Temporary Redirect (HTTP/1.1, method-preserving)
HTTP/1.1 307 Temporary Redirect
Location: /new-endpoint
# - Temporary
# - Method PRESERVED: POST stays POST, body stays intact
# - Browser prompts before re-issuing POST to different host
# 308 Permanent Redirect (RFC 7538)
HTTP/1.1 308 Permanent Redirect
Location: https://newsite.com/api
# - Permanent
# - Method PRESERVED
# - Like 301 but without the POST->GET conversion
# When to use:
# - 307: temporary redirect that must preserve method
# - 308: permanent redirect that must preserve method
# (e.g., API endpoint moved, clients still POSTing)
# Most browsers support both since ~2015.303 See Other (PRG Pattern)
303 See Other explicitly tells the browser to follow up with a GET — the right tool for the Post/Redirect/Get (PRG) pattern. After processing a POST, redirect with 303 to a GET page. If the user refreshes, they re-issue the safe GET (not the POST), avoiding duplicate form submissions. Use 303 for PRG, 307/308 when you need to preserve the original method, 301/302 for plain navigation.
# 303 See Other - explicitly converts to GET
HTTP/1.1 303 See Other
Location: /success
# - Always converts to GET (no body)
# - Used in Post/Redirect/Get (PRG) pattern
# PRG pattern: avoid double-submit on refresh
# 1. User submits form
POST /checkout HTTP/1.1
Host: shop.com
Content-Type: application/x-www-form-urlencoded
item=book&qty=1
# 2. Server processes, redirects to a GET page
HTTP/1.1 303 See Other
Location: /order/123/confirmation
# 3. Browser issues GET (safe to refresh)
GET /order/123/confirmation HTTP/1.1
Host: shop.com
HTTP/1.1 200 OK
# "Order confirmed!"Redirect Chains & Loops
Redirect chains add latency (each hop = round trip) and should be flattened — point the original URL directly at the final destination. Redirect loops are fatal: browsers detect them after a few hops and show an error page. Common causes: HTTPS↔HTTP misconfiguration behind a load balancer (use X-Forwarded-Proto), or trailing-slash conflicts. HSTS avoids HTTP→HTTPS redirect loops by having the browser rewrite internally.
# Redirect chain: A -> B -> C
GET /a -> 301 -> /b
GET /b -> 301 -> /c
GET /c -> 200 OK
# Each hop adds a round trip; chains hurt performance.
# Browsers cap redirects (typically 20), then error.
# Redirect loop (fatal):
GET /a -> 302 -> /b
GET /b -> 302 -> /a
GET /a -> 302 -> /b (browser detects loop, shows error)
# Common cause: HTTPS <-> HTTP loop
# Server A redirects HTTP -> HTTPS
# Server B (behind TLS terminator) sees HTTP internally,
# redirects to HTTPS again -> loop
# Fix: trust X-Forwarded-Proto, or use HSTS instead of redirects
# Common cause: trailing slash mismatch
# /api/users -> 301 -> /api/users/ -> 301 -> /api/users/
# (server config bug)Common Redirect Use Cases
Common redirect use cases: HTTP→HTTPS (301), apex↔www (301), old→new URL migrations (301), login redirects (302/303), PRG pattern (303), API endpoint moves (308), and CDN origin fallbacks. Always prefer 301/308 for permanent moves so caches and SEO update. Use HSTS instead of HTTP→HTTPS redirects where possible — it avoids the redirect round trip on subsequent visits.
# 1. HTTP -> HTTPS upgrade
HTTP/1.1 301 Moved Permanently
Location: https://example.com/
# 2. www -> apex (or vice versa)
HTTP/1.1 301 Moved Permanently
Location: https://example.com/
# 3. Old URL -> new URL (site migration)
HTTP/1.1 301 Moved Permanently
Location: https://example.com/new-page
# 4. Login required (temporary)
HTTP/1.1 302 Found
Location: /login?return=/dashboard
# 5. POST form -> confirmation (PRG)
HTTP/1.1 303 See Other
Location: /success
# 6. API endpoint moved (preserve method)
HTTP/1.1 308 Permanent Redirect
Location: /v2/users
# 7. CDN origin fallback (when content not cached)
HTTP/1.1 302 Found
Location: https://origin.example.com/assetContent Types (MIME)
Common MIME Types
MIME types (media types) describe the format of a body. They consist of type/subtype and optional parameters (charset, boundary). Common types: text/html for pages, application/json for APIs, image/* and video/* for media. application/octet-stream is the fallback for unknown binary data — browsers will download rather than try to render it. IANA maintains the official registry.
# Text
text/html # HTML pages
text/plain # plain text
text/css # stylesheets
text/csv # comma-separated values
text/javascript # JavaScript (legacy, was application/javascript)
# Application
application/json # JSON
application/xml # XML
application/javascript # JavaScript (modern, text/javascript also OK)
application/pdf # PDF
application/zip # ZIP archive
application/octet-stream # binary (unknown type, forces download)
application/x-www-form-urlencoded # form data
application/ld+json # JSON-LD (linked data)
# Image
image/jpeg, image/png, image/gif, image/webp, image/svg+xml, image/avif
# Audio/Video
audio/mpeg, audio/ogg, video/mp4, video/webm
# Multipart
multipart/form-data # file uploads
multipart/byteranges # partial contentContent-Type Header
The Content-Type header declares the MIME type of the body. For text types, charset specifies the encoding (default for text/* is ISO-8859-1; always set UTF-8 explicitly). For multipart bodies, boundary separates parts. Browsers may 'sniff' the type if missing or vague — prevent this with X-Content-Type-Options: nosniff, which forces them to honor the declared type (security hardening against XSS via file uploads).
# Response Content-Type
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
# Request Content-Type (for bodies)
POST /api/users HTTP/1.1
Content-Type: application/json
{"name": "Alice"}
# charset parameter (text types)
Content-Type: text/html; charset=UTF-8
# boundary parameter (multipart)
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
# X-Content-Type-Options: nosniff
# tells browser not to sniff/override the declared typemultipart/form-data
multipart/form-data encodes multiple parts (fields and files) in one body, separated by a boundary. Each part has its own Content-Disposition (with optional filename) and Content-Type. This is the format browsers use when submitting forms with file inputs. It's the only practical way to upload files via HTML forms, though for APIs, base64-in-JSON or direct PUT to a storage URL is often simpler.
# File upload form
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----boundary123
------boundary123
Content-Disposition: form-data; name="title"
My Photo
------boundary123
Content-Disposition: form-data; name="file"; filename="photo.jpg"
Content-Type: image/jpeg
<binary JPEG bytes>
------boundary123--
# Each part has its own headers (Content-Disposition, Content-Type).
# Final boundary has trailing --.
# Used for file uploads (browsers' <input type="file">).
# Less efficient than JSON for plain fields - overhead per part.application/json
application/json is the dominant API format — lightweight, human-readable, and natively supported in every language. JSON is UTF-8 by default (charset parameter is redundant but harmless). Vendor-specific media types (application/vnd.api+json, application/vnd.github.v3+json) enable versioning and profile negotiation. RFC 7807 (application/problem+json) standardizes error responses.
# Most common API Content-Type
POST /api/users HTTP/1.1
Content-Type: application/json
Accept: application/json
{
"name": "Alice",
"age": 30,
"tags": ["admin", "user"],
"metadata": null
}
# Variants:
# application/json - standard
# application/json; charset=utf-8 - JSON is UTF-8 by default, charset is redundant
# application/ld+json - JSON-LD (linked data with @context)
# application/vnd.api+json - JSON:API spec
# application/problem+json - RFC 7807 problem details for errors
# Vendor types: application/vnd.company.v1+json
# (custom versioned JSON)charset Parameter
The charset parameter declares the text encoding. Always set it for text/* types — the default ISO-8859-1 causes mojibake for non-ASCII characters. UTF-8 is the universal best choice. For JSON, charset is redundant (JSON is UTF-8 by spec). For XML, the spec defaults to UTF-8 but declaring it explicitly avoids the BOM-sniffing overhead. Mismatched charsets produce garbage characters.