Skip to content

Split View: HTTP/HTTPS 트러블슈팅 완벽 가이드 - 실무에서 바로 쓰는 디버깅 기법

|

HTTP/HTTPS 트러블슈팅 완벽 가이드 - 실무에서 바로 쓰는 디버깅 기법

들어가며

운영 환경에서 HTTP/HTTPS 관련 문제가 발생하면, 단순히 "안 돼요"라는 보고만으로는 원인을 파악하기 어렵다. 네트워크 레이어, TLS 핸드셰이크, 애플리케이션 로직, 로드밸런서 설정 등 여러 계층에서 문제가 발생할 수 있기 때문이다.

이 글에서는 실무에서 자주 만나는 HTTP/HTTPS 문제를 체계적으로 진단하고 해결하는 방법을 다룬다.

1. HTTP 상태 코드 - 디버깅 관점에서의 재해석

1xx (정보 응답)

실무에서 자주 간과되지만 중요한 상태 코드가 있다.

100 Continue    → 클라이언트가 큰 요청 본문을 보내기 전, 서버가 수락 의사를 표시
101 SwitchingWebSocket 업그레이드 시 반드시 확인해야 하는 코드
103 Early Hints → 브라우저 preload 최적화에 활용

2xx (성공)

200 OK          → 가장 기본. 하지만 본문이 비어 있으면 의심해야 한다
201 CreatedPOSTLocation 헤더 확인 필수
204 No ContentDELETE 응답으로 적합. 본문이 있으면 버그
206 PartialRange 요청 시. 대용량 파일 다운로드 디버깅에 핵심

3xx (리다이렉션) - 함정이 많은 영역

301 Moved Permanently  → 브라우저가 캐시함. 잘못 설정하면 복구가 어렵다
302 Found              → 임시 리다이렉션. SEO 관점에서 301과 혼동 주의
303 See OtherPOSTGET으로 리다이렉트할 때 사용
307 Temporary Redirect → 메서드를 유지하는 리다이렉트
308 Permanent Redirect → 메서드를 유지하는 영구 리다이렉트

4xx (클라이언트 에러) - 디버깅 핵심

# 400 Bad Request 디버깅
curl -v -X POST https://api.example.com/users \
  -H "Content-Type: application/json" \
  -d '{"name": "test"' # 잘못된 JSON - 닫는 괄호 누락

# 401 vs 403 차이 이해
# 401: 인증 자체가 안 됨 (토큰 없음/만료)
# 403: 인증은 됐으나 권한이 없음

# 405 Method Not Allowed
curl -v -X PATCH https://api.example.com/users/1
# Allow 헤더에서 허용된 메서드 확인

# 429 Too Many Requests
# Retry-After 헤더와 X-RateLimit-* 헤더 확인
curl -v https://api.example.com/data 2>&1 | grep -i "rate\|retry"

5xx (서버 에러) - 가장 긴급한 상황

500 Internal Server Error → 서버 로그 확인이 최우선
502 Bad Gateway           → 업스트림 서버 응답 불가 (프록시 뒤 서버 문제)
503 Service Unavailable   → 서버 과부하 또는 점검 중
504 Gateway Timeout       → 업스트림 서버 응답 시간 초과

2. TLS/SSL 핸드셰이크 트러블슈팅

TLS 핸드셰이크 과정 이해

Client                                Server
  |                                     |
  |--- ClientHello (지원 암호화, SNI) --->|
  |                                     |
  |<-- ServerHello (선택된 암호화) ------|
  |<-- Certificate (인증서 체인) --------|
  |<-- ServerKeyExchange ---------------|
  |<-- ServerHelloDone -----------------|
  |                                     |
  |--- ClientKeyExchange -------------->|
  |--- ChangeCipherSpec --------------->|
  |--- Finished ----------------------->|
  |                                     |
  |<-- ChangeCipherSpec ----------------|
  |<-- Finished ------------------------|
  |                                     |
  |====== 암호화된 통신 시작 ============|

openssl을 이용한 핸드셰이크 디버깅

# 기본 연결 테스트
openssl s_client -connect example.com:443 -servername example.com

# TLS 버전 명시 테스트
openssl s_client -connect example.com:443 -tls1_2
openssl s_client -connect example.com:443 -tls1_3

# 인증서 체인 전체 출력
openssl s_client -connect example.com:443 -showcerts 2>/dev/null | \
  openssl x509 -noout -subject -issuer -dates

