- Authors
- Name
- はじめに
- 1. HTTPステータスコード - デバッグ視点での再解釈
- 2. TLS/SSLハンドシェイクのトラブルシューティング
- 3. 証明書チェーン検証の問題
- 4. curlデバッグテクニック
- 5. 502/503/504エラーのトラブルシューティング
- 6. CORS問題の解決
- 7. リダイレクトループのデバッグ
- 8. HTTP/2およびHTTP/3のデバッグ
- 9. ロードバランサーおよびリバースプロキシのトラブルシューティング
- 10. 実践デバッグシナリオ
- まとめ
はじめに
本番環境でHTTP/HTTPS関連の問題が発生した場合、単に「動きません」という報告だけでは原因の特定は困難である。ネットワークレイヤー、TLSハンドシェイク、アプリケーションロジック、ロードバランサーの設定など、複数のレイヤーで問題が発生する可能性があるためだ。
本記事では、実務で頻繁に遭遇するHTTP/HTTPS問題を体系的に診断し、解決する方法を解説する。
1. HTTPステータスコード - デバッグ視点での再解釈
1xx(情報レスポンス)
実務で見落とされがちだが重要なステータスコードがある。
100 Continue → クライアントが大きなリクエストボディを送信する前にサーバーが受理を示す
101 Switching → WebSocketアップグレード時に必ず確認すべきコード
103 Early Hints → ブラウザのpreload最適化に活用
2xx(成功)
200 OK → 最も基本。しかしボディが空の場合は疑うべき
201 Created → POST後のLocationヘッダー確認が必須
204 No Content → DELETEレスポンスとして適切。ボディがあればバグ
206 Partial → Rangeリクエスト時。大容量ファイルダウンロードのデバッグに重要
3xx(リダイレクション) - 落とし穴が多い領域
301 Moved Permanently → ブラウザがキャッシュする。誤設定すると復旧が困難
302 Found → 一時リダイレクト。SEO観点で301との混同に注意
303 See Other → POST後にGETへリダイレクトする場合に使用
307 Temporary Redirect → HTTPメソッドを維持するリダイレクト
308 Permanent Redirect → HTTPメソッドを維持する恒久リダイレクト
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で各段階の所要時間を計測する - 断続的な問題は自動化された監視スクリプトで捕捉する
- プロキシ関連の問題は各ホップで直接テストし、ボトルネック区間を特定する