HTTP には条件付きリクエストの概念があり、対象となるリソースと検証子の値とを比較することで、リクエストの結果や、成功か失敗かまでもが変化することがあります。このようなリクエストは、キャッシュの内容を検証して、無用な制御を避けたり、ダウンロードの再開の時などに文書の整合性を検証したり、サーバー上の文書をアップロードまたは変更するときに更新内容を失うことを避ける場合などに役立つことがあります。

原理

HTTP 条件付きリクエストは、特定のヘッダーの値に応じて異なる処理が行われるリクエストです。これらのヘッダーは前提条件を定義しており、リクエストの結果は前提条件に一致するか否かに応じて変わります。

リクエストで使用したメソッドや前提条件で使用したヘッダー一式によって、さまざまな動作が定義されています。

  • GET などの安全なメソッドは、一般に文書を取得するメソッドであり、条件付きリクエストは関連する文書のみを返信するために利用することができます。これによって、帯域を節約します。
  • PUT などの安全ではないメソッドは、一般に文書をアップロードするメソッドであり、条件付きリクエストは文書がサーバーに格納されているものと同じものに基づいたものである場合に限ってアップロードするようにするために利用することができます。

検証子

すべての条件ヘッダーは、サーバーに保存されているリソースが特定のバージョンに一致するか確認を試みます。このため、条件付きリクエストではリソースのバージョンを示す必要があります。リソース全体をバイト単位で比較することは不可能であり、常に望まれていることとは限らないので、リクエストはバージョンを記述する値を送信します。このような値は検証子と呼ばれ、二種類があります。

  • 文書が最後に変更された日時である last-modified の日時
  • エンティティタグまたは etag と呼ばれる、各バージョンを一意に識別する不透明な文字列。

同じリソースのバージョンの比較は少々複雑です。状況によって、二種類の確認方法があります。

  • 強い検証 (Strong validation) は、ダウンロードを再開するときなど、バイト単位の同一性が求められる場合に使用します。
  • 弱い検証 (Weak validation) は、ユーザーエージェントが二つのリソースが同じであることを確認することだけが必要である場合に使用します。これは広告の違いやフッターの日付の違いなど、小さな違いがある場合も含みます。

検証の種類と使用される検証子は独立しています。 Last-ModifiedETag はどちらの種類の検証もできますが、サーバー側の実装の複雑さは異なります。 HTTP は既定で強い検証を使用し、弱い検証を使用するときはそれを指定します。

強い検証

強い検証は、比較対象のリソースがバイト単位で同一であることを保証します。これは一部の条件ヘッダーで必須、また他のヘッダーでは既定の要件です。強い検証はとても厳密であり、サーバーレベルで保証することが困難である場合もありますが、時には性能を犠牲にしても、データが失われていないことを常に保証します。

Last-Modified で強い検証のための一意な識別子を持つことは、とても困難です。多くの場合、リソースの MD5 ハッシュ (あるいはその派生物) を持つ ETag を使用します。

弱い検証

弱い検証は、内容が等価であるなら二つのバージョンの文書が等しいと見なされるという点で、強い検証と異なります。例えばフッターの日付だけ、あるいは広告だけが異なる二つのページは、弱い検証では同一であるとみなされますが、強い検証では異なるものとみなされます。弱い検証を作り出す 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-MatchIf-Unmodified-Since に似ていますが、 etag または日時をひとつしか持つことができません。条件が失敗すると範囲指定リクエストも失敗して、 206 Partial Content レスポンスの代わりに 200 OK でリソース全体を送信します。

使用例

キャッシュの更新

条件付きリクエストのもっとも一般的な使用例は、キャッシュの更新です。キャッシュが空である、あるいはキャッシュを使用しない状態では 200 OK ステータスと共に、要求したリソースが送信されます。

The request issued when the cache is empty triggers the resource to be downloaded, with both validator value sent as headers. The cache is then filled.

リソースと共に、ヘッダーで検証子を送信します。この例では Last-ModifiedETag の両方を送信していますが、どちらか一方しか使用しません。これらの検証子はリソースと共に (すべてのヘッダーのように) キャッシュへ保存され、キャッシュが陳腐化したときに条件付きリクエストを作成するために使用します。

キャッシュが陳腐化していなければ、条件付きリクエストは行いません。しかしキャッシュが陳腐化すると主に Cache-Control ヘッダーに制御されて、クライアントはキャッシュされた値を直接使用せず、If-Modified-Since または If-Match ヘッダーに検証子の値を指定した条件付きリクエストを発行します。

リソースが変更されていなければ、サーバーは 304 Not Modified レスポンスを返します。これはキャッシュを再び新鮮な状態にして、クライアントはキャッシュされたリソースを使用します。これはリソースをいくらか消費するレスポンスとリクエストのやり取りが発生しますが、通信網でリソース全体を再度転送するよりも効率的です。

With a stale cache, the conditional request is sent. The server can determine if the resource changed, and, as in this case, decide not to send it again as it is the same.

リソースが変更された場合は、サーバーは条件付きではないリクエストと同様に 200 OK レスポンスで新しいバージョンのリソースを送信します。そして、クライアントは新しいリソースを使用します (また、それをキャッシュします)。

In the case where the resource was changed, it is sent back as if the request wasn't conditional.

サーバー側で検証子を設定することをを除いて、この仕組みは透過的です。どのブラウザーもウェブ開発者が特別な作業を行うことなく、キャッシュを管理してこのような条件付きリクエストを送信します。

部分ダウンロードの整合性

ファイルの部分ダウンロードは、以前の操作を再開することが可能な HTTP の機能であり、すでに取得済みの情報を保持することによって帯域や時間を節約します。

A download has been stopped and only partial content has been retrieved.

