HTTP には条件付きリクエストの概念があり、影響を受けるリソースと検証子の値を比較することによって、リクエストが成功した場合でもリクエストの結果を変更できます。このようなリクエストはダウンロードを再開するとき、あるいはサーバー上のドキュメントでアップロードや変更を行うときに更新内容を失うことを避けるときなどに、ドキュメントの整合性を確認するためにキャッシュの内容を検証して無駄な制御を避けるために役立ちます。
原理
HTTP 条件付きリクエストは、特定のヘッダーの値に応じて処理が異なるリクエストです。これらのヘッダーは前提条件を定義しており、リクエストの結果は前提条件に一致するか否かに応じて変わります。
リクエストで使用したメソッドや前提条件で使用したヘッダー一式によって、さまざまな動作が定義されます:
GET
などの 安全な メソッドは一般的にドキュメントを取得するメソッドであり、適切なドキュメントのみ送信して帯域を節約するために、条件付きリクエストを使用できます。PUT
などの 安全ではない メソッドは一般的にドキュメントをアップロードするメソッドであり、サーバーに保存されているデータと同じデータを基にした場合に限りドキュメントをアップロードするために、条件付きリクエストを使用できます。
検証子
すべての条件ヘッダーは、サーバーに保存されているリソースが特定のバージョンに一致するかの確認を試みます。このために、条件付きリクエストではリソースのバージョンを示さなければなりません。リソース全体をバイト単位で比較することは不可能です (また、常にそれが望まれているわけではありません!) ので、リクエストではバージョンを示す値を送信します、このような値は検証子と呼ばれ、2 種類あります:
- ドキュメントが最後に変更された日時である last-modified。
- entity tag または etag と呼ばれる、それぞれのバージョンを一意に示す不透明な文字列。
同じリソースのバージョンの確認は若干複雑です。状況に応じて使い分ける、2 種類の確認方法があります。強い検証 (Strong validation) はダウンロードを再開するときなど、バイト単位の同一性を望む場合に使用します。弱い検証 (Weak validation) はユーザーエージェントが確認しなければならないことが、小さな違い (広告の違いや日付が異なるフッターなど) があったとしても 2 つのリソースが同じ内容であるかのみであるときに使用します。
検証の種類と使用する検証子は独立しています。サーバー側で実装するための複雑さは多様ですが、Last-Modified
と ETag
のどちらも両方の検証ができます。HTTP はデフォルトで強い検証を使用しており、弱い検証を使用できるときはそれを明示します。
強い検証
強い検証は、比較対象のリソースがバイト単一で同一であることを保証します。これは一部の条件ヘッダーで必須、また他のヘッダーではデフォルトの要件です。強い検証はとても厳格でありサーバーレベルで保証することが困難であるかもしれませんが、時にはパフォーマンスを犠牲にしても、データが失われていないことを常に保証します。
Last-Modified
で強い検証のための一意な識別子を持つことはとても困難です。多くの場合、リソースの MD5 ハッシュ (あるいはその派生物) を持つ ETag
を使用します。
弱い検証
弱い検証は、2 つのバージョンのドキュメントの内容が同じかという同一性を判断することが、強い検証と異なります。例えばフッターの日付だけ、あるいは広告だけが異なる 2 つのページは、弱い検証では同一であるとみなしますが、強い検証では異なるとみなします。弱い検証を作り出す etag のシステムを構築することは、ページのさまざまな要素の重要性を知ることが伴うため複雑になるかもしれませんが、キャッシュのパフォーマンスを最適化するためにとても役に立ちます。
条件ヘッダー
条件ヘッダーと呼ばれるいくつかの HTTP ヘッダーが、条件付きリクエストをもたらします。
If-Match
- 遠方のリソースの
ETag
と、このヘッダーに載せた etag が等しければ成功します。デフォルトでは etag に接頭辞'W/'
がついていない限り、強い検証を行います。 If-None-Match
- 遠方のリソースの
ETag
と、このヘッダーに載せたそれぞれの etag が異なっていればば成功します。デフォルトでは etag に接頭辞'W/'
がついていない限り、強い検証を行います。 If-Modified-Since
- 遠方のリソースの
Last-Modified
の日時が、このヘッダーで指定した日時より新しければ成功します。 If-Unmodified-Since
- 遠方のリソースの
Last-Modified
の日時が、このヘッダーで指定した日時より過去または同一であれば成功します。 If-Range
If-Match
やIf-Unmodified-Since
に似ていますが、etag または日時をひとつしか持つことができません。条件が失敗すると range リクエストも失敗して、206
Partial Content
レスポンスの代わりに200
OK
でリソース全体を送信します。
使用例
キャッシュの更新
条件付きリクエストのもっとも一般的な使用例は、キャッシュの更新です。キャッシュが空である、あるいはキャッシュを使用しない状態では 200
OK
ステータスと共に、要求したリソースが送信されます。
リソースと共に、ヘッダーで検証子を送信します。この例では Last-Modified
と ETag
の両方を送信していますが、どちらか一方しか使用しません。これらの検証子はリソースと共に (すべてのヘッダーのように) キャッシュへ保存され、キャッシュが陳腐化したときに条件付きリクエストを作成するために使用します。
キャッシュが陳腐化していなければ、条件付きリクエストは行いません。しかしキャッシュが陳腐化すると主に Cache-Control
ヘッダーに制御されて、クライアントはキャッシュされた値を直接使用せず、If-Modified-Since
または If-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 を設定することが難しいかもしれません)。また、ブラウザーが適切な条件付きリクエストを実行しますので、ウェブ開発者に求められる作業はありません
一方、ロックの仕組みでは、ウェブ開発者が適切なヘッダーを伴ってリクエストを発行しなければなりません。ウェブマスターはほとんどの場合、それらの確認をアプリケーションに頼ることができるでしょう。
どちらのケースでも、条件付きリクエストはウェブの重要な機能です。