- Authors
- Name
- Introduction
- 1. HTTP Status Codes - A Debugging Perspective
- 2. TLS/SSL Handshake Troubleshooting
- 3. Certificate Chain Validation Issues
- 4. curl Debugging Techniques
- 5. Troubleshooting 502/503/504 Errors
- 6. CORS Troubleshooting
- 7. Debugging Redirect Loops
- 8. HTTP/2 and HTTP/3 Debugging
- 9. Load Balancer and Reverse Proxy Troubleshooting
- 10. Real-World Debugging Scenarios
- Conclusion
Introduction
When HTTP/HTTPS issues arise in production, a vague "it's not working" report is never enough to pinpoint the root cause. Problems can originate from multiple layers: the network layer, TLS handshake, application logic, load balancer configuration, and more.
This guide covers systematic approaches to diagnosing and resolving the most common HTTP/HTTPS problems you will encounter in production environments.
1. HTTP Status Codes - A Debugging Perspective
1xx (Informational)
Often overlooked but important status codes in practice:
100 Continue → Server acknowledges the client may send a large request body
101 Switching → Must verify during WebSocket upgrade negotiations
103 Early Hints → Used for browser preload optimization
2xx (Success)
200 OK → The baseline. But an empty body deserves suspicion
201 Created → Always check the Location header after POST
204 No Content → Appropriate for DELETE responses. A body here is a bug
206 Partial → Range requests. Essential for large file download debugging
3xx (Redirection) - Full of Pitfalls
301 Moved Permanently → Browsers cache this aggressively. Misconfigurations are hard to undo
302 Found → Temporary redirect. Watch for SEO confusion with 301
303 See Other → Redirect POST to GET (Post/Redirect/Get pattern)
307 Temporary Redirect → Preserves the HTTP method
308 Permanent Redirect → Permanent redirect that preserves the HTTP method
4xx (Client Errors) - Core Debugging Territory
# Debugging 400 Bad Request
curl -v -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d '{"name": "test"' # Malformed JSON - missing closing brace
# Understanding 401 vs 403
# 401: Authentication failed (no token / token expired)
# 403: Authenticated but not authorized (insufficient permissions)
# 405 Method Not Allowed
curl -v -X PATCH https://api.example.com/users/1
# Check the Allow header for permitted methods
# 429 Too Many Requests
# Inspect Retry-After and X-RateLimit-* headers
curl -v https://api.example.com/data 2>&1 | grep -i "rate\|retry"
5xx (Server Errors) - The Highest Urgency
500 Internal Server Error → Check server-side logs first
502 Bad Gateway → Upstream server returned an invalid response
503 Service Unavailable → Server overloaded or in maintenance mode
504 Gateway Timeout → Upstream server response timed out
2. TLS/SSL Handshake Troubleshooting
Understanding the TLS Handshake Flow
Client Server
| |
|--- ClientHello (ciphers, SNI) ----->|
| |
|<-- ServerHello (chosen cipher) -----|
|<-- Certificate (cert chain) --------|
|<-- ServerKeyExchange ---------------|
|<-- ServerHelloDone -----------------|
| |
|--- ClientKeyExchange -------------->|
|--- ChangeCipherSpec --------------->|
|--- Finished ----------------------->|
| |
|<-- ChangeCipherSpec ----------------|
|<-- Finished ------------------------|
| |
|====== Encrypted communication ======|
Debugging with openssl
# Basic connection test
openssl s_client -connect example.com:443 -servername example.com
# Test specific TLS versions
openssl s_client -connect example.com:443 -tls1_2
openssl s_client -connect example.com:443 -tls1_3
# Dump the full certificate chain
openssl s_client -connect example.com:443 -showcerts 2>/dev/null | \
openssl x509 -noout -subject -issuer -dates
# Debug SNI (Server Name Indication) issues
openssl s_client -connect 1.2.3.4:443 -servername mysite.com
# Test a specific cipher suite
openssl s_client -connect example.com:443 -cipher ECDHE-RSA-AES256-GCM-SHA384
Common TLS Errors and Solutions
# Error: SSL_ERROR_HANDSHAKE_FAILURE
# Cause: No matching cipher suites between client and server
# Resolution: Check the server's supported cipher suites
nmap --script ssl-enum-ciphers -p 443 example.com
# Error: CERTIFICATE_VERIFY_FAILED
# Cause: Incomplete certificate chain or untrusted root CA
# Diagnosis:
openssl s_client -connect example.com:443 2>&1 | grep "verify"
# Error: hostname mismatch
# Cause: Certificate CN/SAN does not match the requested domain
openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -noout -text | grep -A1 "Subject Alternative Name"
3. Certificate Chain Validation Issues
Certificate Chain Structure
Root CA (self-signed, embedded in OS/browser trust stores)
└── Intermediate CA (signed by Root CA)
└── Server Certificate (signed by Intermediate CA)
Chain Verification Script
#!/bin/bash
# Certificate chain verification script
DOMAIN=$1
echo "=== Certificate Chain Verification: $DOMAIN ==="
# Retrieve the certificate chain from the server
echo | openssl s_client -connect ${DOMAIN}:443 -servername ${DOMAIN} \
-showcerts 2>/dev/null | \
awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/{ print }' > /tmp/chain.pem
# Split and display each certificate
csplit -f /tmp/cert- -b '%02d.pem' /tmp/chain.pem \
'/BEGIN CERTIFICATE/' '{*}' 2>/dev/null
for cert in /tmp/cert-*.pem; do
[ -s "$cert" ] || continue
echo "--- Certificate: $cert ---"
openssl x509 -in "$cert" -noout \
-subject -issuer -dates -fingerprint 2>/dev/null
echo ""
done
# Verify the chain
openssl verify -verbose -CAfile /etc/ssl/certs/ca-certificates.crt \
/tmp/chain.pem 2>&1
Common Certificate Problems
# 1. Check certificate expiration
echo | openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -noout -dates
# 2. Detect missing intermediate certificates
# The most reliable method is SSL Labs:
# https://www.ssllabs.com/ssltest/
# 3. Using curl with self-signed certificates
curl --cacert /path/to/ca.crt https://internal.example.com
# 4. Verify certificate renewal was applied
echo | openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -noout -serial -fingerprint
4. curl Debugging Techniques
Basic Debugging Options
# -v (verbose): The most fundamental debugging flag
curl -v https://api.example.com/health
# -vv: Even more detail
curl -vv https://api.example.com/health
# --trace: Full byte-level communication dump
curl --trace /tmp/curl-trace.log https://api.example.com/health
# --trace-ascii: Readable ASCII dump
curl --trace-ascii /tmp/curl-trace.txt https://api.example.com/health
# --trace-time: Include timestamps
curl --trace-time --trace-ascii - https://api.example.com/health
Advanced Debugging Techniques
# DNS override (--resolve): Send requests to a specific IP
# Useful for testing a new server before DNS cutover
curl --resolve api.example.com:443:10.0.1.50 \
https://api.example.com/health
# Measure connection timing breakdown
curl -o /dev/null -s -w "\
DNS Lookup: %{time_namelookup}s\n\
TCP Connect: %{time_connect}s\n\
TLS Handshake: %{time_appconnect}s\n\
Start Transfer: %{time_starttransfer}s\n\
Total Time: %{time_total}s\n\
HTTP Code: %{http_code}\n\
Download Size: %{size_download} bytes\n" \
https://api.example.com/health
# Trace redirect chains
curl -v -L --max-redirs 10 https://example.com 2>&1 | grep "< Location"
# Force specific HTTP version
curl --http1.1 https://api.example.com/health
curl --http2 https://api.example.com/health
curl --http3 https://api.example.com/health
# Debug through a proxy
curl -v --proxy http://proxy.internal:8080 https://api.example.com/health
# Client certificate authentication (mTLS)
curl --cert /path/to/client.crt --key /path/to/client.key \
https://mtls.example.com/api
Practical curl One-Liners
# Monitor response times with repeated requests
for i in $(seq 1 10); do
echo -n "Request $i: "
curl -o /dev/null -s -w "%{http_code} %{time_total}s" \
https://api.example.com/health
echo ""
sleep 1
done
# Quick header inspection
curl -I https://api.example.com/health
# Debug POST requests with filtered output
curl -v -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"name": "test", "email": "test@example.com"}' \
2>&1 | grep -E "^[<>*]"
5. Troubleshooting 502/503/504 Errors
502 Bad Gateway
# Cause: Upstream server returned an invalid response
# 1. Test the upstream server directly
curl -v http://upstream-server:8080/health
# 2. Check Nginx error logs
tail -f /var/log/nginx/error.log | grep "502\|upstream"
# 3. Verify upstream server is listening
ss -tlnp | grep 8080
netstat -tlnp | grep 8080
# 4. Inspect Nginx upstream configuration
nginx -T | grep -A5 "upstream"
Nginx configuration adjustments to resolve 502 errors:
upstream backend {
server 10.0.1.10:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.11:8080 max_fails=3 fail_timeout=30s;
keepalive 32;
}
server {
location /api/ {
proxy_pass http://backend;
proxy_next_upstream error timeout http_502 http_503;
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
# Adjust buffer sizes for large response headers
proxy_buffer_size 16k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
}
}
503 Service Unavailable
# Root cause analysis
# 1. Check server load
top -bn1 | head -5
free -m
df -h
# 2. Check active connections
ss -s
ss -tn state established | wc -l
# 3. Check process status
systemctl status nginx
systemctl status your-app
# 4. Check resource limits
ulimit -n # File descriptor limit
cat /proc/sys/net/core/somaxconn # Socket backlog
504 Gateway Timeout
# Cause: Upstream server timed out
# 1. Diagnose timeout at each stage
curl -o /dev/null -s -w "connect: %{time_connect}s\nttfb: %{time_starttransfer}s\ntotal: %{time_total}s\n" \
https://api.example.com/slow-endpoint
# 2. Check for slow queries (if the root cause is the database)
# MySQL
SHOW PROCESSLIST;
# PostgreSQL
SELECT pid, now() - pg_stat_activity.query_start AS duration, query
FROM pg_stat_activity
WHERE state = 'active' AND now() - query_start > interval '5 seconds';
# 3. Adjust timeout settings
# Nginx: proxy_read_timeout 120s;
# HAProxy: timeout server 120s
6. CORS Troubleshooting
How CORS Works
Browser Server
| |
|--- Preflight (OPTIONS) --------->|
| Origin: https://app.com |
| Access-Control-Request-Method: POST
| |
|<-- 200 OK ----------------------|
| Access-Control-Allow-Origin: https://app.com
| Access-Control-Allow-Methods: POST, GET
| Access-Control-Allow-Headers: Content-Type
| Access-Control-Max-Age: 86400
| |
|--- Actual Request (POST) ------->|
| Origin: https://app.com |
| |
|<-- 200 OK ----------------------|
| Access-Control-Allow-Origin: https://app.com
Debugging CORS
# Simulate a preflight request
curl -v -X OPTIONS https://api.example.com/data \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, Authorization"
# Inspect CORS headers in the response
curl -v https://api.example.com/data \
-H "Origin: https://app.example.com" \
2>&1 | grep -i "access-control"
CORS Configuration in Nginx
location /api/ {
# Handle preflight requests
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Length' 0;
return 204;
}
# Add CORS headers to actual responses
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
proxy_pass http://backend;
}
7. Debugging Redirect Loops
# Trace the redirect chain
curl -v -L --max-redirs 20 https://example.com 2>&1 | \
grep -E "^< (HTTP|Location)" | head -30
# Example output (infinite loop):
# < HTTP/1.1 301 Moved Permanently
# < Location: https://www.example.com/
# < HTTP/1.1 301 Moved Permanently
# < Location: https://example.com/
# < HTTP/1.1 301 Moved Permanently
# < Location: https://www.example.com/ ← Loop detected!
# Common causes:
# 1. HTTP→HTTPS redirect conflicting with HTTPS→HTTP redirect
# 2. www ↔ non-www redirect conflict
# 3. Load balancer SSL termination + application HTTPS enforcement
# Solution: Use the X-Forwarded-Proto header
# Pass protocol information from Nginx to the backend
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
8. HTTP/2 and HTTP/3 Debugging
HTTP/2 Debugging
# Check HTTP/2 support
curl -v --http2 https://example.com 2>&1 | grep "ALPN"
# Frame-level HTTP/2 debugging (using nghttp2)
nghttp -v https://example.com
# Verify HTTP/2 multiplexing
nghttp -v -m 10 https://example.com/style.css \
https://example.com/script.js \
https://example.com/image.png
# Check HTTP/2 server push
nghttp -v --stat https://example.com 2>&1 | grep "push"
# Test h2c (HTTP/2 Cleartext)
curl -v --http2-prior-knowledge http://localhost:8080/health
HTTP/3 (QUIC) Debugging
# Check HTTP/3 support
curl --http3 -v https://example.com 2>&1 | head -20
# Inspect Alt-Svc header (HTTP/3 advertisement)
curl -sI https://example.com | grep -i "alt-svc"
# Example: alt-svc: h3=":443"; ma=86400
# QUIC connection debugging (with quiche tools)
# Test 0-RTT connection
curl --http3 -v --connect-to example.com:443:server-ip:443 \
https://example.com
Common HTTP/2 and HTTP/3 Issues
# 1. HTTP/2 Head-of-Line (HOL) blocking
# HTTP/2 can suffer HOL blocking at the TCP layer
# Resolution: Evaluate migration to HTTP/3 (QUIC)
# 2. HTTP/2 GOAWAY frame debugging
nghttp -v https://example.com 2>&1 | grep "GOAWAY"
# 3. HPACK header compression issues
# Check dynamic table size for requests with large headers
nghttp -v --header-table-size=65536 https://example.com
# 4. Stream concurrency limits
# Check SETTINGS_MAX_CONCURRENT_STREAMS
nghttp -v https://example.com 2>&1 | grep "MAX_CONCURRENT"
9. Load Balancer and Reverse Proxy Troubleshooting
Health Check Failure Debugging
# 1. Test the health endpoint directly
curl -v http://backend-server:8080/health
# 2. Test under the same conditions the load balancer uses
curl -v -H "Host: api.example.com" \
--resolve api.example.com:80:10.0.1.10 \
http://api.example.com/health
# 3. Check HAProxy status
echo "show stat" | socat stdio /var/run/haproxy/admin.sock | \
awk -F',' '{print $1, $2, $18, $19}'
# 4. Nginx upstream status (with nginx-module-vts)
curl http://localhost/status/format/json | jq '.upstreamZones'
Sticky Session Issues
# Inspect cookie-based session affinity
curl -v -c cookies.txt https://app.example.com/login
curl -v -b cookies.txt https://app.example.com/dashboard
# Verify routing consistency to the same backend
for i in $(seq 1 5); do
curl -s -b cookies.txt https://app.example.com/api/server-id
echo ""
done
X-Forwarded-* Header Debugging
# Inspect proxy chain headers
curl -v https://api.example.com/debug-headers 2>&1
# Expected headers:
# X-Forwarded-For: client-ip, proxy1-ip, proxy2-ip
# X-Forwarded-Proto: https
# X-Forwarded-Host: api.example.com
# X-Real-IP: client-ip
# Test whether the proxy correctly forwards the original IP
curl -H "X-Forwarded-For: 1.2.3.4" https://api.example.com/my-ip
10. Real-World Debugging Scenarios
Scenario 1: Intermittent 502 Errors
#!/bin/bash
# Intermittent 502 monitoring script
URL="https://api.example.com/health"
LOG_FILE="/tmp/502-monitor.log"
echo "=== 502 Monitoring Started: $(date) ===" >> $LOG_FILE
while true; do
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}|%{time_total}|%{time_connect}|%{time_starttransfer}" $URL)
HTTP_CODE=$(echo $RESPONSE | cut -d'|' -f1)
TOTAL_TIME=$(echo $RESPONSE | cut -d'|' -f2)
CONNECT_TIME=$(echo $RESPONSE | cut -d'|' -f3)
TTFB=$(echo $RESPONSE | cut -d'|' -f4)
if [ "$HTTP_CODE" != "200" ]; then
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$TIMESTAMP] HTTP $HTTP_CODE | Total: ${TOTAL_TIME}s | Connect: ${CONNECT_TIME}s | TTFB: ${TTFB}s" >> $LOG_FILE
# Collect detailed information on 502 occurrence
if [ "$HTTP_CODE" = "502" ]; then
echo " Detailed trace:" >> $LOG_FILE
curl -v $URL 2>> $LOG_FILE
echo "---" >> $LOG_FILE
fi
fi
sleep 5
done
Scenario 2: TLS Certificate Expiry Monitoring
#!/bin/bash
# Certificate expiry monitoring script
DOMAINS=(
"api.example.com"
"app.example.com"
"admin.example.com"
)
WARNING_DAYS=30
for domain in "${DOMAINS[@]}"; do
EXPIRY=$(echo | openssl s_client -connect ${domain}:443 -servername ${domain} 2>/dev/null | \
openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -z "$EXPIRY" ]; then
echo "[ERROR] $domain: Connection failed"
continue
fi
EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s 2>/dev/null || date -j -f "%b %d %T %Y %Z" "$EXPIRY" +%s 2>/dev/null)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
if [ $DAYS_LEFT -lt 0 ]; then
echo "[CRITICAL] $domain: Certificate EXPIRED! ($EXPIRY)"
elif [ $DAYS_LEFT -lt $WARNING_DAYS ]; then
echo "[WARNING] $domain: Expires in ${DAYS_LEFT} days ($EXPIRY)"
else
echo "[OK] $domain: ${DAYS_LEFT} days remaining ($EXPIRY)"
fi
done
Scenario 3: Comprehensive HTTP Diagnostics
#!/bin/bash
# Comprehensive HTTP diagnostic script
URL=${1:-"https://example.com"}
echo "============================================"
echo "HTTP Comprehensive Diagnostics: $URL"
echo "Timestamp: $(date)"
echo "============================================"
# 1. DNS resolution
echo -e "\n[1] DNS Resolution"
DOMAIN=$(echo $URL | awk -F[/:] '{print $4}')
dig +short $DOMAIN A
dig +short $DOMAIN AAAA
# 2. TCP connectivity
echo -e "\n[2] TCP Connection Check"
nc -zv $DOMAIN 443 2>&1
# 3. TLS information
echo -e "\n[3] TLS Information"
echo | openssl s_client -connect ${DOMAIN}:443 -servername ${DOMAIN} 2>/dev/null | \
grep -E "Protocol|Cipher|Verify"
# 4. Certificate details
echo -e "\n[4] Certificate Details"
echo | openssl s_client -connect ${DOMAIN}:443 -servername ${DOMAIN} 2>/dev/null | \
openssl x509 -noout -subject -issuer -dates 2>/dev/null
# 5. HTTP response timing
echo -e "\n[5] HTTP Response"
curl -s -o /dev/null -w "\
HTTP Code: %{http_code}\n\
DNS Lookup: %{time_namelookup}s\n\
TCP Connect: %{time_connect}s\n\
TLS Handshake: %{time_appconnect}s\n\
TTFB: %{time_starttransfer}s\n\
Total Time: %{time_total}s\n\
Download Size: %{size_download} bytes\n\
HTTP Version: %{http_version}\n\
Remote IP: %{remote_ip}\n" $URL
# 6. Response headers
echo -e "\n[6] Key Response Headers"
curl -sI $URL | grep -iE "^(server|x-|content-type|cache-control|strict|location|set-cookie)"
# 7. Security headers audit
echo -e "\n[7] Security Headers Audit"
HEADERS=$(curl -sI $URL)
for header in "Strict-Transport-Security" "X-Content-Type-Options" "X-Frame-Options" "Content-Security-Policy" "X-XSS-Protection"; do
if echo "$HEADERS" | grep -qi "$header"; then
echo " [PASS] $header"
else
echo " [FAIL] $header (missing)"
fi
done
echo -e "\n============================================"
echo "Diagnostics Complete"
echo "============================================"
Conclusion
The key to HTTP/HTTPS troubleshooting is a layered approach. By systematically narrowing down through DNS, TCP connection, TLS handshake, HTTP protocol, and application logic -- in that order -- you can efficiently resolve the majority of production issues.
Make it a habit to use the tools and techniques covered here for systematic diagnosis of production problems. In particular, curl -w format strings and openssl s_client are indispensable tools that every engineer should master.
Key Takeaways:
- Status codes are clues, not answers -- always inspect the response body and headers together
- For TLS issues, start with
openssl s_client - For timing problems, use
curl -wto measure each phase individually - For intermittent issues, deploy automated monitoring scripts to capture them
- For proxy-related problems, test at each hop to isolate the bottleneck