部分ダウンロードをサポートするサーバーは、Accept-Ranges ヘッダーを送信してそのことを知らせます。このヘッダーが送信されると、クライアントは Ranges ヘッダーで欠落している範囲を送信することで、ダウンロードを再開できます。

The client resumes the requests by indicating the range he needs and preconditions checking the validators of the partially obtained request.

この原理はシンプルですが、潜在的な問題がひとつあります。2 回のダウンロードの間にリソースが変更されると、取得した範囲が 2 つの異なるバージョンのリソースに対応してしまい、最終的な文書が壊れてしまうでしょう。

これを防ぐため、条件付きリクエストを使用します。範囲についてこれを行うための方法が 2 つあります。より柔軟な方法は If-Modified-SinceIf-Match を使用することであり、前提条件に合わない場合はサーバーがエラーを返します。すると、クライアントはダウンロードを始めから再実行します。

When the partially downloaded resource has been modified, the preconditions will fail and the resource will have to be downloaded again completely.

この方法でも動作しますが、文書が変更されると余分なレスポンスやリクエストの交換が発生します。これはパフォーマンスを低下させますので HTTP には、この問題を避けるために特化した追加ヘッダーである If-Range があります。

The If-Range headers allows the server to directly send back the complete resource if it has been modified, no need to send a 412 error and wait for the client to re-initiate the download.

この解決策はより効率的ですが、柔軟性が若干劣ります (条件で etag をひとつしか使用できません)。ただし、これ以上の柔軟性はほとんど必要ありません。

楽観的ロックでロストアップデートを避ける

リモートの文書の更新は、ウェブアプリケーションで一般的な操作です。これはファイルシステムやソース管理アプリケーションではごく一般的ですが、リモートにリソースを保存できるようにするにはこのような仕組みが必要です。同様に、wiki のような一般的なウェブサイトや他の CMS でも必要です。

PUT を使用して、この機能を実装できます。クライアントは始めに元のファイルを読み込んで、変更した後にサーバーへ送信します。

Updating a file with a PUT is very simple when concurrency is not involved.

残念ながら、並行処理を考慮すると若干の間違いが出てきます。あるクライアントがリソースの新たな複製をローカルで変更している間に、第二のクライアントが同じリソースを取得して、クライアント側で同じことを行えます。これにより、とても不幸なことが発生します。両者がリソースを引き渡すと、最初のクライアントが渡した変更点が次に渡されたものによって破棄されて、第二のクライアントは新たな変更点に気づきません。誰が勝ち取ったかの結果は他者には伝わりませんが、どのクライアントの変更点が反映されるかは引き渡す速度によって変わります。またその速度はクライアントやサーバーのパフォーマンス、さらにはクライアント側で人間が文書を編集するパフォーマンスに依存します。勝ち取る者は、その時々で変わります。これは競合状態であり、検出やデバッグが難しい不確かな動作をもたらします。

When several clients update the same resource in parallel, we are facing a race condition: the slowest win, and the others don't even know they lost. Problematic!

2 つのクライアントの片方を困らせることなく、この問題に対処する方法はありません。しかし、ロストアップデートや競合状態は避けるべきです。予測可能な結果や、クライアントが変更点を却下されたときに通知を受けることを望みます。

条件付きリクエストで、楽観的ロックアルゴリズム (ほとんどの wiki やソース管理システムで使用されています) を実装できます。この考え方ではすべてのクライアントに、リソースの複製の取得を許可してローカルで変更することを許可します。そして、最初のクライアントが更新内容を送信することを許可して成功させて、以降の古いバージョンになったリソースに基づく更新は拒否します。

Conditional requests allow to implement optimistic locking: now the quickest wins, and the others get an error.

これは If-Match および If-Unmodified-Since ヘッダーを使用して実装します。etag が元のファイルと一致しない、あるいはファイルが取得したときから変更されている場合は、変更点が 412 Precondition Failed エラーで拒否されます。このエラーへの対処はクライアント次第であり、今度は最新のバージョンで再び実行するよう人間に通知する、あるいは "diff" を表示して変更点を維持するかを人間が選択できるように支援します。

リソースの最初のアップロードに対処する

リソースの最初のアップロードは、前述の状況の特別なケースです。リソースの更新と同様に、2 つのクライアントが同時 (あるいはほぼ同時) にアップロードしようとする競合状態を仮定します。これを防ぐために条件付きリクエストを使用できます。すべての etag を表す特別な値 '*' を持つ If-None-Match を追加することで、それより前のリクエストが存在しない場合に限り、リクエストが成功します。

Like for a regular upload, the first upload of a resource is subject to a race condition: If-None-Match can prevent it.

If-None-Match は HTTP/1.1 (およびそれ以降) に準拠するサーバーのみで動作します。サーバーが対応しているかが不明である場合は、始めに確認用の HEAD リクエストをリソースに対して発行しなければなりません。

まとめ

条件付きリクエストは HTTP の重要な機能であり、効率的で複雑なアプリケーションの構築を可能にします。キャッシュやダウンロードの再開について、ウェブマスターに求められる作業はサーバーを適切に設定することだけです (一部の環境では正しい etag を設定することが難しいかもしれません)。また、ブラウザーが適切な条件付きリクエストを実行しますので、ウェブ開発者に求められる作業はありません

一方、ロックの仕組みでは、ウェブ開発者が適切なヘッダーを伴ってリクエストを発行しなければなりません。ウェブマスターはほとんどの場合、それらの確認をアプリケーションに頼ることができるでしょう。

どちらにせよ、条件付きリクエストはウェブの重要な機能であることは明らかです。

ドキュメントのタグと貢献者

このページの貢献者: mfuji09, kachick, SphinxKnight, yyss
最終更新者: mfuji09,