Skip to content

HTTP Protocol — Street-Level Ops

Quick Diagnosis Commands

# Get response headers only (fastest health check)
curl -sI https://api.example.com/health

# Full verbose request/response with TLS handshake details
curl -v https://api.example.com/health 2>&1

# Timing breakdown: DNS, connect, TLS, TTFB, total
curl -o /dev/null -s -w \
  "dns:     %{time_namelookup}s\n\
connect:  %{time_connect}s\n\
tls:      %{time_appconnect}s\n\
ttfb:     %{time_starttransfer}s\n\
total:    %{time_total}s\n\
http:     %{http_code}\n\
size:     %{size_download} bytes\n" \
  https://api.example.com/health

# Follow redirects and show each hop
curl -L -v https://example.com 2>&1 | grep -E '^[<>*]|^< HTTP|^< Location'

# Send a JSON POST request
curl -X POST https://api.example.com/v2/deploy \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"image": "myapp:v2.1.0", "replicas": 3}'

# Check certificate expiration
echo | openssl s_client -connect api.example.com:443 \
  -servername api.example.com 2>/dev/null \
  | openssl x509 -noout -enddate -subject
# notAfter=Mar 18 00:00:00 2027 GMT

# Check certificate chain (find missing intermediates)
openssl s_client -connect api.example.com:443 \
  -servername api.example.com -showcerts </dev/null 2>/dev/null \
  | grep -E 'Certificate chain|s:|i:'

# Test a specific HTTP method
curl -X OPTIONS -v https://api.example.com/v2/deploy 2>&1 \
  | grep -E 'Access-Control|Allow'

# Check response compression
curl -sI -H "Accept-Encoding: gzip" https://api.example.com/data \
  | grep -i content-encoding
# content-encoding: gzip  ← compression active

# Test with a specific Host header (virtual host routing)
curl -s -H "Host: app2.example.com" http://10.0.1.50/health

Gotcha: 502 Bad Gateway — Backend Not Reachable

Symptom: Users get 502 errors. The application logs show nothing because the request never reached the app.

Rule: 502 means the proxy/load balancer tried to connect to the upstream backend and failed. The problem is between the proxy and the backend, not between the client and the proxy.

Remember: 502 = "I tried to reach the backend and it rejected me." 504 = "I tried to reach the backend and it was too slow." 503 = "I know the backend can't handle this right now." These three error codes tell you exactly where in the request lifecycle the failure occurred.

# Step 1: Confirm the backend is listening
curl -s http://backend-host:8000/health
# If connection refused → backend is down

# Step 2: Check from the proxy server itself
ssh proxy-server
curl -s http://localhost:8000/health    # if backend is local
curl -s http://backend-ip:8000/health   # if backend is remote

