Skip to content
Published on

HTTP/HTTPSトラブルシューティング完全ガイド - 現場で使えるデバッグ手法

Authors
  • Name
    Twitter

はじめに

本番環境でHTTP/HTTPS関連の問題が発生した場合、単に「動きません」という報告だけでは原因の特定は困難である。ネットワークレイヤー、TLSハンドシェイク、アプリケーションロジック、ロードバランサーの設定など、複数のレイヤーで問題が発生する可能性があるためだ。

本記事では、実務で頻繁に遭遇するHTTP/HTTPS問題を体系的に診断し、解決する方法を解説する。

1. HTTPステータスコード - デバッグ視点での再解釈

1xx(情報レスポンス)

実務で見落とされがちだが重要なステータスコードがある。

100 Continue    → クライアントが大きなリクエストボディを送信する前にサーバーが受理を示す
101 SwitchingWebSocketアップグレード時に必ず確認すべきコード
103 Early Hints → ブラウザのpreload最適化に活用

2xx(成功)

200 OK          → 最も基本。しかしボディが空の場合は疑うべき
201 CreatedPOST後のLocationヘッダー確認が必須
204 No ContentDELETEレスポンスとして適切。ボディがあればバグ
206 PartialRangeリクエスト時。大容量ファイルダウンロードのデバッグに重要

3xx(リダイレクション) - 落とし穴が多い領域

301 Moved Permanently  → ブラウザがキャッシュする。誤設定すると復旧が困難
302 Found              → 一時リダイレクト。SEO観点で301との混同に注意
303 See OtherPOST後にGETへリダイレクトする場合に使用
307 Temporary RedirectHTTPメソッドを維持するリダイレクト
308 Permanent RedirectHTTPメソッドを維持する恒久リダイレクト

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
# 原因: クライアントとサーバー間で対応する暗号スイートがない
# 解決: サーバーの暗号スイートを確認
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. 証明書チェーン検証の問題

証明書チェーンの構造

ルートCA(自己署名、OS/ブラウザに内蔵)
  └── 中間CA(ルートCAが署名)
        └── サーバー証明書(中間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

# クライアント証明書認証(mTLS)
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設定の点検
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ヘッダー圧縮の問題
# 大きなヘッダーを持つリクエストの動的テーブルサイズを確認
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'

スティッキーセッションの問題

# Cookie基盤のセッション確認
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. TCP接続確認
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 "  [OK] $header"
  else
    echo "  [NG] $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で各段階の所要時間を計測する
  • 断続的な問題は自動化された監視スクリプトで捕捉する
  • プロキシ関連の問題は各ホップで直接テストし、ボトルネック区間を特定する