# SNI(Server Name Indication) 문제 디버깅
openssl s_client -connect 1.2.3.4:443 -servername mysite.com

# 특정 암호화 스위트 테스트
openssl s_client -connect example.com:443 -cipher ECDHE-RSA-AES256-GCM-SHA384

자주 발생하는 TLS 에러와 해결법

# 에러: SSL_ERROR_HANDSHAKE_FAILURE
# 원인: 클라이언트와 서버 간 지원하는 암호화 스위트가 없음
# 해결: 서버의 cipher suite 확인
nmap --script ssl-enum-ciphers -p 443 example.com

# 에러: CERTIFICATE_VERIFY_FAILED
# 원인: 인증서 체인 불완전 또는 루트 CA 미신뢰
# 진단:
openssl s_client -connect example.com:443 2>&1 | grep "verify"

# 에러: hostname mismatch
# 원인: 인증서의 CN/SAN이 요청 도메인과 불일치
openssl s_client -connect example.com:443 2>/dev/null | \
  openssl x509 -noout -text | grep -A1 "Subject Alternative Name"

3. 인증서 체인 검증 문제

인증서 체인 구조

Root CA (자체 서명, OS/브라우저에 내장)
  └── Intermediate CA (Root CA가 서명)
        └── Server Certificate (Intermediate CA가 서명)

체인 검증 스크립트

#!/bin/bash
# 인증서 체인 검증 스크립트
DOMAIN=$1

echo "=== 인증서 체인 검증: $DOMAIN ==="

# 서버에서 인증서 체인 가져오기
echo | openssl s_client -connect ${DOMAIN}:443 -servername ${DOMAIN} \
  -showcerts 2>/dev/null | \
  awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/{ print }' > /tmp/chain.pem

# 각 인증서 정보 출력
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

# 체인 검증
openssl verify -verbose -CAfile /etc/ssl/certs/ca-certificates.crt \
  /tmp/chain.pem 2>&1

일반적인 인증서 문제

# 1. 인증서 만료 확인
echo | openssl s_client -connect example.com:443 2>/dev/null | \
  openssl x509 -noout -dates

# 2. 중간 인증서 누락 확인
# SSL Labs 테스트 (가장 확실한 방법)
# https://www.ssllabs.com/ssltest/

# 3. 자체 서명 인증서 사용 시 curl
curl --cacert /path/to/ca.crt https://internal.example.com

# 4. 인증서 갱신 후 적용 확인
echo | openssl s_client -connect example.com:443 2>/dev/null | \
  openssl x509 -noout -serial -fingerprint

4. curl 디버깅 기법

기본 디버깅 옵션

# -v (verbose): 가장 기본적인 디버깅
curl -v https://api.example.com/health

# -vv: 더 상세한 출력
curl -vv https://api.example.com/health

# --trace: 바이트 단위 전체 통신 덤프
curl --trace /tmp/curl-trace.log https://api.example.com/health

# --trace-ascii: 가독성 좋은 ASCII 덤프
curl --trace-ascii /tmp/curl-trace.txt https://api.example.com/health

# --trace-time: 타임스탬프 포함
curl --trace-time --trace-ascii - https://api.example.com/health

고급 디버깅 기법

# DNS 우회 (--resolve): 특정 IP로 요청 보내기
# 배포 전 새 서버 테스트에 유용
curl --resolve api.example.com:443:10.0.1.50 \
  https://api.example.com/health

# 연결 시간 측정
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

# 리다이렉트 추적
curl -v -L --max-redirs 10 https://example.com 2>&1 | grep "< Location"

# 특정 HTTP 버전 강제
curl --http1.1 https://api.example.com/health
curl --http2 https://api.example.com/health
curl --http3 https://api.example.com/health

# 프록시 경유 디버깅
curl -v --proxy http://proxy.internal:8080 https://api.example.com/health

# 클라이언트 인증서 사용
curl --cert /path/to/client.crt --key /path/to/client.key \
  https://mtls.example.com/api

curl 실전 디버깅 원라이너

# 연속 요청으로 응답 시간 모니터링
for i in $(seq 1 10); do
  echo -n "요청 $i: "
  curl -o /dev/null -s -w "%{http_code} %{time_total}s" \
    https://api.example.com/health
  echo ""
  sleep 1
done

# 헤더만 빠르게 확인
curl -I https://api.example.com/health

