【実務・中級編】HTTP/1.1ステータスコード1xx(Informational)の役割とクライアントの待機挙動 – HTTPプロトコル・通信規格実践ガイド

「100 Continue」は、君のAPIリクエストが「まだ大丈夫」というサインだ! ~HTTP/1.1 Informationalステータスコードの裏側~

Web APIの設計やインフラ運用に携わる諸君、そして現場で日々格闘しているエンジニア諸君、ごきげんよう! 今日は、HTTP/1.1 のちょっと地味だけど、実はめちゃくちゃ重要な「1xx Informational」ステータスコード、特に「100 Continue」に焦点を当てて、その真の役割と、クライアント側の挙動について、俺たちの現場経験も交えながら、じっくりと掘り下げていこう。

RFCの仕様書を眺めているだけでは見えてこない、パケットがネットワークを駆け巡るリアルな挙動、そしてあの「タイムアウト」という悪夢を回避するために、この 1xx コードがどう働いているのか。諸君らのデバッグや設計に、必ずや役立つはずだ。

なぜ「100 Continue」が必要なのか? ~巨大なリクエストを無駄にしないために~

まず、HTTP/1.1 の「100 Continue」を理解するには、HTTP/1.0 時代まで遡る必要がある。当時は、クライアントがサーバーにリクエストを送ると、サーバーはすぐに最終的なレスポンス(200 OKとか404 Not Foundとか)を返していた。

ところが、HTTP/1.1 になって、Keep-Alive が標準化され、接続が維持されるようになった。さらに、Web アプリケーションがリッチになるにつれて、POST リクエストで送られてくるデータ量も増大した。

ここで問題が発生する。

  • 巨大なリクエストボディをサーバーに送りつけた結果、実はサーバー側で「そのリクエストは受け付けられない」ということが判明した場合。 例えば、認証情報が間違っていたり、リクエストボディのフォーマットがおかしかったり。この場合、クライアントはすでに大量のデータをアップロードし終えているのに、サーバーから「あ、ごめん、それ無理だったわ」と返される。これは、ネットワーク帯域の無駄だし、クライアント側の処理も無駄になる。

この非効率性を解消するために登場したのが、「100 Continue」というわけだ。

「100 Continue」の通信フロー:まるで「下見」のよう

「100 Continue」は、クライアントがサーバーにリクエストを送信する際に、「このリクエスト、本当に受け付けてくれる?」と事前に確認する仕組みだ。

通信の流れは、こんな感じだ。

1. クライアント、ヘッダーのみを送信: クライアントは、リクエストボディの大部分、あるいは全てを送信する前に、まずリクエストヘッダーに `Expect: 100-continue` というヘッダーを付けて、サーバーに送る。
2. サーバー、ヘッダーをチェック: サーバーは、このリクエストヘッダーを受け取り、内容をチェックする。
3. サーバー、「100 Continue」を返す(場合): もしサーバーが、ヘッダーの内容(例えば、リクエストURI、メソッド、一部のヘッダーフィールド)を見て、「うん、このリクエストは後続のボディを受け付ける準備ができている」と判断した場合、「100 Continue」 というステータスコードを返す。
4. クライアント、ボディを送信: クライアントは、「100 Continue」を受け取ったのを確認してから、リクエストボディをサーバーに送信する。
5. サーバー、最終的なレスポンスを返す: サーバーは、リクエストボディ全体を受け取った後、最終的なレスポンス(例: 200 OK, 400 Bad Request, 413 Payload Too Large など)を返す。

もし、サーバーがヘッダーをチェックした段階で「これはダメだ」と判断した場合は、「100 Continue」を返さずに、いきなり最終的なエラーレスポンス(例: 400 Bad Request, 417 Expectation Failed など)を返す。この場合、クライアントはボディの送信を中止する。

この「100 Continue」のやり取りは、まるで「これから重い荷物を運ぶんだけど、運ぶ前に『その場所まで運んでも大丈夫?』って確認する」ようなものだ。事前に確認することで、無駄な労力(=ネットワーク帯域やCPUリソース)を省くことができる。

「100 Continue」を実際に使ってみよう!

さて、理論は分かった。でも、実際にどうやって使うのか?

