Skip to content

HTTP Hoja de referencia

HyperText Transfer Protocol for client-server communication.

01

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

http
# 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
# 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
# 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: close

Message 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).

http
# 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.

http
# 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.com
02

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

http
# 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.

http
# 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).

http
# 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 returned

PATCH — 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.

http
# 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.

http
# 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.

http
# 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, POST
03

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

http
# 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=style

2xx 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).

http
# 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/2048

3xx 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.

http
# 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/api

4xx 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.

http
# 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: 60

5xx 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.

http
# 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 Timeout

Status 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'.

http
# 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).
04

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.

http
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: 42

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

http
# 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.

http
# 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.

http
# 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.

http
# 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 changed

Custom & 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).

http
# 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.com
05

Response 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
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.com

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

http
# 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-8

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

http
# 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.com

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

http
# 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-Count

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

http
# 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: 120
06

Cookies & Set-Cookie

Setting Cookies (Set-Cookie)

Set-Cookie instructs the browser to store a cookie. Each attribute controls scope and lifetime: Domain/Path restrict where it's sent; Max-Age (seconds) or Expires set lifetime; Secure limits to HTTPS; HttpOnly blocks JS access; SameSite controls cross-site sending. To delete a cookie, set Max-Age=0 with the same Path/Domain.

http
# Basic cookie
HTTP/1.1 200 OK
Set-Cookie: session=abc123

# With attributes
Set-Cookie: session=abc123; Path=/; Domain=example.com; Max-Age=3600; HttpOnly; Secure; SameSite=Lax

# Multiple cookies = multiple Set-Cookie headers
Set-Cookie: session=abc123; HttpOnly; Secure
Set-Cookie: theme=dark; Max-Age=86400
Set-Cookie: lang=en-US; Path=/

# Deleting a cookie: set Max-Age=0 or expired Expires
Set-Cookie: session=; Max-Age=0; Path=/

Cookie Attributes

Cookie attributes control security and scope. Secure + HttpOnly + SameSite is the modern baseline. SameSite=Lax is the browser default — it allows cookies on top-level navigations but blocks them in third-party contexts (defeats most CSRF). __Host- prefix forces Path=/, no Domain, and Secure — preventing subdomain cookie injection. __Secure- requires Secure.

http
# Security attributes
Secure           # only sent over HTTPS
HttpOnly         # not accessible via document.cookie (XSS protection)
SameSite=Strict  # never sent on cross-site requests
SameSite=Lax     # sent on top-level navigation (default in modern browsers)
SameSite=None    # sent cross-site (requires Secure)

# Scope attributes
Domain=example.com    # visible to subdomains too
Path=/                # visible to all paths
Path=/api             # only /api and below

# Lifetime attributes
Max-Age=3600          # seconds (preferred)
Expires=Wed, 21 Oct 2025 07:28:00 GMT  # absolute date

# __Host- and __Secure- prefixes (extra protection)
Set-Cookie: __Host-session=abc; Path=/; Secure; HttpOnly; SameSite=Lax

Sending Cookies (Cookie)

The Cookie request header sends all matching cookies as name=value pairs (no attributes). Cookies are sent automatically by the browser on every matching request. For cross-origin fetch with cookies, the client must set credentials: 'include' AND the server must echo a specific origin (not *) with Access-Control-Allow-Credentials: true.

http
# Browser sends stored cookies back to the server
GET /api/profile HTTP/1.1
Host: example.com
Cookie: session=abc123; theme=dark; lang=en-US

# Format: name=value pairs separated by "; "
# No attributes are sent back - only name=value

# Server reads cookies (Express.js example)
# const sessionId = req.headers.cookie
#   .split(';').find(c => c.trim().startsWith('session='))
#   .split('=')[1];

# Cross-site requests (with credentials)
fetch('https://api.example.com/data', {
  credentials: 'include'  # sends cookies cross-origin
});
# Server must respond with:
# Access-Control-Allow-Origin: https://app.example.com (specific, no *)
# Access-Control-Allow-Credentials: true

Cookie vs Token

Cookies are stored and sent automatically by the browser; tokens (JWT) are stored in JS and sent manually. Cookies are vulnerable to CSRF (mitigated by SameSite). Tokens are vulnerable to XSS (mitigated by short lifetimes and secure storage). Cookies suit same-origin web apps; tokens suit APIs and SPAs. The best of both: store refresh token in HttpOnly cookie, access token in memory.

http
# Cookie-based auth (stateful server-side session)
POST /login HTTP/1.1
Host: example.com
Content-Type: application/json