# POST 요청 디버깅
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. 502/503/504 에러 트러블슈팅

502 Bad Gateway

# 원인: 업스트림 서버가 유효하지 않은 응답을 반환

# 1. 업스트림 서버 직접 확인
curl -v http://upstream-server:8080/health

# 2. Nginx 에러 로그 확인
tail -f /var/log/nginx/error.log | grep "502\|upstream"

# 3. 업스트림 서버 포트 리스닝 확인
ss -tlnp | grep 8080
netstat -tlnp | grep 8080

# 4. Nginx 설정 점검
# proxy_pass에서 업스트림 주소가 올바른지 확인
nginx -T | grep -A5 "upstream"

Nginx 502 해결을 위한 설정 조정:

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;

        # 버퍼 크기 조정 (큰 응답 헤더 처리)
        proxy_buffer_size 16k;
        proxy_buffers 4 32k;
        proxy_busy_buffers_size 64k;
    }
}

503 Service Unavailable

# 원인 분석
# 1. 서버 과부하 확인
top -bn1 | head -5
free -m
df -h

# 2. 연결 수 확인
ss -s
ss -tn state established | wc -l

# 3. 프로세스 상태 확인
systemctl status nginx
systemctl status your-app

# 4. 리소스 제한 확인
ulimit -n  # 파일 디스크립터 수
cat /proc/sys/net/core/somaxconn  # 소켓 백로그

504 Gateway Timeout

# 원인: 업스트림 서버 응답 시간 초과

# 1. 타임아웃 단계별 진단
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. 느린 쿼리 확인 (DB 원인인 경우)
# 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. 타임아웃 설정 조정
# Nginx: proxy_read_timeout 120s;
# HAProxy: timeout server 120s

6. CORS 문제 해결

CORS 작동 원리

브라우저                          서버
  |                                |
  |--- 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
  |                                |
  |--- 실제 요청 (POST) ----------->|
  |    Origin: https://app.com     |
  |                                |
  |<-- 200 OK --------------------|
  |    Access-Control-Allow-Origin: https://app.com

CORS 디버깅

# Preflight 요청 시뮬레이션
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"

# 응답에서 CORS 헤더 확인
curl -v https://api.example.com/data \
  -H "Origin: https://app.example.com" \
  2>&1 | grep -i "access-control"

Nginx에서 CORS 설정

location /api/ {
    # Preflight 요청 처리
    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;
    }

    # 실제 요청에도 CORS 헤더 추가
    add_header 'Access-Control-Allow-Origin' '$http_origin' always;
    add_header 'Access-Control-Allow-Credentials' 'true' always;

    proxy_pass http://backend;
}

7. 리다이렉트 루프 디버깅

# 리다이렉트 체인 추적
curl -v -L --max-redirs 20 https://example.com 2>&1 | \
  grep -E "^< (HTTP|Location)" | head -30

# 결과 예시 (무한 루프):
# < 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/   ← 루프!

# 흔한 원인:
# 1. HTTP → HTTPS 리다이렉트 + HTTPS → HTTP 리다이렉트 충돌
# 2. www ↔ non-www 리다이렉트 충돌
# 3. 로드밸런서의 SSL termination + 앱의 HTTPS 강제

# 해결: X-Forwarded-Proto 헤더 활용
# Nginx에서 백엔드로 프로토콜 정보 전달
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

8. HTTP/2 및 HTTP/3 디버깅

HTTP/2 디버깅

# HTTP/2 지원 확인
curl -v --http2 https://example.com 2>&1 | grep "ALPN"

# HTTP/2 프레임 레벨 디버깅 (nghttp2)
nghttp -v https://example.com

# HTTP/2 멀티플렉싱 확인
nghttp -v -m 10 https://example.com/style.css \
  https://example.com/script.js \
  https://example.com/image.png

# HTTP/2 서버 푸시 확인
nghttp -v --stat https://example.com 2>&1 | grep "push"

# h2c (HTTP/2 Cleartext) 테스트
curl -v --http2-prior-knowledge http://localhost:8080/health

HTTP/3 (QUIC) 디버깅

# HTTP/3 지원 확인
curl --http3 -v https://example.com 2>&1 | head -20

# Alt-Svc 헤더 확인 (HTTP/3 광고)
curl -sI https://example.com | grep -i "alt-svc"
# 예: alt-svc: h3=":443"; ma=86400