1. クライアント側(Fetch API)

Fetch API では、`Expect` ヘッダーを直接設定することはできない。しかし、`body` に大きなデータを設定した場合、ブラウザの実装によっては自動的に `Expect: 100-continue` が付与されることがある。

もし、明示的に `Expect: 100-continue` を使いたい場合は、`Headers` オブジェクトで指定する方法もあるが、注意が必要だ。 多くのサーバーはこのヘッダーを正しく処理しない可能性があるし、ブラウザのセキュリティポリシーで制限されることもある。

// Fetch API で大きなデータをPOSTする例(Expectヘッダーはブラウザが自動で付与する場合がある)
async function uploadLargeFile(url, file) {
const response = await fetch(url, {
method: ‘POST’,
headers: {
// ‘Expect’: ‘100-continue’, // 直接指定は注意が必要
‘Content-Type’: file.type // ファイルのMIMEタイプを指定
},
body: file // ファイルオブジェクトをbodyに設定
});

// ここで response.status をチェックするのではなく、
// サーバーからの最終的なレスポンスを待つことになる。
// もしサーバーが100 Continueを返した場合、fetchはそれを内部で処理し、
// 最終的なレスポンスを待つ。

if (response.ok) {
console.log(‘ファイルアップロード成功!’);
const result = await response.json();
console.log(‘サーバーからの応答:’, result);
} else {
console.error(‘ファイルアップロード失敗:’, response.status, response.statusText);
}
}

// 使用例
// const fileInput = document.getElementById(‘fileInput’);
// const uploadUrl = ‘/api/upload’;
// fileInput.addEventListener(‘change’, (event) => {
// const file = event.target.files[0];
// if (file) {
// uploadLargeFile(uploadUrl, file);
// }
// });

ポイント:

  • Fetch APIでは、`body`に大きなデータを設定すると、ブラウザが賢く `Expect: 100-continue` を追加してくれる場合が多い。
  • `100 Continue` は、クライアント側で特別な処理を要求するものではなく、サーバーからの「まだ待っていていいよ」という一時的な通知として受け止める。
  • `fetch` 関数は、`100 Continue` を受け取った場合、それを内部で処理し、最終的なレスポンスを返してくれる。クライアントコード側で `100 Continue` を直接ハンドリングする必要はほとんどない。

2. クライアント側(curl)

コマンドラインツール `curl` は、`Expect: 100-continue` を明示的に使うためのオプションが用意されている。

curl で Expect: 100-continue を使ってPOSTする例
-v オプションで通信の詳細を表示する
-H でヘッダーを指定する
–data-binary でバイナリデータをそのまま送信する
curl -v \
-H “Expect: 100-continue” \
-X POST \
–data-binary @your_large_file.bin \
http://your-api-server.com/upload