{"user": "alice", "password": "secret"}

# Server response
HTTP/1.1 200 OK
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax

# Subsequent requests carry cookie automatically
GET /api/profile HTTP/1.1
Cookie: session=abc123

# Token-based auth (stateless, e.g., JWT)
# Server response
HTTP/1.1 200 OK
Content-Type: application/json

{"token": "eyJhbGc..."}

# Subsequent requests carry token in header (manual)
GET /api/profile HTTP/1.1
Authorization: Bearer eyJhbGc...

Third-Party Cookies & SameSite

Third-party cookies are set by a different domain than the page the user is visiting — long used for tracking and ads. SameSite=None+Secure is required for them. Modern browsers (Chrome, Firefox, Safari) are phasing out third-party cookies. Alternatives include CHIPS (Partitioned cookies, scoped per top-level site), the Storage Access API, and server-side tracking. ITP (Safari) blocks them outright.

http
# First-party cookie: domain matches the page
Set-Cookie: session=abc; Domain=example.com
# Visiting example.com -> sent to example.com (first-party)

# Third-party cookie: embedded in another site's page
# e.g., analytics.com cookie loaded via <img> on shop.com
Set-Cookie: tracker=xyz; Domain=analytics.com; SameSite=None; Secure

# SameSite behavior:
# Strict  -> NOT sent on shop.com -> analytics.com requests
# Lax     -> NOT sent on embedded cross-site requests
#            (sent on top-level navigation)
# None    -> sent cross-site (requires Secure)

# Browsers are phasing out third-party cookies (2024+).
# Alternatives: Partitioned cookies (CHIPS)
Set-Cookie: tracker=xyz; Partitioned; Secure; SameSite=None
07

Authentication

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.

http
# 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.

http
# 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).

http
# 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).

http
# 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.

http
# 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.

http
# 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 control
08

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

http
# 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.

http
# 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).

http
# 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 compression

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

http
# 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.

http
# 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.5
09

Caching

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.

http
# 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 cache

ETag & 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).

http
# 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.

http
# 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 elsewhere

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

http
# 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.

http
# 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)
10

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.

http
# 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.com

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

http
# 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 response

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

http
# 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.

http
# 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.

http
# 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 response
11

HTTPS & 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.

http
# 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 connections

HTTPS 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
# 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.

http
# 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 -noout

