Redirects (Often Confused)
307 vs 308: both preserve method; 307 temporary, 308 permanent—use these for POST/PUT redirects
301/302 may change POST to GET (browser behavior)—don't use for API redirects with body
Include
Locationheader with absolute URL—relative may fail in older clientsRedirect loops: limit to 5-10 follows; infinite loops crash clients
Caching Combinations
Cache-Control: no-storefor sensitive data—never written to diskno-cachestill caches but revalidates every time—not "don't cache"private, max-age=0, must-revalidatefor user-specific, always-fresh contentpublic, max-age=31536000, immutablefor versioned static assetsVary: Accept-Encoding, Authorizationwhen response depends on these headers—forgetting Vary breaks caching
Conditional Requests
ETag+If-None-Match: prefer for APIs—content hash basedStrong vs weak ETags:
"abc"vsW/"abc"—weak allows semantically equivalent responsesIf-Matchfor optimistic locking: fail update if resource changed since read412 Precondition Failed when
If-Matchfails—not 409 Conflict
CORS Preflight Triggers
Custom headers (anything not Accept, Accept-Language, Content-Language, Content-Type simple values)
Content-Type other than: application/x-www-form-urlencoded, multipart/form-data, text/plain
PUT, DELETE, PATCH methods—even to same origin if other conditions met
ReadableStream body—triggers preflight
Preflight cached per
Access-Control-Max-Age—set to 86400 to reduce OPTIONS spam
Security Headers (Always Set)
Strict-Transport-Security: max-age=31536000; includeSubDomains—HSTS, once set can't easily undoX-Content-Type-Options: nosniff—prevents MIME sniffing attacksX-Frame-Options: DENYorSAMEORIGIN—prevents clickjackingContent-Security-Policy—complex but essential; start with report-only mode
Range Requests
Accept-Ranges: bytessignals support—clients can request partial contentRange: bytes=0-1023requests first 1024 bytes;bytes=-500requests last 500Return 206 Partial Content with
Content-Range: bytes 0-1023/5000416 Range Not Satisfiable if range invalid—include
Content-Range: bytes */5000
Error Response Best Practices
Structured JSON errors:
{"error": {"code": "VALIDATION_FAILED", "message": "...", "details": [...]}}Include request ID in error response—enables log correlation
Don't leak stack traces in production—log server-side, return generic message
409 Conflict for business rule violations (duplicate email, insufficient funds)—not just 400
Retry Patterns
Retry only idempotent methods by default—GET, PUT, DELETE, HEAD
POST retry needs idempotency key—
Idempotency-Key: <client-generated-uuid>Exponential backoff: 1s, 2s, 4s, 8s... with jitter—prevents thundering herd
Respect
Retry-Afterheader—can be seconds or HTTP dateSet reasonable timeout (30s typical)—don't wait forever
Headers Often Forgotten
Vary: must include headers that affect response—CORS withoutVary: OriginbreaksContent-Disposition: attachment; filename="report.pdf"for downloadsX-Request-ID: generate if not present, propagate to downstream servicesAccept-Languagefor localized responses—respect with graceful fallback
Connection Behavior
HTTP/1.1 without
Content-Lengthor chunked = connection close after responseTransfer-Encoding: chunkedfor streaming—can't set Content-LengthHTTP/2 is binary, multiplexed—no head-of-line blocking at HTTP level
WebSocket upgrade: GET with
Connection: Upgrade,Upgrade: websocket