このコマンドを実行すると、以下のような出力が見られるはずだ。

  • Trying your-api-server.com:80…
  • Connected to your-api-server.com (xxx.xxx.xxx.xxx) port 80 (#0)

> POST /upload HTTP/1.1
> Host: your-api-server.com
> User-Agent: curl/7.79.1
> Accept: /
> Expect: 100-continue
>
< HTTP/1.1 100 Continue <-- ここ!サーバーが「OK、ボディ送ってこい」と返した < HTTP/1.1 200 OK <-- ここ!最終的なレスポンス < Content-Type: application/json < Content-Length: 15 <

  • Connection #0 to host your-api-server.com left intact

{“status”:”success”}

ポイント:

  • `curl` では `-H “Expect: 100-continue”` で明示的に指定できる。
  • `-v` オプションを使うと、`HTTP/1.1 100 Continue` という行が確認できる。
  • `–data-binary` は、ファイルの内容をそのまま送信するため、特にバイナリデータで重要。

3. サーバー側(Node.js / Express.js の場合)

多くのWebフレームワーク(Express.jsなど)は、HTTP/1.1 に準拠しており、デフォルトで `Expect: 100-continue` をサポートしている。

const express = require(‘express’);
const app = express();
const port = 3000;

// bigbody パーサーミドルウェア(例: 10MBまで許可)
// 本番環境では、もっと厳密なサイズ制限やバリデーションが必要
app.use(express.raw({ limit: ’10mb’, type: ‘/’ })); // 全てのContent-Typeを受け入れ、raw bodyをパース

app.post(‘/upload’, (req, res) => {
// req.headers[‘expect’] を直接見る必要はない。
// Expressが内部で ‘100-continue’ に対応している場合、
// ボディのパースが完了してからこのハンドラが呼ばれる。
// もし ‘Expect: 100-continue’ が期待通りに機能しない場合、
// サーバー側で Expect ヘッダーを明示的にチェックして、
// 100 Continue を返す実装も可能だが、通常はフレームワークが吸収してくれる。

console.log(`リクエストボディのサイズ: ${req.body.length} バイト`);

// ここでリクエストボディの内容をチェック
if (req.body.length > 5 1024 1024) { // 5MB以上の場合
// 5MB以上のリクエストボディは拒否する例
console.log(‘リクエストボディが大きすぎます。413 Payload Too Large を返します。’);
return res.status(413).send(‘Payload Too Large’);
}

// 成功した場合の処理
console.log(‘ファイルアップロード(またはデータ受信)成功!’);
res.status(200).json({ status: ‘success’, received_bytes: req.body.length });
});

app.listen(port, () => {
console.log(`サーバーが http://localhost:${port} で起動しました。`);
});

ポイント:

  • Express.js の `express.raw()` ミドルウェア(または `express.json()`, `express.text()` など)は、リクエストボディをパースする際に `Expect: 100-continue` を考慮してくれる。
  • クライアントから `Expect: 100-continue` が送られてきた場合、ミドルウェアはリクエストボディ全体を受け取る準備ができたと判断し、サーバーはこのハンドラを呼び出す前に、クライアントに `100 Continue` を返している(フレームワークの内部処理)。
  • もし、リクエストボディが大きすぎる、フォーマットがおかしいなどの理由で処理できない場合は、このハンドラ内で `res.status(4xx)` を返せば良い。サーバーは、クライアントがボディを送信し終える前にエラーを返せる。

「101 Switching Protocols」:プロトコルの「乗り換え」

さて、1xx コードにはもう一つ、「101 Switching Protocols」がある。これは、「100 Continue」とは少し役割が違う。

これは、クライアントがリクエストヘッダーで `Upgrade` ヘッダー を使って、「このHTTP接続を別のプロトコル(例: WebSocket)に切り替えたいんだけど、いい?」とサーバーに提案した場合に、サーバーが「OK、切り替えてあげよう」と返答する際に使われる。

通信の流れはこうだ。

1. クライアント、Upgradeヘッダー付きでリクエスト: クライアントは、HTTP/1.1 のコネクション上で、`Upgrade` ヘッダーに切り替えたいプロトコル(例: `websocket`)を指定し、リクエストを送信する。さらに、`Connection: Upgrade` ヘッダーも必須。
2. サーバー、Upgradeヘッダーをチェック: サーバーは、このリクエストを受け取り、`Upgrade` ヘッダーの内容を見て、そのプロトコルへの切り替えをサポートしているか判断する。
3. サーバー、「101 Switching Protocols」を返す: もしサーバーが切り替えをサポートしている場合、「101 Switching Protocols」 というステータスコードを返し、レスポンスヘッダーに `Upgrade` と `Connection: Upgrade` を含めて返す。
4. 通信プロトコルの切り替え: このレスポンスを受け取った後、クライアントとサーバーは、指定された新しいプロトコル(例: WebSocket)で通信を開始する。HTTPのセマンティクスは、この時点から適用されなくなる。

「101 Switching Protocols」の利用例(WebSocket)

// クライアント側(ブラウザのWebSocket API)
const socket = new WebSocket(‘ws://your-server.com/ws’);

socket.onopen = (event) => {
console.log(‘WebSocket接続が開きました!’);
// ここで WebSocket プロトコルでのメッセージ送信を開始
socket.send(‘こんにちは、WebSocketサーバー!’);
};

socket.onmessage = (event) => {
console.log(‘サーバーからメッセージを受信:’, event.data);
};

socket.onerror = (event) => {
console.error(‘WebSocketエラー:’, event);
};

socket.onclose = (event) => {
console.log(‘WebSocket接続が閉じました。’);
};

// サーバー側(Node.js / wsライブラリの例)
const WebSocket = require(‘ws’);

const wss = new WebSocket.Server({ noServer: true }); // httpサーバーとは別にWebSocketサーバーを起動

wss.on(‘connection’, (ws) => {
console.log(‘新しいWebSocketクライアントが接続しました。’);
ws.on(‘message’, (message) => {
console.log(‘クライアントからメッセージを受信:’, message);
ws.send(`サーバーから応答: ${message}`);
});
ws.on(‘close’, () => {
console.log(‘WebSocketクライアントが切断しました。’);
});
});

// HTTPサーバーのセットアップ(Expressなど)
const express = require(‘express’);
const http = require(‘http’);
const app = express();
const server = http.createServer(app); // ExpressアプリをHTTPサーバーに渡す

app.get(‘/’, (req, res) => {
res.send(‘HTTPサーバーは稼働中です。WebSocket接続を試みてください。’);
});

// WebSocketUpgradeリクエストのハンドリング
server.on(‘upgrade’, (req, socket, head) => {
// クライアントが WebSocket 接続を求めているかチェック
if (req.headers.upgrade && req.headers.upgrade.toLowerCase() === ‘websocket’) {
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit(‘connection’, ws, req);
});
} else {
socket.end(‘HTTP/1.1 400 Bad Request\r\n’);
}
});