HSTS (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.

http
# 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.

http
# 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/
12

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
# 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
# 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).

http
# 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 Hints

Stream 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
# 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
# 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.
13

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.

http
# 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
# 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.com

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

http
# 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 mechanisms

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

http
# 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.

http
# 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-Svc
14

Redirection (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.

http
# 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.

http
# 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.

http
# 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.

http
# 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.

http
# 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/asset
15

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

http
# 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 content

Content-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).

http
# 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 type

multipart/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.

http
# 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.

http
# 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.

http
# charset specifies the text encoding
Content-Type: text/html; charset=UTF-8      # preferred
Content-Type: text/html; charset=iso-8859-1 # legacy

# For JSON, charset is redundant (JSON spec mandates UTF-8)
Content-Type: application/json              # correct
Content-Type: application/json; charset=utf-8  # harmless but redundant

# Default charsets (when omitted):
# text/*                  -> ISO-8859-1 (legacy, problematic)
# application/json        -> UTF-8 (mandatory per spec)
# application/xml         -> UTF-8 (per XML spec)

# Always declare charset for text types to avoid mojibake.
# Mismatched charset = broken characters.

# In HTML, also declare in the document:
<meta charset="UTF-8">
16

Chunked Transfer

Transfer-Encoding: chunked

Chunked transfer encoding lets the server stream a body without knowing its total length upfront. Each chunk is prefixed by its length in hex, followed by CRLF, the data, and CRLF. A zero-length chunk signals the end. This is essential for dynamically generated responses (template rendering, database queries) where the size isn't known until the data is produced. HTTP/2 and HTTP/3 don't use chunked encoding — they have their own framing.

http
# When the server doesn't know the final Content-Length
# (e.g., streaming, dynamically generated content),
# it sends the body in chunks.

HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked

4

Wiki

6

pedia 

E

in chunks.

0




# Each chunk: <hex-length>
<data>

# Final chunk: 0
 (zero length)
# Then optional trailer headers + final 

Streaming Responses

Streaming responses use chunked encoding to push data incrementally. Server-Sent Events (SSE) is the canonical pattern: the server keeps the connection open and writes events as 'data: <payload>\n\n'. The browser's EventSource API receives them as they arrive. Other uses: progressive HTML rendering (flush as you go), large JSON arrays, log tailing. Ensure no intermediate proxy buffers the response — set X-Accel-Buffering: no for nginx.

http
# Server-Sent Events (SSE): one-way streaming over HTTP
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
Transfer-Encoding: chunked

data: {"msg": "hello"}


data: {"msg": "world"}


data: {"msg": "done"}



# Each event: "data: <payload>

"
# Browser keeps the connection open via EventSource API.

# Other streaming patterns:
# - Server-side rendering: flush HTML as it's generated
# - Large JSON: stream array elements
# - Logs tailing: continuous chunk stream

# Don't buffer: set X-Accel-Buffering: no (nginx)

Trailer Headers

Trailer headers are sent after the body when using chunked encoding. They're useful for summary stats computed during streaming — e.g., a row count, checksum, or processing duration. The Trailer header announces which trailers will appear. Some headers are forbidden as trailers (Host, Content-Length, Transfer-Encoding, auth headers) because they affect connection framing. Client support is patchy — many clients ignore trailers.

http
# Trailer headers are sent AFTER the body (with chunked encoding)
HTTP/1.1 200 OK
Content-Type: application/json
Transfer-Encoding: chunked
Trailer: X-Total-Count, X-Duration

5

{"a":1

5

,"b":2

0

X-Total-Count: 2

X-Duration: 0.045s




# Use case: compute summary stats during streaming
# (e.g., total count, checksum) and send at the end.

# The "Trailer" header declares which trailers to expect.
# Some headers are forbidden as trailers:
# - Transfer-Encoding, Content-Length, Host
# - Authentication-related headers

Content-Length vs Chunked

Content-Length tells the client the exact body size up front — simple, enables progress bars, and lets the connection be reused cleanly. Chunked encoding lets you stream without knowing the size. You must use one or the other (or close the connection at the end). Sending both is ambiguous; per spec chunked wins, but old clients may misbehave. For static files, always use Content-Length; for dynamic/streaming, use chunked.

http
# Content-Length: known body size up front
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 11

Hello World

# Pros: simple, allows progress bars, enables persistent conn reuse
# Cons: requires knowing the size before sending

# Transfer-Encoding: chunked: stream without knowing size
HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked

5
Hello

6
 World

0



# Pros: can stream as data is produced
# Cons: no progress bar, slightly more overhead per chunk

# Rule: you must use ONE of these (or close the connection).
# Using both is ambiguous - chunked wins per spec but is buggy in old clients.

HTTP/2 Streaming (No Chunked)

HTTP/2 and HTTP/3 don't use chunked encoding — they have their own binary framing. DATA frames carry body bytes; the END_STREAM flag signals the end. This gives the same streaming capability without the per-chunk overhead of hex-length framing. The Transfer-Encoding header is illegal in HTTP/2 (RFC 9113). Content-Length is still allowed but optional. The semantics of streaming are preserved; only the wire mechanism changes.

http
# HTTP/2 and HTTP/3 don't use Transfer-Encoding: chunked.
# They have their own framing: DATA frames carry body bytes,
# END_STREAM flag marks the end.

# In HTTP/2, "streaming" means sending multiple DATA frames
# on a single stream, with the END_STREAM flag on the last.

# Server push of partial response:
# HEADERS frame (status, headers)
# DATA frame (chunk 1)
# DATA frame (chunk 2)
# DATA frame + END_STREAM (chunk 3, final)

# Effect: same streaming behavior as chunked,
# but at the framing layer, not the encoding layer.

# Transfer-Encoding header is illegal in HTTP/2.
# Content-Length is allowed but optional.
17

Range Requests

Range Header

Range requests let a client fetch part of a resource — essential for resumable downloads, video seeking, and parallel chunked downloads. The Range header specifies byte ranges. The server responds with 206 Partial Content and a Content-Range header showing what was returned. If the server doesn't support ranges, it ignores the header and returns 200 with the full body. Always check Accept-Ranges first.

http
# Request a portion of a resource (e.g., resume downloads, video seeking)
GET /video.mp4 HTTP/1.1
Host: example.com
Range: bytes=0-1023        # first 1024 bytes

# Server response: 206 Partial Content
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1023/1048576
Content-Length: 1024
Content-Type: video/mp4

<1024 bytes of video>

# Range formats:
Range: bytes=0-1023        # bytes 0 through 1023
Range: bytes=1024-         # from byte 1024 to end
Range: bytes=-1024         # last 1024 bytes
Range: bytes=0-1023,2048-3071  # multiple ranges (multipart response)

Accept-Ranges Header

The Accept-Ranges header advertises whether the server supports range requests. bytes means yes; none (or absence) means no. Clients should check this before relying on Range — a server that ignores Range will return the full body with 200. Browsers use this automatically for <video> seeking (jump to a timestamp = range request) and resumable downloads. Most CDNs and static servers support ranges; dynamic endpoints often don't.

http
# Server advertises range support
HTTP/1.1 200 OK
Accept-Ranges: bytes        # supports byte-range requests
Content-Length: 1048576

# Server doesn't support ranges
HTTP/1.1 200 OK
Accept-Ranges: none

# (or omit the header entirely - same as "none")

# Client checks before sending Range:
# 1. HEAD or GET request to inspect Accept-Ranges
# 2. If "bytes", send Range: bytes=...
# 3. If "none" or missing, expect 200 with full body

# Browsers handle this transparently for video/audio,
# resumable downloads, and PDF preview.

Partial Content Response (206)

206 Partial Content is the response to a successful range request. Content-Range shows the unit (bytes), the returned range, and the total size. If the requested range exceeds the resource, the server returns 416 Range Not Satisfiable. If-Range makes the range conditional: if the validator (ETag or date) matches, return 206; if the resource changed, return 200 with the full body — preventing partial-corruption issues.

http
# Single range request
GET /file.bin HTTP/1.1
Range: bytes=100-199

# 206 Partial Content
HTTP/1.1 206 Partial Content
Content-Range: bytes 100-199/1024
Content-Length: 100
Content-Type: application/octet-stream

<100 bytes>

# Content-Range format: <unit> <start>-<end>/<total>
# total can be "*" if unknown (streaming)

# If range is out of bounds:
# Range: bytes=1000-2000 on a 500-byte file ->
HTTP/1.1 416 Range Not Satisfiable
Content-Range: bytes */500

# If-Range: only return range if validator still matches
GET /file.bin HTTP/1.1
Range: bytes=0-1023
If-Range: "etag123"
# If etag matches: 206 (partial)
# If etag differs: 200 (full body, resource changed)

Multi-Range Requests

Multi-range requests ask for several byte ranges in one request. The server responds with multipart/byteranges — each part has its own Content-Type and Content-Range. In practice, multi-range is rarely used: clients typically issue parallel single-range requests instead (simpler, more cache-friendly, easier to recover from errors). Some servers cap the number of ranges or coalesce overlapping ones. Avoid relying on multi-range for critical paths.

http
# Request multiple ranges in one request
GET /file.bin HTTP/1.1
Range: bytes=0-99,200-299,400-499

# Server responds with multipart/byteranges
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=THIS_STRING_SEPARATES

--THIS_STRING_SEPARATES
Content-Type: application/octet-stream
Content-Range: bytes 0-99/1024

<100 bytes>
--THIS_STRING_SEPARATES
Content-Type: application/octet-stream
Content-Range: bytes 200-299/1024

<100 bytes>
--THIS_STRING_SEPARATES
Content-Type: application/octet-stream
Content-Range: bytes 400-499/1024

<100 bytes>
--THIS_STRING_SEPARATES--

# Most clients use single-range; multi-range is rare in practice.

Range Use Cases

Range requests power video seeking (jumping to a timestamp), resumable downloads (continue after a pause), parallel downloads (download managers split files into chunks), PDF page preview, and reading metadata stored at file ends (ID3 tags). For all these, the server must support ranges and advertise it via Accept-Ranges: bytes. Without range support, video seeking degrades to full download, and resumable downloads are impossible.

http
# 1. Video seeking (HTML5 <video>)
# Browser sends Range when user jumps to a timestamp
GET /video.mp4 HTTP/1.1
Range: bytes=5242880-    # seek to ~5MB offset

# 2. Resumable downloads
# Pause at 50%, resume later
GET /bigfile.zip HTTP/1.1
Range: bytes=524288000-  # continue from 500MB

# 3. Parallel downloads (download managers)
# Split file into N chunks, fetch concurrently
GET /bigfile.zip HTTP/1.1
Range: bytes=0-10485759      # chunk 1 (first 10MB)
# (parallel connection)
GET /bigfile.zip HTTP/1.1
Range: bytes=10485760-20971519  # chunk 2

# 4. PDF preview (load only visible pages)
GET /doc.pdf HTTP/1.1
Range: bytes=102400-204800   # specific page region

# 5. Streaming audio with metadata at the end
# Fetch ID3 tag without downloading the whole file
18

Session Management

Cookie-Based Sessions

Cookie-based sessions store a server-side session (in memory, Redis, or a DB) keyed by a random session ID. The cookie holds only the ID. Pros: revocable (delete the session), small cookie, mature. Cons: stateful (server must store sessions), horizontal scaling needs shared session storage (Redis), vulnerable to CSRF (mitigate with SameSite). Best for traditional web apps with server-rendered pages.

http
# 1. User logs in
POST /login HTTP/1.1
Host: example.com
Content-Type: application/json

{"username": "alice", "password": "secret"}

# 2. Server validates, creates session, stores in DB/Redis,
#    returns session ID in a cookie
HTTP/1.1 200 OK
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax

# 3. Subsequent requests carry the cookie
GET /dashboard HTTP/1.1
Cookie: session=abc123

# 4. Server looks up session ID, finds user, serves request
# 5. Logout: delete session server-side, expire cookie
POST /logout HTTP/1.1
Cookie: session=abc123

HTTP/1.1 200 OK
Set-Cookie: session=; Max-Age=0; Path=/

Token-Based Sessions (JWT)

Token-based sessions (usually JWT) store the session state IN the token. The server validates the signature without a DB lookup — stateless. Pros: scalable (no session store), works across services, mobile-friendly. Cons: can't revoke until expiration (mitigate with short access tokens + refresh tokens stored server-side), larger than a session ID, vulnerable to XSS if stored in localStorage. Best for APIs and SPAs.

http
# 1. Login returns a JWT (no server-side state)
POST /login HTTP/1.1

HTTP/1.1 200 OK
Content-Type: application/json
{"token": "eyJhbGciOiJIUzI1NiJ9..."}

# 2. Client stores token (memory, sessionStorage)
# 3. Each request includes the token in a header
GET /api/profile HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

# 4. Server validates signature + claims, no DB lookup needed
# 5. Logout: client discards token (no server-side invalidation
#    until exp - this is the stateless tradeoff)

# Refresh token flow:
# - Short-lived access token (5-15 min)
# - Long-lived refresh token (days), stored server-side, revocable
# - When access token expires, exchange refresh token for new one

Session in Headers

Sessions via headers (Authorization, X-API-Key) are stateless and work for any client (browsers, mobile, CLI, server-to-server). The refresh-token flow handles expiration: short-lived access tokens limit the blast radius of a stolen token, while the refresh token (stored securely, ideally in an HttpOnly cookie) lets the client get new access tokens without re-authenticating. Detect stolen refresh tokens via rotation.

http
# Bearer token in Authorization header
GET /api/data HTTP/1.1
Authorization: Bearer eyJhbGc...

# Custom header (e.g., API gateway)
GET /api/data HTTP/1.1
X-API-Key: abc123def456

# Basic auth (per-request credentials)
GET /api/data HTTP/1.1
Authorization: Basic dXNlcjpwYXNz

# Bearer + refresh flow:
# 1. Access token expires -> API returns 401
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token"

# 2. Client uses refresh token to get a new access token
POST /token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=GmR...

# 3. New access token returned
{"access_token": "...", "expires_in": 900}

Session Expiration & Sliding

Session expiration policies: absolute (fixed lifetime from login), sliding (resets on activity), or combined (whichever comes first). Combined is most secure — caps total session length while allowing reasonable idle time. Cookies use Max-Age; server-side sessions have a separate TTL. JWTs can't slide — the exp claim is fixed at issuance. To 'extend' a JWT, issue a new one (refresh-token flow) rather than mutating the old.

http
# Absolute expiration: session dies after fixed time from login
# e.g., 8 hours after login, regardless of activity
session.expires_at = session.created_at + 8h

# Sliding/Idle expiration: resets on each activity
# e.g., session dies after 30 min of inactivity
session.expires_at = NOW() + 30min   # updated on each request

# Combined (most secure):
# - Absolute cap (e.g., 24h max from login)
# - Idle timeout (e.g., 30min inactivity)
# Whichever comes first.

# Set-Cookie with Max-Age sets cookie lifetime,
# but server-side session may expire earlier (DB cleanup).
Set-Cookie: session=abc; Max-Age=1800  # 30 min

# JWT exp claim (absolute):
# {"exp": 1516242622}  # hardcoded, can't slide

CSRF Protection

CSRF attacks exploit the fact that browsers send cookies automatically. SameSite cookies (Lax default in modern browsers) block most CSRF by refusing to send cookies on cross-site POSTs. The CSRF-token pattern adds a server-generated token to forms and validates it on submission — attackers can't read the token due to SOP. Double-submit and custom-header defenses add defense-in-depth. APIs using Bearer tokens (no cookies) are inherently CSRF-immune.

http
# CSRF (Cross-Site Request Forgery): attacker tricks the user's
# browser into sending a state-changing request using their cookies.

# Defense 1: SameSite cookies (modern default)
Set-Cookie: session=abc; SameSite=Lax   # blocks most CSRF
Set-Cookie: session=abc; SameSite=Strict # blocks all cross-site

# Defense 2: CSRF token (synchronizer pattern)
# Server embeds a random token in the form
<form action="/transfer" method="POST">
  <input type="hidden" name="csrf" value="randomtoken123">
  ...
</form>
# Server validates the token on POST

# Defense 3: Double-submit cookie
Set-Cookie: csrf=xyz
# JS reads cookie, includes value in a custom header
X-CSRF-Token: xyz
# (custom headers can't be sent cross-site without preflight)

# Defense 4: Require custom header on state-changing requests
# (browsers block cross-site custom headers without CORS preflight)
19

WebSocket

WebSocket Upgrade

WebSocket starts as an HTTP request with the Upgrade header. If the server agrees, it returns 101 Switching Protocols. After that, the same TCP connection speaks the WebSocket binary protocol — full-duplex, low-overhead, no HTTP overhead per message. The Sec-WebSocket-Accept value (SHA-1 of the client's key plus a fixed GUID) proves the server intentionally upgraded. Origin lets the server authorize the upgrade source.

http
# Client initiates HTTP upgrade
GET /ws HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com

# Server accepts upgrade
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

# After 101, the TCP connection is "upgraded" - HTTP is done.
# Both sides send WebSocket frames (binary protocol).

# Sec-WebSocket-Accept = base64(SHA1(key + magic GUID))
# Server must compute this to prove it understood the upgrade.

WebSocket Handshake (JS Client)

The browser WebSocket API is event-driven: onopen fires after the upgrade succeeds, onmessage on each incoming frame, onclose on disconnect, onerror on failures. Send strings or binary (ArrayBuffer, Blob). Close with a code (1000 = normal) and reason. The connection is full-duplex — both sides can send at any time. WSS (WebSocket Secure) uses TLS, equivalent to HTTPS.

http
# Browser client
const ws = new WebSocket('wss://example.com/ws');

ws.onopen = () => {
  console.log('connected');
  ws.send('Hello Server');
};

ws.onmessage = (event) => {
  console.log('received:', event.data);
};

ws.onclose = (event) => {
  console.log('closed:', event.code, event.reason);
};

ws.onerror = (error) => {
  console.error('error:', error);
};

# Send binary
ws.send(new ArrayBuffer(8));

# Close
ws.close(1000, 'normal closure');

WebSocket Frames

WebSocket frames are binary: a FIN bit (message completion), opcode (text/binary/close/ping/pong), mask bit (client frames must be masked to prevent cache poisoning by intermediaries), payload length, and data. Close codes convey why the connection ended — 1000 is normal, 1006 is abnormal (no close frame, often a network drop). Ping/pong frames keep the connection alive and detect dead peers. Frames can be fragmented across multiple frames.

http
# After the handshake, communication is via binary frames:
# - FIN bit (1 = final fragment of a message)
# - Opcode (4 bits): 0x1 text, 0x2 binary, 0x8 close,
#                    0x9 ping, 0xA pong
# - Mask bit (client->server frames are masked)
# - Payload length (7, 7+16, or 7+64 bits)
# - Masking key (4 bytes, client->server only)
# - Payload data

# Close codes:
# 1000  Normal closure
# 1001  Endpoint going away
# 1006  Abnormal closure (no close frame)
# 1008  Policy violation
# 1011  Internal server error
# 4000-4999  Application-defined

# Ping/Pong: keep-alive and liveness check
# Server: ping -> Client: pong (within timeout)

# Frames can be fragmented (FIN=0) and reassembled.

WebSocket vs HTTP

WebSocket is full-duplex and bidirectional, ideal for real-time apps (chat, gaming, live dashboards, collaborative editing). HTTP is request-response — the client asks, the server answers. For one-way server-to-client updates, SSE is simpler (built on HTTP, auto-reconnects, plays nice with proxies). For stateless CRUD, HTTP/REST is right. WebSocket has the lowest per-message overhead but the highest setup cost (upgrade, framing).

http
# HTTP: request-response, half-duplex (client initiates)
# WebSocket: full-duplex, either side can send anytime

# Use HTTP when:
# - Client asks, server answers (CRUD APIs)
# - Stateless, cacheable resources
# - Occasional data fetches

# Use WebSocket when:
# - Real-time updates (chat, live sports, dashboards)
# - Bidirectional streaming (collaborative editing, gaming)
# - High-frequency, low-latency messaging
# - Server pushes events without client polling

# Alternatives:
# - SSE (Server-Sent Events): one-way server->client over HTTP
# - HTTP/2 streaming: server can push via DATA frames
# - Long polling: HTTP hack (client polls, server holds open)

# WebSocket has lower per-message overhead than HTTP.

WebSocket Use Cases

WebSocket shines for real-time bidirectional apps: chat, live dashboards, multiplayer games, collaborative editing, notifications, and WebRTC signaling. The challenge is scaling: a client connects to one specific server, so broadcasting to all clients needs a pub/sub backend (Redis, Kafka) to fan out across server instances. Use sticky sessions at the load balancer, or design stateless servers that route messages via the pub/sub layer.

http
# 1. Chat application
# Client sends: {"type": "message", "text": "hi"}
# Server broadcasts to all connected clients

# 2. Live dashboard (stock prices, sports scores)
# Server pushes updates every second
# Client just listens

# 3. Multiplayer game
# Low-latency bidirectional: position, actions, events

# 4. Collaborative editing (Google Docs style)
# Operational Transform / CRDT messages both ways

# 5. Live notifications
# Server pushes: "you have a new message"

# 6. Voice/video signaling (WebRTC)
# WS carries SDP offers/answers before media flows over UDP

# Scaling WebSockets: sticky sessions or a pub/sub backend
# (Redis Pub/Sub, Kafka) so any server can reach any client.
20

REST API Design

Resource Naming

REST resources are nouns (plural: /users not /user), with HTTP methods expressing the action. Don't put verbs in URLs (/createUser is wrong — POST /users is right). Sub-resources express relationships (/users/42/orders). Query parameters handle filtering, sorting, pagination — never bake them into the path. Use kebab-case for multi-word path segments (/password-resets, not /passwordResets). Consistency is the most important rule.

http
# Resources are nouns, pluralized
GET /api/users            # list users
POST /api/users           # create user
GET /api/users/42         # get user 42
PUT /api/users/42         # replace user 42
PATCH /api/users/42       # update user 42
DELETE /api/users/42      # delete user 42

# Sub-resources for relationships
GET /api/users/42/orders              # user 42's orders
GET /api/users/42/orders/1001         # specific order

# Use query params for filtering/sorting/pagination
GET /api/users?role=admin&sort=-created_at&page=2&limit=20

# Bad (verbs in URL):
POST /api/createUser     # should be POST /api/users
GET /api/getUser/42      # should be GET /api/users/42

# Good: nouns + HTTP methods express the action.

HTTP Methods in REST

The standard REST CRUD mapping: POST=create, GET=read, PUT=replace, PATCH=update, DELETE=delete. GET/HEAD/OPTIONS are safe (no side effects). PUT and DELETE are idempotent. POST is not (repeating creates duplicates). For actions that don't fit CRUD (lock, archive, send-email), use a sub-resource with POST: /users/42/lock. This 'RPC escape hatch' is pragmatic — pure REST doesn't model every operation cleanly.

http
# Standard CRUD mapping:
POST    /users         # Create (server assigns ID)
GET     /users         # List (with filtering/pagination)
GET     /users/:id     # Read one
PUT     /users/:id     # Replace (full update)
PATCH   /users/:id     # Update (partial)
DELETE  /users/:id     # Delete

# Safe (no side effects): GET, HEAD, OPTIONS
# Idempotent (repeatable): GET, HEAD, OPTIONS, PUT, DELETE
# Neither: POST (creates new resource each time)
# PATCH: idempotency depends on the patch format

# Custom "actions" on a resource (RPC-style escape hatch):
POST /users/42/lock      # lock the user (not pure REST)
POST /users/42/unlock
POST /users/42:lock      # Google AIP style
POST /users/42/actions/lock  # explicit escape hatch

Status Codes in REST

Use status codes semantically. 200 is generic success; 201 indicates creation (with Location); 204 means success without a body. 400 is malformed input; 422 (WebDAV) is well-formed but semantically invalid (validation). 401 vs 403 is the most confused pair: 401 = 'who are you?', 403 = 'I know who you are, but you can't do this'. 5xx means the server messed up — clients may retry with backoff. Be consistent across your API.

http
# Success (2xx)
200 OK              # generic success, returns data
201 Created         # POST/PUT created a resource (Location header)
202 Accepted        # async job queued (return job URL)
204 No Content      # success, no body (DELETE, PUT)

# Client errors (4xx)
400 Bad Request     # malformed input, validation failure
401 Unauthorized    # not authenticated (needs login)
403 Forbidden       # authenticated but lacks permission
404 Not Found       # resource doesn't exist
409 Conflict        # version conflict, duplicate
422 Unprocessable   # semantic error (well-formed but invalid)
429 Too Many Requests  # rate limited (Retry-After)

# Server errors (5xx)
500 Internal Server Error  # unexpected failure
503 Service Unavailable    # maintenance, overload

# Idempotency: 204 for DELETE/PUT is conventional.
# 201 vs 200 for POST: 201 if a resource was created.

Pagination

Page-based pagination (offset/limit) is simple but inefficient on large datasets (OFFSET scans past rows) and unstable (new inserts shift rows). Cursor-based pagination uses an opaque token pointing to the last item — stable under concurrent inserts and fast (indexed lookup on the cursor column). Use cursor for infinite scrolls and large feeds; use page-based for admin UIs needing 'jump to page N'. The Link header (RFC 5988) is a HATEOAS-style way to expose pagination URLs.

http
# Page-based (offset/limit)
GET /api/users?page=2&limit=20
# Response:
{
  "data": [...],
  "page": 2,
  "limit": 20,
  "total": 1000,
  "total_pages": 50
}

# Cursor-based (token, better for large/realtime datasets)
GET /api/users?cursor=abc123&limit=20
# Response:
{
  "data": [...],
  "next_cursor": "def456",
  "has_more": true
}

# Link header (RFC 5988):
Link: <https://api.example.com/users?page=3>; rel="next",
      <https://api.example.com/users?page=50>; rel="last"

# Pros of cursor: stable under inserts/deletes,
# faster on large datasets (no OFFSET scan).
# Cons: can't jump to arbitrary page.

API Versioning

API versioning prevents breaking changes from hurting existing clients. URI versioning (/v1/, /v2/) is the most common and explicit approach — easy to debug, route, and cache. Header versioning keeps URLs clean but is harder to test in a browser. When deprecating, use the Sunset and Deprecation headers (RFC 8594) to announce the end-of-life date, give clients ample time (6-24 months), and document the migration path. Non-breaking changes (adding optional fields) don't need a version bump.

http
# 1. URI versioning (most common, explicit)
GET /api/v1/users
GET /api/v2/users

# 2. Header versioning (cleaner URLs)
GET /api/users
Accept: application/vnd.example.v2+json

# 3. Custom header
X-API-Version: 2

# 4. Query parameter
GET /api/users?version=2

# Deprecation flow:
# - Mark old version as deprecated (Sunset header)
Sunset: Sat, 31 Dec 2025 23:59:59 GMT
Deprecation: true
# - Document migration guide
# - Maintain old version for a transition period (6-24 months)
# - Notify clients via email, response headers, logs
# - Eventually shut down the old version

# Breaking changes require a new major version.
# Non-breaking changes (adding fields) don't.

REST Best Practices

REST best practices: plural nouns, methods express actions, semantic status codes, pagination by default, filtering via query strings, consistent error format (RFC 7807 problem+json is the standard), and hypermedia links for discoverability (HATEOAS). For POST endpoints that aren't naturally idempotent (payments, order creation), accept an Idempotency-Key header so clients can safely retry without duplicating effects — the server stores the response keyed by the UUID and replays it on retries.

http
# 1. Use plural nouns: /users not /user
# 2. HTTP methods express actions (no verbs in URLs)
# 3. Status codes are semantic (201 for create, 404 for missing)
# 4. Pagination on list endpoints (default limit, max cap)
# 5. Filtering via query params: ?role=admin&active=true
# 6. Consistent error format (RFC 7807 problem+json):
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json

{
  "type": "https://example.com/errors/validation",
  "title": "Validation failed",
  "status": 422,
  "detail": "Email is required",
  "instance": "/api/users",
  "errors": [{"field": "email", "message": "required"}]
}

# 7. Hypermedia (HATEOAS) links in responses (optional):
{"data": {...}, "_links": {"self": "/users/42", "orders": "/users/42/orders"}}
# 8. Idempotency keys for safe POST retries (Stripe pattern)
Idempotency-Key: client-generated-uuid

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.