# QUIC 연결 디버깅 (quiche 도구)
# 0-RTT 연결 테스트
curl --http3 -v --connect-to example.com:443:server-ip:443 \
  https://example.com

HTTP/2와 HTTP/3의 일반적인 문제

# 1. HTTP/2의 HEAD-OF-LINE blocking 확인
# HTTP/2는 TCP 레이어에서 HOL blocking 발생 가능
# 해결: HTTP/3(QUIC) 사용 검토

# 2. HTTP/2 GOAWAY 프레임 디버깅
nghttp -v https://example.com 2>&1 | grep "GOAWAY"

# 3. HPACK 헤더 압축 문제
# 큰 헤더를 가진 요청에서 HPACK 동적 테이블 크기 확인
nghttp -v --header-table-size=65536 https://example.com

# 4. 스트림 동시성 제한
# SETTINGS_MAX_CONCURRENT_STREAMS 확인
nghttp -v https://example.com 2>&1 | grep "MAX_CONCURRENT"

9. 로드밸런서 및 리버스 프록시 트러블슈팅

헬스체크 실패 디버깅

# 1. 직접 헬스체크 엔드포인트 확인
curl -v http://backend-server:8080/health

# 2. 로드밸런서에서 보는 것과 동일한 조건으로 테스트
curl -v -H "Host: api.example.com" \
  --resolve api.example.com:80:10.0.1.10 \
  http://api.example.com/health

# 3. HAProxy 상태 확인
echo "show stat" | socat stdio /var/run/haproxy/admin.sock | \
  awk -F',' '{print $1, $2, $18, $19}'

# 4. Nginx 업스트림 상태 (nginx-module-vts 사용 시)
curl http://localhost/status/format/json | jq '.upstreamZones'

세션 고정(Sticky Session) 문제

# 쿠키 기반 세션 확인
curl -v -c cookies.txt https://app.example.com/login
curl -v -b cookies.txt https://app.example.com/dashboard

# 같은 백엔드로 라우팅되는지 확인
for i in $(seq 1 5); do
  curl -s -b cookies.txt https://app.example.com/api/server-id
  echo ""
done

X-Forwarded-* 헤더 디버깅

# 프록시 체인 확인
curl -v https://api.example.com/debug-headers 2>&1

# 예상되는 헤더:
# X-Forwarded-For: client-ip, proxy1-ip, proxy2-ip
# X-Forwarded-Proto: https
# X-Forwarded-Host: api.example.com
# X-Real-IP: client-ip

# 프록시가 원본 IP를 올바르게 전달하는지 테스트
curl -H "X-Forwarded-For: 1.2.3.4" https://api.example.com/my-ip

10. 실전 디버깅 시나리오

시나리오 1: 간헐적 502 에러

#!/bin/bash
# 간헐적 502 모니터링 스크립트
URL="https://api.example.com/health"
LOG_FILE="/tmp/502-monitor.log"

echo "=== 502 모니터링 시작: $(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

    # 502 발생 시 상세 정보 수집
    if [ "$HTTP_CODE" = "502" ]; then
      echo "  Detailed trace:" >> $LOG_FILE
      curl -v $URL 2>> $LOG_FILE
      echo "---" >> $LOG_FILE
    fi
  fi

  sleep 5
done

시나리오 2: TLS 인증서 만료 모니터링

#!/bin/bash
# 인증서 만료 모니터링 스크립트
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: 연결 실패"
    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: 인증서 만료됨! ($EXPIRY)"
  elif [ $DAYS_LEFT -lt $WARNING_DAYS ]; then
    echo "[WARNING] $domain: ${DAYS_LEFT}일 후 만료 ($EXPIRY)"
  else
    echo "[OK] $domain: ${DAYS_LEFT}일 남음 ($EXPIRY)"
  fi
done

시나리오 3: 전체 HTTP 통신 종합 진단

#!/bin/bash
# HTTP 종합 진단 스크립트
URL=${1:-"https://example.com"}
echo "============================================"
echo "HTTP 종합 진단: $URL"
echo "시간: $(date)"
echo "============================================"

# 1. DNS 확인
echo -e "\n[1] DNS 확인"
DOMAIN=$(echo $URL | awk -F[/:] '{print $4}')
dig +short $DOMAIN A
dig +short $DOMAIN AAAA

# 2. 포트 연결 확인
echo -e "\n[2] TCP 연결 확인"
nc -zv $DOMAIN 443 2>&1

