大きなリクエストボディ送信前に「待って!」を伝える技:HTTP/1.1の`Expect: 100-continue`ヘッダーを使いこなす
Web APIを設計したり、インフラの運用に携わったりしている皆さん、こんにちは。現場で数々のネットワーク障害と格闘してきたベテランエンジニアです。今日は、HTTP/1.1のちょっとニッチだけど、知っておくと「なるほど!」となる、`Expect: 100-continue`ヘッダーについて、その実用的な使い方をじっくり解説していきましょう。
なぜ`Expect: 100-continue`が必要なのか?
皆さんは、巨大なファイルをアップロードするAPIや、大量のデータをPOSTするような処理を実装したことはありますか? そういった場合、クライアント(ブラウザやアプリケーション)は、いきなり大きなリクエストボディをサーバーに送りつけます。
しかし、もしサーバー側で「あ、そのリクエスト、受け付けられないよ」という場合、どうなるでしょうか? クライアントは、せっかく送った大量のデータを無駄にしてしまい、さらにサーバー側も、受け取ったデータを処理するためのリソースを無駄にしてしまう可能性があります。これは、特にネットワーク帯域が限られている環境や、サーバーリソースに余裕がない場合には、非常に痛いロスです。
ここで登場するのが、`Expect: 100-continue`ヘッダーです。これは、クライアントが大きなリクエストボディを送信する前に、サーバーに対して「このリクエスト、送っても大丈夫?」と確認するための仕組みなのです。
RFC 7231(HTTP/1.1の仕様を定義する主要なRFCの一つ)では、この`Expect`ヘッダーについて次のように定義されています。
> The `Expect` header field allows a client to indicate that it requires the server to respond with certain behavior before it sends the message body. The only defined extension to `Expect` is `100-continue` […]
> (クライアントは、メッセージボディを送信する前に、サーバーに特定の動作を期待することを`Expect`ヘッダーフィールドで示すことができます。`Expect`の定義されている唯一の拡張は`100-continue`です。)
つまり、`Expect: 100-continue`は、クライアントが「メッセージボディを送信する前に、サーバーからの応答を待つ」という意思表示をするためのものです。
通信フロー:パケットが旅する道筋を追う
では、`Expect: 100-continue`が使われたときの通信フローを、パケットの旅を想像しながら見ていきましょう。
1. クライアント:「ちょっと待って、リクエストボディ送る前に確認!」
クライアントは、リクエストメソッド(POSTやPUTなど)と、大きなリクエストボディを送信しようとしています。その際、ヘッダーに`Expect: 100-continue`を追加します。
POST /upload HTTP/1.1
Host: example.com
Content-Type: application/octet-stream
Content-Length: 104857600 // 100MBのデータ
Expect: 100-continue // ここがポイント!
この時点では、リクエストボディ本体はまだ送信されていません。
2. サーバー:「OK、リクエストは受け取ったよ。ボディをどうぞ!」
サーバーはリクエストヘッダーを受け取ります。`Expect: 100-continue`ヘッダーがあるのを確認すると、サーバーはリクエストのヘッダー部分だけを解析し、そのリクエストが受け入れ可能かどうかを判断します。
もし、リクエストヘッダーの情報(例えば、認証情報やリクエストURIなど)だけで、リクエストボディを受け付けるべきでないと判断した場合(例: 認証エラー、リソース上限超過など)、サーバーはすぐにエラーレスポンス(4xx系や5xx系)を返します。
しかし、もしヘッダー情報だけでは問題ないと判断した場合、サーバーはクライアントに100 Continueというステータスコードを返します。
HTTP/1.1 100 Continue
この100 Continueは、クライアントに対して「リクエストボディを送信しても良いですよ」という合図です。
3. クライアント:「よし、じゃあボディを送るね!」
クライアントは、サーバーから100 Continueを受け取ると、いよいよ本来送信したかったリクエストボディ本体を送信します。
<リクエストボディ本体(例: 100MBのバイナリデータ)>
4. サーバー:「ボディも受け取ったよ!処理するね。」
サーバーはリクエストボディ全体を受け取ります。そして、そのリクエストを正常に処理し、最終的なレスポンス(例: 200 OK, 201 Createdなど)をクライアントに返します。
もし、サーバーが100 Continueを返さなかったら?
サーバーが`Expect: 100-continue`ヘッダーをサポートしていない場合、あるいはヘッダー情報だけでリクエストを却下する場合、クライアントは100 Continueを受け取れません。
- サーバーが100 Continueをサポートしていない場合: サーバーは、クライアントからのリクエストヘッダーを受け取った後、リクエストボディの受信を待たずに、そのまま最終的なレスポンス(例: 200 OK, 404 Not Foundなど)を返してしまうことがあります。この場合、クライアントは100 Continueを受け取っていないので、そのままリクエストボディを送信してしまいます。結果として、ヘッダーとボディが分離された状態で処理されることになりますが、多くのサーバー実装では問題なく扱われます。
- サーバーがリクエストを却下した場合: ヘッダー情報でリクエストが却下された場合、サーバーは100 Continueではなく、例えば400 Bad Requestや401 Unauthorizedのようなエラーレスポンスを返します。この場合、クライアントはリクエストボディを送信せずに処理を中断できます。
`Expect`ヘッダーのパラメーター:`100-continue`以外は?
RFC 7231では、`Expect`ヘッダーの拡張として`100-continue`以外にも定義されていますが、実質的に広く使われているのは`100-continue`だけです。他の拡張は、互換性の問題や実用性の低さから、ほとんど実装されていません。
したがって、皆さんが実務で「`Expect`ヘッダー」と聞いたら、ほぼ間違いなく「`Expect: 100-continue`」のことだと考えて良いでしょう。
実践!コードで見てみよう
概念を理解したところで、実際にコードでどのように使われるのかを見ていきましょう。
1. `curl`での利用例
`curl`は、コマンドラインでHTTPリクエストを送信するための強力なツールです。`-H`オプションでヘッダーを指定できます。
100MBのダミーデータを作成
dd if=/dev/zero of=dummy_data.bin bs=1M count=100
Expect: 100-continue を付けてPOSTリクエストを送信
-v オプションで通信の詳細を表示すると、100 Continue が確認できます
curl -v -X POST \
-H “Content-Type: application/octet-stream” \
-H “Expect: 100-continue” \
–data-binary @dummy_data.bin \
http://example.com/upload
このコマンドを実行すると、`curl`はまず`Expect: 100-continue`ヘッダーを付けてリクエストを送信します。サーバーから`HTTP/1.1 100 Continue`という応答があれば、`curl`は`dummy_data.bin`の内容を送信します。もしサーバーが100 Continueを返さなければ、`curl`はボディを送信し、サーバーからの最終的なレスポンスを待ちます。
2. JavaScript (Fetch API) での利用例
ブラウザのJavaScriptからFetch APIを使う場合、`headers`オブジェクトに`Expect`ヘッダーを指定することで同様の動作を期待できます。
async function uploadFile(file) {
const url = ‘http://example.com/upload’;
// リクエストボディのサイズを確認
if (file.size > 10 1024 1024) { // 例: 10MB以上のファイルの場合
console.log(‘大きなファイルのため、Expect: 100-continue を使用します。’);
try {
const response = await fetch(url, {
method: ‘POST’,
headers: {
// ExpectヘッダーはFetch APIでは直接設定できません。
// これはFetch APIの制限事項です。
// 実際には、クライアントライブラリやサーバー側での判断に依存します。
// 多くのモダンなHTTPクライアントライブラリは、
// Content-Lengthヘッダーを見て自動的に Expect: 100-continue を追加するかどうかを決定します。
// もし明示的に設定したい場合は、XMLHttpRequestを使用するか、
// Axiosなどのライブラリで対応しているか確認が必要です。
// 以下は概念を示すためのコメントですが、Fetch APIでは直接は動作しません。
// ‘Expect’: ‘100-continue’,
‘Content-Type’: ‘application/octet-stream’,
// Content-Length は自動的に設定されます
},
body: file // ファイルオブジェクトを直接bodyに設定
});
// Fetch API は 100 Continue を直接扱わないため、
// サーバーからの最初のレスポンスは最終的なレスポンスになります。
// サーバー側で Expect: 100-continue をサポートし、
// ヘッダーだけでリクエストを却下した場合、ここにエラーが返ります。
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json(); // または response.text() など
console.log(‘Upload successful:’, result);
return result;
} catch (error) {
console.error(‘Upload failed:’, error);
}
} else {
// 小さなファイルの場合は通常通りPOST
console.log(‘小さなファイルなので、通常の方法でアップロードします。’);
try {
const response = await fetch(url, {
method: ‘POST’,
headers: {
‘Content-Type’: ‘application/octet-stream’,
},
body: file
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
console.log(‘Upload successful:’, result);
return result;
} catch (error) {
console.error(‘Upload failed:’, error);
}
}
}
// 使用例 (HTMLのinput要素からファイルを取得するなど)
// const fileInput = document.getElementById(‘file-input’);
// fileInput.addEventListener(‘change’, (event) => {
// const file = event.target.files[0];
// if (file) {
// uploadFile(file);
// }
// });
【重要】Fetch APIの注意点:
実は、JavaScriptのFetch APIは、RFCで定義されている `Expect: 100-continue` の挙動を直接サポートしていません。`Expect`ヘッダーを明示的に設定しようとしても、ブラウザの実装によっては無視されるか、正しく機能しないことがあります。
多くの場合、Fetch APIのようなモダンなクライアントは、`Content-Length`ヘッダーの存在を見て、自動的に `Expect: 100-continue` を送信するかどうかを判断するようになっています。しかし、この挙動はブラウザやライブラリの実装に依存するため、厳密な制御が必要な場合は、`XMLHttpRequest`オブジェクトを使用するか、Axiosのようなサードパーティライブラリのドキュメントを確認することをおすすめします。
3. Python (`requests`ライブラリ) での利用例
Pythonの`requests`ライブラリは、`Expect: 100-continue`をスマートに扱ってくれます。デフォルトでは、大きなボディ(`Content-Length`ヘッダーが設定され、かつサイズが大きい場合)に対して自動的に`Expect: 100-continue`ヘッダーを追加します。
import requests
ダミーファイルを作成 (例: 100MB)
with open(‘dummy_data.bin’, ‘wb’) as f:
f.seek(100 1024 1024 – 1) # 100MB – 1バイト目に移動
f.write(b’\0′) # 最後のバイトに何か書き込む
url = ‘http://example.com/upload’
file_path = ‘dummy_data.bin’
large_file_size_threshold = 1 1024 1024 # 例: 1MB以上でExpectヘッダーを考慮
ファイルを開く
with open(file_path, ‘rb’) as f:
# ファイルサイズを取得
f.seek(0, 2) # ファイルの末尾に移動
file_size = f.tell()
f.seek(0) # ファイルの先頭に戻す
headers = {
‘Content-Type’: ‘application/octet-stream’,
# requestsライブラリは、Content-Lengthが設定され、
# かつサイズが大きい場合に自動的に ‘Expect’: ‘100-continue’ を追加します。
# 明示的に無効にしたい場合は allow_redirects=False のような引数で制御できる場合もありますが、
# 通常は自動に任せるのが良いでしょう。
}
try:
# data引数ではなく files 引数でファイルを送信するのが一般的
# files={‘file’: (file_path, f)} は、filenameもサーバーに伝える場合
# 単純なバイナリ送信なら、data引数でbytesを指定しても良い
# requests は Content-Length ヘッダーを自動で設定します
response = requests.post(url, headers=headers, data=f) # data引数にファイルオブジェクトを渡す
# response.request.headers を見ると、Expectヘッダーが追加されているか確認できます
print(“Sent Headers:”, response.request.headers)
response.raise_for_status() # HTTPエラーがあれば例外を発生させる
print(“Upload successful!”)
print(“Response:”, response.json()) # または response.text()
except requests.exceptions.RequestException as e:
print(f”Upload failed: {e}”)
except Exception as e:
print(f”An unexpected error occurred: {e}”)
Pythonの`requests`ライブラリのように、多くのHTTPクライアントライブラリは、`Content-Length`ヘッダーを見て、そのサイズが大きい場合に自動的に`Expect: 100-continue`ヘッダーを付加するようになっています。これは、開発者が明示的にヘッダーを記述する手間を省き、効率的な通信を実現するための賢い設計と言えます。
4. Webサーバーの設定例 (Nginx)
Webサーバー側では、`Expect: 100-continue`ヘッダーを受け取った際の挙動を制御することができます。Nginxの場合、`client_header_buffer_size`や`large_client_header_buffers`といった設定が関連してきますが、`Expect: 100-continue`自体を明示的に無効にするような直接的なディレクティブは、標準では用意されていません。
これは、`Expect: 100-continue`がクライアント側の機能であり、サーバー側はそれを受け取ったら仕様に従って処理するのが基本だからです。
もし、どうしても`Expect: 100-continue`を無効にしたい、あるいは特定の条件下でリクエストボディの送信を許可したくない、といった高度な制御が必要な場合は、リバースプロキシやアプリケーションサーバー(PHP-FPM, uWSGIなど)の設定、あるいはWebアプリケーションのコード側でロジックを実装する必要があります。
一般的には、Nginxは`Expect: 100-continue`を適切に解釈し、サーバー側でリクエストボディの受信を待ってから処理を行うため、特別な設定なしで期待通りに動作することがほとんどです。
トラブルシューティングのヒント
`Expect: 100-continue`でハマるケースは少ないかもしれませんが、もし挙動がおかしい場合は、以下の点を確認してみてください。
- サーバー側のサポート: 使用しているWebサーバーやアプリケーションフレームワークが`Expect: 100-continue`を正しくサポートしているか確認しましょう。古いバージョンのソフトウェアでは、このヘッダーの扱いが不十分な場合があります。
- リクエストヘッダーの解析: サーバー側で、`Expect: 100-continue`を受け取った際に、リクエストヘッダー(URI、メソッド、認証情報など)を正しく解析できているか確認してください。ヘッダーの解析に失敗していると、ボディの受信に進めず、不審な挙動の原因になることがあります。
- ネットワーク機器の干渉: ファイアウォールやロードバランサーなど、クライアントとサーバーの間に存在するネットワーク機器が、HTTPヘッダーを改変したり、特定のヘッダーをブロックしたりしていないか確認します。特に、ヘッダーのサイズ制限などが影響する可能性もゼロではありません。
- クライアントライブラリの挙動: 使用しているHTTPクライアントライブラリが、`Expect: 100-continue`を期待通りに送信しているか、ライブラリのドキュメントやデバッグログで確認しましょう。前述のFetch APIのように、期待通りの動作をしない場合もあります。
まとめ:賢く使って効率アップ!
`Expect: 100-continue`ヘッダーは、大きなリクエストボディを送信する際に、無駄な通信やリソース消費を防ぐための非常に有効な手段です。RFCの仕様を理解し、クライアントライブラリがどのようにこれを扱っているかを知っておくことで、より堅牢で効率的なWeb APIやアプリケーションを設計・運用できるようになります。
特に、ファイルアップロードや大量データ送信の処理を実装する際には、このヘッダーの存在を意識してみてください。現場でのデバッグやパフォーマンスチューニングの際にも、きっと役立つはずです。
それでは、また次回の技術解説でお会いしましょう!
コメント