HTTP/1.1の裏技?Upgradeヘッダーで「ただのWeb」をリアルタイム通信に変える技術
ネットワークエンジニアとして現場を渡り歩いていると、幾度となく「HTTPって本当に懐が深いプロトコルだな」と感嘆させられます。元々は静的なドキュメントをブラウザに届けるためだけに生まれた超シンプルなテキストベースのプロトコルが、今や動画のストリーミングを支え、WebSocketによる双方向のリアルタイム通信をハンドシェイクし、果てはTLSへのトンネリングまでやってのける。
その「変身能力」の裏側で密かに、しかし確実に主導権を握っているのが今回解説する `Upgrade` ヘッダー です。
Web APIの設計やインフラのサイジングを行っていると、「なぜこのリクエストは途中でプロトコルが変わるのか」「Nginxの逆プロキシでなぜハンドシェイクが切断されるのか」という壁に必ずぶつかります。教科書通りのGET/POSTのやり取りだけを知っている状態から、パケットがトランスポート層の上下でどう振る舞っているかを理解するステージへステップアップするために、Upgradeヘッダーのメカニズムを徹底的に解剖していきましょう。
—
1. UpgradeヘッダーとConnectionヘッダーの基本思想
HTTP/1.1(RFC 7230 / RFC 9110)において、通信の途中から全く別のプロトコルへ切り替えるためのメカニズムが規定されています。これが Upgrade機構 です。
ここで重要なのは、Upgradeヘッダー単体では機能しないという点です。必ず `Connection: upgrade` というヘッダーとセットで運用しなければなりません。
なぜConnectionヘッダーが必要なのか?
HTTP/1.1では、プロキシサーバーやロードバランサーが無数のリクエストを中継します。もしクライアントが勝手に「ここから先のプロトコルを変えるぜ」と言い出しても、経路上にある中継機器(リバースプロキシやキャッシュサーバー)がそのプロトコルを解釈できなければ、通信はそこで破綻してしまいます。
そのため、HTTPの「ホップバイホップヘッダー(Hop-by-hop header)」のルールに従い、
1. `Connection: upgrade` によって、「この接続(Connection)単位で、次の転送時にプロトコルを昇格させる意図がある」ことを中継機器に伝える。
2. 経路上の中継機器は、自身がそのプロトコルをサポートしていなければリクエストをフォワードしない、あるいは適切にトンネリングモードに切り替える。
という厳密な合意形成が行われます。この「事前の握手」がないまま勝手にバイナリデータを流し始めようものなら、ファイアウォールやWAF(Web Application Firewall)に不審なパケットとして即座にドロップされるのがオチです。
—
2. プロトコル昇格の通信フロー(シーケンス)
百聞は一見に如かず。HTTPからWebSocketへ昇格(アップグレード)する際の、実際のHTTPハンドシェイクのシーケンスを見てみましょう。
Client Server/Gateway
| |
| —– [1. GET /chat (HTTP/1.1)] ————————> |
| Host: api.example.com |
| Upgrade: websocket |
| Connection: Upgrade |
| Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==== |
| Sec-WebSocket-Version: 13 |
| |
| <---- [2. HTTP/1.1 101 Switching Protocols] ------------ |
| Upgrade: websocket |
| Connection: Upgrade |
| Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= |
| |
| ===== [3. WebSocket Protocol (TCP継続・双方向)] ======= |
| |
ステップごとの挙動解説
1. クライアントからの昇格リクエスト
クライアント(ブラウザやAPIクライアント)は、通常のHTTP GETリクエストに `Upgrade: websocket` と `Connection: Upgrade` を付与して送信します。WebSocket特有のセキュリティキー(`Sec-WebSocket-Key`)もここに同梱されます。
2. サーバーからの「101 Switching Protocols」応答
サーバーがリクエストを受け入れ、プロトコルを切り替える準備が整うと、歴史的なステータスコードである `101 Switching Protocols` を返します。
この瞬間を境に、これまでのHTTP/1.1のセマンティクス(リクエスト・レスポンスの往復モデル)が終了し、同じTCPコネクションの上でまったく別のプロトコル(WebSocketのフレームワーク)の通信が開始されます。
—
3. 実務で役立つ!各種パラメーターと仕様の深掘り
現場のトラブルシューティングやプロキシ設計で絶対に押さえておくべきポイントをいくつか整理しておきます。
ステータスコード「101」の重み
HTTPステータスコードの `101` は、「サーバーがクライアントからのUpgradeヘッダーの要求を理解し、通信プロトコルを切り替える用意がある」ことを示します。
もしサーバー側がUpgradeに対応していない場合、あるいは拒否する場合は、`400 Bad Request` や `426 Upgrade Required`(RFC 7230で定義された「クライアントは指定されたプロトコルにアップグレードして再試行すべき」というコード)が返されます。
ケータイ・プロキシ・ロードバランサーの罠
インフラエンジニアとして一番頭を悩ませるのが、「途中のリバースプロキシ(NginxやEnvoy、AWSALBなど)がUpgradeヘッダーを素通りさせてくれるか」という問題です。
デフォルトの設定では、多くのリバースプロキシは `Connection` ヘッダーを「ホップバイホップヘッダー」と見なし、バックエンドへ転送する際に削除してしまいます。これを見落とすと、クライアントがUpgradeを要求しているのに、バックエンドサーバーには「ただの普通のGETリクエスト」として届いてしまい、永遠に101が返ってこないという泥沼のデバッグに陥ります。
—
4. 実装・検証サンプルコードと設定例
それでは、実際に手を動かしてこの挙動を確認・実装するためのコードと設定を見ていきましょう。
A. デバッグの基本:cURLを使ったハンドシェイク確認
まずは、APIの疎通確認やプロキシの挙動テストに欠かせないcURLコマンドです。WebSocketのハンドシェイクをシミュレートしてみます。
WebSocketのエンドポイントに対して手動でUpgradeリクエストを投げる
curl -i -N \
-H “Connection: Upgrade” \
-H “Upgrade: websocket” \
-H “Host: echo.websocket.events” \
-H “Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==” \
-H “Sec-WebSocket-Version: 13” \
https://echo.websocket.events/
実務Tips: `-i` オプションでレスポンスヘッダー(`HTTP/1.1 101 Switching Protocols` や `Upgrade: websocket` が返ってくるか)を必ず目視で確認してください。ここが崩れている場合、Nginxなどのプロキシ層でヘッダーがドロップされています。
B. フロントエンド:Fetch API等における制限とWebSocketの利用
勘の良い方なら気づくかもしれませんが、標準の `fetch()` APIは、実はHTTP/1.1の `Upgrade` ヘッダーを明示的に指定してWebSocketや独自のTCPプロトコルへ昇格させることはできません(ブラウザのセキュリティとプロトコル層の抽象化のためです)。
そのため、Webブラウザからプロトコルを昇格させる場合は、専用のラッパーである `WebSocket` オブジェクトを使用します。
// JavaScriptでのWebSocket接続の初期化
// ブラウザはこの内部で自動的に HTTP/1.1 の Upgrade リクエストを生成・送信しています
const socket = new WebSocket(‘wss://echo.websocket.events’);
// 接続確立(101 Switching Protocolsの完了)時のコールバック
socket.onopen = function(event) {
console.log(“【INFO】プロトコルの昇格に成功し、リアルタイム通信が確立しました。”);
// サーバーへメッセージを送信
socket.send(“こんにちは、HTTP/1.1 Upgradeの世界!”);
};
// サーバーからのメッセージ受信
socket.message = function(event) {
console.log(“受信データ: “, event.data);
};
socket.onerror = function(error) {
console.error(“【ERROR】ハンドシェイク失敗または通信エラー:”, error);
};
C. インフラ担当者必見:Nginxのリバースプロキシ設定
現場で最も多いトラブルが「Nginxの後ろにアプリケーションサーバー(Node.jsやPython等)を置いたらWebSocketがつながらない」というものです。Nginxに明示的に「Upgradeを背後にパススルーしろ」と教えてあげる必要があります。
server {
listen 80;
server_name api.example.com;
location /chat {
proxy_pass http://127.0.0.1:8080;
# HTTP版の基本設定
proxy_http_version 1.1;
# 【超重要】クライアントからのUpgrade要求をバックエンドへ確実に渡す
proxy_set_header Upgrade $http_upgrade;
# 【超重要】Connectionヘッダーを動的に書き換える(またはそのまま渡す)
# クライアントがConnection: upgradeと言ってきたら、Nginxもバックエンドに対してそれを維持する
proxy_set_header Connection “upgrade”;
# タイムアウトの調整(リアルタイム通信で勝手に切断されないように長めに設定)
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
}
シニアからのアドバイス: ここで `$http_upgrade` 変数を使用するのがミソです。リクエストにUpgradeヘッダーが存在しない通常のHTTPリクエストの場合は空文字になり、WebSocket等のリクエストの場合は `websocket` がそのままバックエンドに引き渡されます。
—
5. まとめ:HTTP/1.1から次世代プロトコルへの架け橋
Upgradeヘッダーは、HTTP/1.1という「枯れた、しかし偉大なプロトコル」が、現代のリッチなリアルタイムWebアプリケーションやカスタムプロトコルを包み込むために用意された、極めてエレガントな延命・拡張メカニズムです。
APIの設計やインフラの構築において、「ただルーティングするだけ」から「プロトコルのライフサイクルそのものを理解して設計する」視点に切り替えるだけで、障害発生時の切り分けスピードは圧倒的に変わります。パケットキャプチャを開いたときに `101 Switching Protocols` が美しく光る瞬間を、ぜひ皆さんの現場でも味わってみてください。
コメント