const PORT = 8080;
server.listen(PORT, () => {
console.log(`HTTPサーバーがポート ${PORT} で起動しました。`);
console.log(`WebSocketサーバーも同時に起動しています。`);
});

ポイント:

  • `101 Switching Protocols` は、HTTP接続を別のプロトコルに「アップグレード」する際の合意を示す。
  • WebSocket が最も典型的な例だが、HTTP/2 のネゴシエーションなどでも関連する概念がある。
  • クライアント側で `WebSocket` API を使う場合、`new WebSocket(…)` のURLに `ws://` または `wss://` を指定すれば、ブラウザが自動的に `Upgrade` ヘッダーを付けてリクエストしてくれる。

クライアントの「待機挙動」とタイムアウト制御

さて、ここで本題に戻って、「1xx Informational」コード、特に「100 Continue」を受け取った時のクライアントの「待機挙動」と、それに伴うタイムアウト制御について考えよう。

「100 Continue」とタイムアウト

クライアントが `Expect: 100-continue` を送信し、サーバーからの応答を待っている間、クライアントは一定時間待機する。この待機時間には、いくつかのタイムアウトが関係してくる。

1. TCP接続タイムアウト: まず、そもそもサーバーとのTCP接続が確立できなければ、HTTPリクエストは始まらない。これは、DNS解決、SYN/ACKのやり取りなど、TCPレベルでのタイムアウト。
2. HTTPリクエストヘッダー送信後のタイムアウト: TCP接続が確立し、クライアントがリクエストヘッダー(`Expect: 100-continue` を含む)を送信した後、サーバーから応答(`100 Continue` または最終的なレスポンス)が返ってくるまでの時間。

  • このタイムアウトは、クライアント側のHTTPライブラリやブラウザの実装に依存する。
  • もし、このタイムアウト内にサーバーからの応答がなければ、クライアントはリクエストを諦め、「タイムアウトエラー」を発生させる。 これは、`504 Gateway Timeout` ではなく、クライアント側のリクエスト失敗として扱われることが多い(例: Fetch API の `TypeError`)。
  • 問題は、このタイムアウト値が固定されている場合が多いことだ。 ネットワーク遅延が大きい環境や、サーバーの処理に時間がかかる場合、デフォルトのタイムアウト値では短すぎてしまう可能性がある。

タイムアウトを回避するための工夫

  • サーバー側のチューニング:
  • `Expect: 100-continue` を受け取ったサーバーは、ヘッダーチェックを迅速に行い、できるだけ早く `100 Continue` を返すように実装する。
  • リクエストボディのバリデーションや処理も、できるだけ高速に行う。
  • クライアント側のタイムアウト設定:
  • HTTPクライアントライブラリによっては、タイムアウト値をカスタマイズできるものがある。例えば、Pythonの `requests` ライブラリなら `timeout` パラメータで指定できる。
  • Fetch API では、`AbortController` を使って、明示的なタイムアウトを設定することが一般的だ。