# 3. TLS 정보
echo -e "\n[3] TLS 정보"
echo | openssl s_client -connect ${DOMAIN}:443 -servername ${DOMAIN} 2>/dev/null | \
  grep -E "Protocol|Cipher|Verify"

# 4. 인증서 정보
echo -e "\n[4] 인증서 정보"
echo | openssl s_client -connect ${DOMAIN}:443 -servername ${DOMAIN} 2>/dev/null | \
  openssl x509 -noout -subject -issuer -dates 2>/dev/null

# 5. HTTP 응답
echo -e "\n[5] HTTP 응답"
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. 응답 헤더
echo -e "\n[6] 주요 응답 헤더"
curl -sI $URL | grep -iE "^(server|x-|content-type|cache-control|strict|location|set-cookie)"

# 7. 보안 헤더 확인
echo -e "\n[7] 보안 헤더 검사"
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 "  [O] $header"
  else
    echo "  [X] $header (누락)"
  fi
done

echo -e "\n============================================"
echo "진단 완료"
echo "============================================"

마치며

HTTP/HTTPS 트러블슈팅은 계층적 접근이 핵심이다. DNS, TCP 연결, TLS 핸드셰이크, HTTP 프로토콜, 애플리케이션 로직 순서로 문제를 좁혀 나가면 대부분의 문제를 효율적으로 해결할 수 있다.

여기서 소개한 도구와 기법을 활용해서 운영 환경에서 발생하는 문제를 체계적으로 진단하는 습관을 기르자. 특히 curl -w 포맷 문자열과 openssl s_client는 반드시 익혀두면 실무에서 큰 도움이 된다.

핵심 정리:

  • 상태 코드는 단서일 뿐이다 - 반드시 응답 본문과 헤더를 함께 확인
  • TLS 문제는 openssl s_client로 시작
  • 타이밍 문제는 curl -w로 각 단계별 시간 측정
  • 간헐적 문제는 자동화된 모니터링 스크립트로 포착
  • 프록시 관련 문제는 각 홉에서 직접 테스트하여 병목 구간 특정

The Complete HTTP/HTTPS Troubleshooting Guide - Production Debugging Techniques

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 ContinueServer acknowledges the client may send a large request body
101 SwitchingMust verify during WebSocket upgrade negotiations
103 Early HintsUsed for browser preload optimization

2xx (Success)

200 OKThe baseline. But an empty body deserves suspicion
201 CreatedAlways check the Location header after POST
204 No ContentAppropriate for DELETE responses. A body here is a bug
206 PartialRange requests. Essential for large file download debugging

3xx (Redirection) - Full of Pitfalls

301 Moved PermanentlyBrowsers cache this aggressively. Misconfigurations are hard to undo
302 FoundTemporary redirect. Watch for SEO confusion with 301
303 See OtherRedirect POST to GET (Post/Redirect/Get pattern)
307 Temporary RedirectPreserves the HTTP method
308 Permanent RedirectPermanent 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 ErrorCheck server-side logs first
502 Bad GatewayUpstream server returned an invalid response
503 Service UnavailableServer overloaded or in maintenance mode
504 Gateway TimeoutUpstream 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 -w to 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

Quiz

Q1: What is the main topic covered in "The Complete HTTP/HTTPS Troubleshooting Guide - Production Debugging Techniques"?

A comprehensive guide covering HTTP status code analysis, TLS handshake debugging, certificate chain validation, advanced curl techniques, 502/503/504 error resolution, CORS issues, redirect loops, HTTP/2 and HTTP/3 debugging, and load balancer troubleshooting with real-world sce...

Q2: What approach is recommended for HTTP Status Codes - A Debugging Perspective?

1xx (Informational) Often overlooked but important status codes in practice: 2xx (Success) 3xx (Redirection) - Full of Pitfalls 4xx (Client Errors) - Core Debugging Territory 5xx (Server Errors) - The Highest Urgency

Q3: What approach is recommended for TLS/SSL Handshake Troubleshooting? Understanding the TLS Handshake Flow Debugging with openssl Common TLS Errors and Solutions

Q4: What are the key aspects of Certificate Chain Validation Issues? Certificate Chain Structure Chain Verification Script Common Certificate Problems

Q5: What approach is recommended for curl Debugging Techniques? Basic Debugging Options Advanced Debugging Techniques Practical curl One-Liners