HTTP 条件付きリクエスト
HTTP には条件付きリクエストの概念があり、対象となるリソースと検証子の値とを比較することで、リクエストの結果や、成功か失敗かまでもが変化することがあります。このようなリクエストは、キャッシュの内容を検証して、無用な制御を避けたり、ダウンロードの再開の時などに文書の整合性を検証したり、サーバー上の文書をアップロードまたは変更するときに更新内容を失うことを避ける場合などに役立つことがあります。
原理
HTTP 条件付きリクエストは、特定のヘッダーの値に応じて異なる処理が行われるリクエストです。これらのヘッダーは前提条件を定義しており、リクエストの結果は前提条件に一致するか否かに応じて変わります。
リクエストで使用したメソッドや前提条件で使用したヘッダー一式によって、さまざまな動作が定義されています。
検証子
すべての条件ヘッダーは、サーバーに保存されているリソースが特定のバージョンに一致するか確認を試みます。このため、条件付きリクエストではリソースのバージョンを示す必要があります。リソース全体をバイト単位で比較することは不可能であり、常に望まれていることとは限らないので、リクエストはバージョンを記述する値を送信します。このような値は検証子と呼ばれ、二種類があります。
- 文書が最後に変更された日時である last-modified の日時
- エンティティタグまたは etag と呼ばれる、各バージョンを一意に識別する不透明な文字列。
同じリソースのバージョンの比較は少々複雑です。状況によって、二種類の確認方法があります。
- 強い検証 (Strong validation) は、ダウンロードを再開するときなど、バイト単位の同一性が求められる場合に使用します。
- 弱い検証 (Weak validation) は、ユーザーエージェントが二つのリソースが同じであることを確認することだけが必要である場合に使用します。これは広告の違いやフッターの日付の違いなど、小さな違いがある場合も含みます。
検証の種類と使用される検証子は独立しています。 Last-Modified
と ETag
はどちらの種類の検証もできますが、サーバー側の実装の複雑さは異なります。 HTTP は既定で強い検証を使用し、弱い検証を使用するときはそれを指定します。
強い検証
強い検証は、比較対象のリソースがバイト単位で同一であることを保証します。これは一部の条件ヘッダーで必須、また他のヘッダーでは既定の要件です。強い検証はとても厳密であり、サーバーレベルで保証することが困難である場合もありますが、時には性能を犠牲にしても、データが失われていないことを常に保証します。
Last-Modified
で強い検証のための一意な識別子を持つことは、とても困難です。多くの場合、リソースの MD5 ハッシュ(あるいはその派生物)を持つ ETag
を使用します。
弱い検証
弱い検証は、内容が等価であるなら二つのバージョンの文書が等しいと見なされるという点で、強い検証と異なります。例えばフッターの日付だけ、あるいは広告だけが異なる二つのページは、弱い検証では同一であるとみなされますが、強い検証では異なるものとみなされます。弱い検証を作り出す etag のシステムを構築することは、ページのさまざまな要素の重要性を知ることが伴うため複雑になるかもしれませんが、キャッシュの性能を最適化するためにとても役に立ちます。
条件ヘッダー
条件ヘッダーと呼ばれるいくつかの HTTP ヘッダーが、条件付きリクエストをもたらします。
If-Match
-
遠方のリソースの
ETag
と、このヘッダーに載せた etag が等しければ成功します。強い検証を行います。 If-None-Match
-
遠方のリソースの
ETag
と、このヘッダーに載せたそれぞれの etag が異なっていれば成功します。弱い検証を行います。 If-Modified-Since
-
遠方のリソースの
Last-Modified
の日時が、このヘッダーで指定した日時より新しければ成功します。 If-Unmodified-Since
-
遠方のリソースの
Last-Modified
の日時が、このヘッダーで指定した日時以前であれば成功します。 If-Range
-
If-Match
やIf-Unmodified-Since
に似ていますが、 etag または日時をひとつしか持つことができません。条件が失敗すると範囲指定リクエストも失敗して、206
Partial Content
レスポンスの代わりに200
OK
でリソース全体を送信します。
使用例
キャッシュの更新
条件付きリクエストのもっとも一般的な使用例は、キャッシュの更新です。キャッシュが空である、あるいはキャッシュを使用しない状態では 200
OK
ステータスと共に、要求したリソースが送信されます。
リソースと共に、ヘッダーで検証子を送信します。この例では Last-Modified
と ETag
の両方を送信していますが、どちらか一方しか使用しません。これらの検証子はリソースと共に (すべてのヘッダーのように) キャッシュへ保存され、キャッシュが陳腐化したときに条件付きリクエストを作成するために使用します。
キャッシュが陳腐化していなければ、条件付きリクエストは行いません。しかしキャッシュが陳腐化すると主に Cache-Control
ヘッダーに制御されて、クライアントはキャッシュされた値を直接使用せず、If-Modified-Since
または If-None-Match
ヘッダーに検証子の値を指定した条件付きリクエストを発行します。
リソースが変更されていなければ、サーバーは 304
Not Modified
レスポンスを返します。これはキャッシュを再び新鮮な状態にして、クライアントはキャッシュされたリソースを使用します。これはリソースをいくらか消費するレスポンスとリクエストのやり取りが発生しますが、通信網でリソース全体を再度転送するよりも効率的です。
リソースが変更された場合、サーバーは新しいバージョンのリソースを含む 200 OK
レスポンスを返します(リクエストが条件付きでなかったかのように)。
クライアントはこの新しいリソースを使用します(そしてキャッシュします)。
サーバー側で検証子を設定することをを除いて、この仕組みは透過的です。どのブラウザーもウェブ開発者が特別な作業を行うことなく、キャッシュを管理してこのような条件付きリクエストを送信します。
部分ダウンロードの整合性
ファイルの部分ダウンロードは、以前の操作を再開することが可能な HTTP の機能であり、すでに取得済みの情報を保持することによって帯域や時間を節約します。
部分ダウンロードをサポートするサーバーは、Accept-Ranges
ヘッダーを送信してそのことを知らせます。このヘッダーが送信されると、クライアントは Ranges
ヘッダーで欠落している範囲を送信することで、ダウンロードを再開できます。
この原理はシンプルですが、潜在的な問題がひとつあります。2 回のダウンロードの間にリソースが変更されると、取得した範囲が 2 つの異なるバージョンのリソースに対応してしまい、最終的な文書が壊れてしまうでしょう。
これを防ぐため、条件付きリクエストを使用します。範囲についてこれを行うための方法が 2 つあります。より柔軟な方法は If-Modified-Since
と If-Match
を使用することであり、前提条件に合わない場合はサーバーがエラーを返します。すると、クライアントはダウンロードを始めから再実行します。
この方法でも動作しますが、文書が変更されると余分なレスポンスやリクエストの交換が発生します。これはパフォーマンスを低下させますので HTTP には、この問題を避けるために特化した追加ヘッダーである If-Range
があります。
この解決策はより効率的ですが、柔軟性が若干劣ります(条件で etag をひとつしか使用できません)。ただし、これ以上の柔軟性はほとんど必要ありません。
楽観的ロックで更新が失われる問題を避ける
リモートの文書の更新は、ウェブアプリケーションで一般的な操作です。これはファイルシステムやソース管理アプリケーションではごく一般的ですが、リモートにリソースを保存できるようにするにはこのような仕組みが必要です。同様に、wiki のような一般的なウェブサイトや他の CMS でも必要です。
PUT
を使用して、この機能を実装できます。クライアントは始めに元のファイルを読み込んで、変更した後にサーバーへ送信します。
残念ながら、並行処理を考慮すると若干の間違いが出てきます。あるクライアントがリソースの新たな複製をローカルで変更している間に、第二のクライアントが同じリソースを取得して、クライアント側で同じことを行えます。これにより、とても不幸なことが発生します。両者がリソースを引き渡すと、最初のクライアントが渡した変更点が次に渡されたものによって破棄されて、第二のクライアントは新たな変更点に気づきません。誰が勝ち取ったかの結果は他者には伝わりませんが、どのクライアントの変更点が反映されるかは引き渡す速度によって変わります。またその速度はクライアントやサーバーのパフォーマンス、さらにはクライアント側で人間が文書を編集するパフォーマンスに依存します。勝ち取る者は、その時々で変わります。これは競合状態であり、検出やデバッグが難しい不確かな動作をもたらします。
2 つのクライアントの片方を困らせることなく、この問題に対処する方法はありません。しかし、更新が失われたりや競合状態になったりすることは避けるべきです。予測可能な結果や、クライアントが変更点を却下されたときに通知を受けることを望みます。
条件付きリクエストで、楽観的ロックアルゴリズム (ほとんどの wiki やソース管理システムで使用されています)を実装できます。この考え方ではすべてのクライアントに、リソースの複製の取得を許可してローカルで変更することを許可します。そして、最初のクライアントが更新内容を送信することを許可して成功させて、以降の古いバージョンになったリソースに基づく更新は拒否します。
これは If-Match
および If-Unmodified-Since
ヘッダーを使用して実装します。etag が元のファイルと一致しない、あるいはファイルが取得したときから変更されている場合は、変更点が 412
Precondition Failed
エラーで拒否されます。このエラーへの対処はクライアント次第であり、今度は最新のバージョンで再び実行するよう人間に通知する、あるいは diff を表示して変更点を維持するかを人間が選択できるように支援します。
リソースの最初のアップロードに対処する
リソースの最初のアップロードは、前述の状況の特別なケースです。リソースの更新と同様に、2 つのクライアントが同時 (あるいはほぼ同時) にアップロードしようとする競合状態を仮定します。これを防ぐために条件付きリクエストを使用できます。すべての etag を表す特別な値 '*'
を持つ If-None-Match
を追加することで、それより前のリクエストが存在しない場合に限り、リクエストが成功します。
If-None-Match
は HTTP/1.1 (およびそれ以降)に準拠するサーバーのみで動作します。サーバーが対応しているかが不明である場合は、始めに確認用の HEAD
リクエストをリソースに対して発行しなければなりません。
まとめ
条件付きリクエストは HTTP の重要な機能であり、効率的で複雑なアプリケーションの構築を可能にします。キャッシュやダウンロードの再開について、ウェブマスターに求められる作業はサーバーを適切に設定することだけです (一部の環境では正しい etag を設定することが難しいかもしれません)。また、ブラウザーが適切な条件付きリクエストを実行しますので、ウェブ開発者に求められる作業はありません
一方、ロックの仕組みでは、ウェブ開発者が適切なヘッダーを伴ってリクエストを発行しなければなりません。ウェブマスターはほとんどの場合、それらの確認をアプリケーションに頼ることができるでしょう。
どちらにせよ、条件付きリクエストはウェブの重要な機能であることは明らかです。