Fetch API でのタイムアウト制御例

Fetch API では、`AbortController` を使って、クライアント側でタイムアウトを設定するのが定石だ。

async function fetchDataWithTimeout(url, timeoutMs = 5000) {
const controller = new AbortController();
const signal = controller.signal;

// 指定時間後にabort()を呼び出す
const timeoutId = setTimeout(() => {
console.log(`${timeoutMs}ms 経過しました。リクエストを中止します。`);
controller.abort();
}, timeoutMs);

try {
const response = await fetch(url, {
method: ‘POST’, // 例としてPOST
headers: {
‘Content-Type’: ‘application/json’,
// ‘Expect’: ‘100-continue’ // 必要に応じて
},
body: JSON.stringify({ message: ‘Hello from client’ }), // 送信するデータ
signal: signal // AbortControllerのsignalを渡す
});

// リクエストが成功した場合(タイムアウトせずに完了した場合)
clearTimeout(timeoutId); // タイムアウトタイマーをクリア

if (!response.ok) {
// HTTPエラーレスポンスの場合
console.error(‘HTTPエラー:’, response.status, response.statusText);
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
console.log(‘データ受信成功:’, data);
return data;

} catch (error) {
// ネットワークエラー、タイムアウト、Abortによる中止など
if (error.name === ‘AbortError’) {
console.error(‘リクエストはタイムアウトしました。’);
} else {
console.error(‘リクエスト中にエラーが発生しました:’, error);
}
throw error; // エラーを再スローして呼び出し元でハンドリングできるようにする
}
}

// 使用例
// fetchDataWithTimeout(‘http://your-api-server.com/data’, 3000) // 3秒でタイムアウト
// .then(data => console.log(‘処理完了:’, data))
// .catch(error => console.error(‘処理失敗:’, error));

ポイント:

  • `AbortController` を使うことで、クライアント側で明示的にリクエストを中断できる。
  • `setTimeout` と組み合わせることで、指定時間経過後に `controller.abort()` を呼び出し、タイムアウトを実現する。
  • `fetch` の `try…catch` ブロックで `AbortError` を捕捉し、タイムアウト発生時の処理を記述する。
  • タイムアウトせずにリクエストが完了した場合は、`clearTimeout` でタイマーを解除することを忘れない。

まとめ:1xx コードは「円滑な通信」の陰の立役者

今日は、HTTP/1.1 の「1xx Informational」ステータスコード、特に「100 Continue」と「101 Switching Protocols」について、その役割、通信フロー、そしてクライアント側の待機挙動とタイムアウト制御という実践的な側面から解説してきた。

  • 100 Continue:
  • 巨大なリクエストボディを無駄に送信するのを防ぐための「事前確認」メカニズム。
  • クライアントは `Expect: 100-continue` を付けてヘッダーのみを送信し、サーバーからの応答を待つ。
  • サーバーが `100 Continue` を返せば、クライアントはボディを送信。それ以外の場合は中止。
  • クライアント側のタイムアウト管理が重要になる。
  • 101 Switching Protocols:
  • HTTP接続を他のプロトコル(WebSocketなど)に切り替える際の合意を示す。
  • `Upgrade` ヘッダーが鍵となる。

これらのコードは、HTTP/1.1 がHTTP/1.0 から進化し、より効率的で柔軟な通信を実現するための重要なピースだ。特に「100 Continue」は、APIのパフォーマンスやリソース効率に直接関わる部分でもある。

現場でAPIを設計する際、あるいはインフラを運用する際、クライアントがどのような挙動をするのか、そしてネットワークの遅延やタイムアウトといった現実的な問題にどう対処すべきかを理解することは、非常に重要だ。

今回の解説が、諸君らの日々の開発や運用における「なるほど!」に繋がっていれば幸いだ。何か疑問があれば、いつでもコメントで質問してくれ。また、次回の記事で会おう!

コメント

タイトルとURLをコピーしました