HTTPのIf-Match、古い編集で新しい内容を上書きしない仕組み

淡い緑の台にある差し込み口へ桃色の切り欠き付きタイルが近づき、形の異なる薄紫のタイルが横に置かれた抽象的な構図。

二人が同じ予約情報を開き、一人は会議室を、もう一人は開始時刻を直したとします。後から保存した内容が、先に行われた変更を元へ戻してしまうことがあります。通信に成功していても、編集の結果は正しく残りません。

HTTPのIf-Matchは、「読み取ったときの状態と一致する場合だけ、この変更を適用してほしい」と条件を付ける仕組みです。照合にはETagという識別値を使います。[1] 二人の編集例を通して、古い内容による上書きをどう防ぎ、どこからはアプリケーション側の判断になるのかを見ます。

簡単にまとめると?

  • 読み取った表現のETagをIf-Matchへ付けると、現在のETagとの一致を更新条件にできます。
  • 例では、一人目の更新後に古いETagで保存しようとした二人目へ412を返し、先の変更を保持します。
  • 競合を見つける機能であって、自動で編集を統合する機能ではありません。最新内容を読み直し、変更の意図を確認してから次の更新を組み立てます。

二人が同じ古い状態から編集すると

説明用のデータを、会議室がA、開始時刻が10時の予約とします。二人とも同じデータを読みました。編集者1は会議室をBへ変え、編集者2は開始時刻を11時へ変えます。どちらも、読み取ったデータ全体を編集して置き換える方式の保存だとします。

編集者1が先に保存すると、サーバー上は「B・10時」です。続いて編集者2が手元の全体を保存すると、その手元には会議室Aが残っているため、「A・11時」になります。編集者2は会議室を戻すつもりがなくても、編集者1の変更が失われました。これが、ここで扱うlost update、更新の消失です。

差分だけを送る方式なら、この特定の例では衝突を避けられるかもしれません。しかし、二人が同じ項目を変える場合や、複数項目の関係を保つ必要がある場合には、それだけで競合がなくなるわけではありません。まず「何を単位として、どの状態に対して変更したのか」を明確にする必要があります。

ETagを更新条件として返す

ETagは、サーバーが表現へ付ける識別値です。例えば読み取り応答にETag: "r1"が含まれていたなら、保存要求へIf-Match: "r1"を付けます。r1という文字列は説明用であり、連番や時刻として解釈する必要はありません。利用側で中身の意味を推測せず、返された値を扱います。[1]

例では、編集者1の保存が成功すると、サーバー上の内容は「B・10時」、ETagは”r2″へ変わります。編集者2が古い”r1″を条件に保存しようとしても、現在の”r2″とは一致しません。サーバーは変更を適用せず、この例では412 Precondition Failedを返します。「B・10時」が保持されるので、上書きによる変更の消失を避けられます。[1]

同じ初期状態から二人が保存する例。条件なしでは会議室A・11時となって会議室Bへの変更が消える。If-Matchでは二人目に412を返しB・10時を保持。読み直して統合するとB・11時となる。
独自の状態モデルで作った例。全体置換と、比較・更新を一つの不可分な処理として扱う条件付き置換を比較しました。実サーバーへの通信試験や性能測定ではありません。

412の後は、何を読み直すのか

編集者2は、手元の変更を捨てる必要があるとは限りません。まず最新データを読み、現在が「B・10時」であることを確認します。自分の意図が開始時刻だけを11時へ変えることなら、その変更を最新データへ適用して「B・11時」を作り、今度は読み直した”r2″を条件として保存できます。

この間に第三者が更新すれば、再び条件が不一致になることもあります。読み直した値を条件として送るのは、読み取り後の変更も見逃さないためです。単に古い本文をそのまま使い、ETagだけ最新へ差し替えると、例では再び「A・11時」で上書きしてしまいます。条件を通過させることと、編集意図を正しく統合することは別です。

二人とも開始時刻を変えていたなら、どちらを残すかは通信規格だけでは決まりません。利用者に差分を示す、編集時の前提を確認する、業務上の優先規則を適用する、といった判断が必要です。If-Matchは、その判断をせずに古い内容が入り込むのを止めるために使います。

似ている条件を、同じものとして扱わない

If-MatchのETag比較は強い比較です。例えばW/"r1"のような弱いETagは、見えている文字列が同じでもこの比較では一致しません。またIf-Match: *は対象の現在の表現が存在するかという条件であり、読み取った版から変わっていないかの確認にはなりません。[1]

例の予約が存在し続けるなら、*では一人目の更新前後を区別できません。「存在していればよい」のか、「自分が読んだ状態のままであってほしい」のかを先に決めると、条件の選び間違いを減らせます。

保存要求を送った後、応答が途切れる場面もあります。その場合、保存されなかったと即断して再送するのではなく、現在の内容と操作の記録を照合します。RFC 9110は、条件が不一致でも、要求された変更がすでに適用済みと見える場合に成功応答を返せる余地を定めています。したがって、412が返らなかったことだけを、初回の一回で新しく変更された証拠にはできません。[1]

導入時に確かめたい三つの境界

まず、クライアントが条件を付け、サーバーが対象の更新でその条件を評価しているかを確認します。次に、状態の比較と書き込みの間へ別の更新が割り込まないよう、実装上の不可分性を確かめます。最後に、不一致や応答不明のとき、最新内容と利用者の編集意図をどう照合するかを決めます。

今回の小さなモデルは、条件なしの上書き、古い条件の拒否、最新内容へ意図した変更だけを統合する流れを検査したものです。ネットワーク越しの実装が同じ保証を備えることまでは示しません。If-Matchというヘッダーがあるかだけでなく、二人が同時に編集する試験と、保存後の読み戻しで確かめるのが実用上の確認点です。

参考資料

  • [1] R. Fielding, M. Nottingham, J. Reschke (eds.), RFC 9110: HTTP Semantics, June 2022, STD 97。8.8.3節のETag、8.8.3.2節の比較、13.1.1節のIf-Match、15.5.13節の412を参照。予約情報の例と図、状態モデルは独自に作成しました。