# Step 3: Check nginx upstream config
grep -A5 'upstream' /etc/nginx/conf.d/*.conf
# Verify IP, port, and health check settings

# Step 4: Check for connection reuse issues (keepalive mismatch)
# If the backend closes idle connections before nginx expects:
# nginx.conf upstream block:
#   keepalive_timeout 60s;
# Backend (e.g., gunicorn):
#   --keep-alive 65  # must be LONGER than proxy timeout

# Step 5: Check backend process limits
ss -tlnp | grep 8000
# Is the socket listening? Is the process running?

Gotcha: 504 Gateway Timeout — Backend Too Slow

Symptom: Slow requests return 504 while fast requests work fine.

# Step 1: Measure actual backend response time
curl -o /dev/null -s -w "ttfb: %{time_starttransfer}s\ntotal: %{time_total}s\n" \
  http://backend:8000/slow-endpoint

# Step 2: Check proxy timeout settings
# Nginx:
grep -E 'proxy_read_timeout|proxy_connect_timeout|proxy_send_timeout' \
  /etc/nginx/conf.d/*.conf
# Default proxy_read_timeout is 60s

> **Default trap:** Nginx's default `proxy_read_timeout` is 60 seconds, but AWS ALB's default idle timeout is also 60 seconds. If your backend takes exactly 60s, you get a race condition between nginx and ALB timing out. Set ALB idle timeout higher than nginx timeout, or set nginx timeout lower than ALB  never equal.

# Step 3: Increase timeout for specific slow endpoints
# In nginx location block:
# location /api/reports {
#     proxy_read_timeout 300s;  # 5 minutes for report generation
# }

# Step 4: Verify ALB/ELB idle timeout (AWS)
aws elbv2 describe-target-group-attributes \
  --target-group-arn $TG_ARN \
  | jq '.Attributes[] | select(.Key == "deregistration_delay.timeout_seconds")'

Pattern: Debugging CORS Errors

CORS errors appear in the browser console but not in server logs. The server is working correctly — the browser is blocking the response.

# Step 1: Reproduce the preflight request
curl -v -X OPTIONS https://api.example.com/data \
  -H "Origin: https://dashboard.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type, Authorization" \
  2>&1 | grep -E 'Access-Control|HTTP/'

# Expected response headers for working CORS:
# Access-Control-Allow-Origin: https://dashboard.example.com
# Access-Control-Allow-Methods: GET, POST, OPTIONS
# Access-Control-Allow-Headers: Content-Type, Authorization
# Access-Control-Max-Age: 86400

# Step 2: Check if the server returns CORS headers on actual requests
curl -sI -H "Origin: https://dashboard.example.com" \
  https://api.example.com/data \
  | grep -i access-control

# Common fixes in nginx:
# add_header Access-Control-Allow-Origin "https://dashboard.example.com";
# add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
# add_header Access-Control-Allow-Headers "Content-Type, Authorization";
# if ($request_method = OPTIONS) { return 204; }

Pattern: Certificate Expiry Monitoring

# Check cert expiry for a list of domains
for domain in api.example.com app.example.com cdn.example.com; do
  expiry=$(echo | openssl s_client -connect ${domain}:443 \
    -servername $domain 2>/dev/null \
    | openssl x509 -noout -enddate 2>/dev/null \
    | cut -d= -f2)
  days_left=$(( ($(date -d "$expiry" +%s) - $(date +%s)) / 86400 ))
  echo "$domain: $days_left days ($expiry)"
done

# Check for incomplete certificate chain
openssl s_client -connect api.example.com:443 \
  -servername api.example.com </dev/null 2>&1 \
  | grep "Verify return code"
# 0 = OK
# 21 = unable to verify the first certificate (missing intermediate)

> **Gotcha:** "Verify return code: 21" means your server is missing an intermediate certificate. The fix is to concatenate the intermediate cert(s) with your server cert in the right order: server cert first, then intermediate(s), root last (or omitted). Most browsers cache intermediates and work anyway, but curl and API clients fail  so you discover the problem in production, not testing.

Pattern: HTTP Request Debugging Playbook

# Full request lifecycle analysis
# 1. Check DNS resolution
dig +short api.example.com

# 2. Check TCP connectivity
nc -zv api.example.com 443 2>&1

# 3. Check TLS handshake
openssl s_client -connect api.example.com:443 \
  -servername api.example.com </dev/null 2>&1 \
  | head -20

# 4. Check HTTP response
curl -sv https://api.example.com/health 2>&1

# 5. Check from different locations (is it network-specific?)
# Use a jump host or different network:
ssh jump-host "curl -sI https://api.example.com/health"

Pattern: Rate Limit Detection and Handling

# Check rate limit headers
curl -sI https://api.example.com/data | grep -i 'rate\|limit\|retry'
# Common headers:
# X-RateLimit-Limit: 100
# X-RateLimit-Remaining: 0
# X-RateLimit-Reset: 1710754800
# Retry-After: 60

# Decode the reset timestamp
python3 -c "import datetime; print(datetime.datetime.fromtimestamp(1710754800))"

# Retry loop with exponential backoff
for i in 1 2 4 8 16; do
  status=$(curl -s -o /dev/null -w "%{http_code}" https://api.example.com/data)
  if [ "$status" != "429" ]; then
    echo "Success: HTTP $status"
    break
  fi
  echo "Rate limited (429), retrying in ${i}s..."
  sleep $i
done

Pattern: Debugging Cache Issues

# Check cache headers on a response
curl -sI https://cdn.example.com/static/app.js | grep -iE 'cache|etag|age|expires'
# Cache-Control: public, max-age=31536000, immutable
# ETag: "abc123"
# Age: 3600  ← served from cache, cached 1 hour ago

# Force cache bypass
curl -sI -H "Cache-Control: no-cache" https://cdn.example.com/static/app.js
# Or use a cache-busting query string:
curl -sI "https://cdn.example.com/static/app.js?v=$(date +%s)"

# Check if CDN is serving stale content
curl -sI https://cdn.example.com/api/config | grep -E 'X-Cache|Age|Date'
# X-Cache: HIT  ← CDN served from cache
# X-Cache: MISS ← CDN fetched fresh from origin

Useful One-Liners

# Test all HTTP methods against an endpoint
for method in GET HEAD POST PUT PATCH DELETE OPTIONS; do
  status=$(curl -s -o /dev/null -w "%{http_code}" -X $method https://api.example.com/test)
  echo "$method$status"
done

# Extract and decode a JWT from a response
curl -s https://api.example.com/auth/token -d 'user=admin&pass=secret' \
  | jq -r '.token' \
  | cut -d. -f2 \
  | base64 -d 2>/dev/null \
  | jq .

# Check HTTP/2 support
curl -sI --http2 https://api.example.com/ 2>&1 | head -1
# HTTP/2 200 ← supports HTTP/2

> **Under the hood:** HTTP/2 multiplexes multiple requests over a single TCP connection. This eliminates head-of-line blocking at the HTTP layer but not at the TCP layer  a single lost packet still stalls all streams. HTTP/3 (QUIC) fixes this by using UDP with per-stream loss recovery.

# Measure TTFB for multiple requests (load test lite)
for i in $(seq 1 10); do
  curl -o /dev/null -s -w "%{time_starttransfer}\n" https://api.example.com/health
done | awk '{sum+=$1; n++} END {printf "avg TTFB: %.3fs (n=%d)\n", sum/n, n}'

# Check if server supports gzip compression
curl -sI -H "Accept-Encoding: gzip, deflate, br" https://api.example.com/data \
  | grep -i content-encoding

# View redirect chain
curl -sIL https://example.com 2>&1 | grep -E 'HTTP/|Location:'