HTTP 428と412の違い――If-Matchで同時更新の上書きを防ぐ

ネットワーク・RFCカテゴリを表すパンダのイラスト ネットワーク・RFC

この記事について
この記事は、生成AIを活用した自動生成フローで作成しています。RFC 6585の428、RFC 9110のIf-Match・ETag・412の定義を2026年10月28日に再確認し、同時編集の上書き事故を防ぐ条件分岐をPython 3.12.14の状態モデルで検算しました。実在するAPIへの更新要求は実行していません。

検証ステータス:🧪 公式仕様・状態モデル検算・同一実行内の5役セルフQA完了(外部API/WordPress未確認)
情報確認日:2026年10月28日(JST)

HTTP 428と412の違い――If-Matchで同時更新の上書きを防ぐ

同じ文書を2人が編集するAPIで、後から保存した人の内容が先の人の変更を消してしまうことがあります。HTTPの If-Match を使えば、「自分が読み取った版のままである場合に限り更新する」という条件をリクエストへ付けられます。サーバーが条件付き更新を必須にしているのに条件を付けなければ 428 Precondition Required、付けた条件が現在の状態と合わなければ通常は 412 Precondition Failed です。

この違いは、単にエラーコードを覚える話ではありません。クライアントが「どの版を読んだのか」を記録し、サーバーが更新直前に照合するという、データを失わないための協力関係を理解することが重要です。

まず、更新が失われる場面を考える

架空の文書APIに、共有メモの内容が「初期案」として保存されているとします。AさんとBさんがほぼ同時にGETし、同じ内容を受け取ります。Aさんが「Aの修正」を保存した後、Bさんが古い「初期案」を元にした「Bの修正」をPUTすると、無条件に上書きする設計ではAさんの変更が消える可能性があります。これが lost update(更新の喪失) です。

たとえば文書の本文だけをPUTで丸ごと置き換えるAPIでは、BさんがAさんの変更を知らないまま古い内容から編集した結果を送信すると、後勝ちになりがちです。HTTP自体がすべてのAPIの競合を自動的に防ぐわけではありません。APIが適切な条件付き更新を実装し、クライアントもその条件を送る必要があります。

sequenceDiagram
    participant A as 編集者A
    participant B as 編集者B
    participant S as 文書API
    A->>S: GET /documents/42
    S-->>A: 200 + ETag "v7"
    B->>S: GET /documents/42
    S-->>B: 200 + ETag "v7"
    A->>S: PUT + If-Match "v7"
    S-->>A: 200 + 新ETag "v8"
    B->>S: PUT + If-Match "v7"
    S-->>B: 412 Precondition Failed
    Note over B,S: Bの古い版による上書きを防止

図のETag文字列は説明用の架空値です。実際のETagがv7、v8という連番になるという意味ではありません。サーバーは内部の版管理、表現の内容などに基づいて識別子を生成しますが、クライアントが値の生成規則を推測する必要はありません。

GETでETagを受け取り、If-Matchで返す

ETag は、サーバーが選択したリソース表現の版を識別するためのHTTPフィールドです。GETの応答でETagが提供された場合、クライアントはその値を保持できます。ETagは「暗号化されたデータ」でも「ユーザー認証用のトークン」でもありません。内容が同じかを比較するための検証子(validator)です。

架空の最初のGET応答を次のように考えます。

HTTP/1.1 200 OK
Content-Type: application/json
ETag: "v7"

{"title":"共有メモ","text":"初期案"}

Aさんは編集後、GETで受け取ったETagを If-Match ヘッダーにそのまま指定します。次は通信の形を示す例で、実行済みのAPIログではありません。

PUT /documents/42 HTTP/1.1
Host: example.com
Content-Type: application/json
If-Match: "v7"

{"title":"共有メモ","text":"Aの修正"}

サーバーは、更新処理を適用する直前に現在の表現とIf-Matchの条件を比較します。現在の版が一致すれば更新を進められます。Aさんの更新後に版がv8へ変わっていれば、Bさんが古いv7を指定した更新は条件不一致になります。

重要なのは、クライアントが先にGETしたこと自体は安全性を保証しない という点です。GETの後からPUTまでの間に別の更新が入る可能性があります。条件の確認と実際の変更をサーバー側で競合が起きないように扱って初めて意味があります。アプリが「確認した直後に別の処理が書き換えた」という競合を許してしまえば、If-Matchを使っていても目的を達成できません。

この前提条件の評価順序は RFC 9110の条件付きリクエスト に定義されています。

428は「条件が必要」、412は「条件が不一致」

似た番号でも、原因と利用者の次の行動が違います。

応答主な意味次に行うこと
428 Precondition Requiredサーバーが条件付き要求を必要としている最新版を取得し、必要な条件を付ける
412 Precondition Failed指定した前提条件が偽になった最新版と自分の変更を比較する
409 Conflictリソースの現在状態と処理が競合するAPIが返す競合理由と解決手順を確認する

条件を付け忘れた場合:428

サーバーが「この文書を更新するときは必ずIf-Matchを指定する」という方針なら、条件なしのPUTを拒否できます。

PUT /documents/42 HTTP/1.1
Host: example.com
Content-Type: application/json

{"title":"共有メモ","text":"変更案"}
HTTP/1.1 428 Precondition Required
Content-Type: application/json

{"error":"If-Match is required"}

この応答は「ETagが古い」という意味ではありません。条件付きにしてほしいのに、必要な条件がない という状態です。RFC 6585は、428を受けたクライアントがどう再送すればよいか説明することを推奨しています。また428応答はキャッシュに保存してはいけないと定めています。

ただし428の使用はすべてのサーバーに義務付けられているわけではありません。APIが無条件の更新を許可している場合、同じ要求を受け付けることもあります。「428が返らないから競合防止が実装済み」と判断するのは誤りです。

根拠:RFC 6585 §3 — 428 Precondition Required。

古い条件を付けた場合:412

Aさんの更新で現在のETagがv8になった後、Bさんがv7を条件にPUTすると、現在の表現と一致しません。

PUT /documents/42 HTTP/1.1
Host: example.com
Content-Type: application/json
If-Match: "v7"

{"title":"共有メモ","text":"Bの修正"}
HTTP/1.1 412 Precondition Failed

412は「サーバーが評価した条件の少なくとも1つが偽だった」ことを示します。通常の競合場面では、サーバーは更新を適用せず、クライアントに現在の版を確認させます。ただしRFC 9110には、状態変更要求がすでに成功したと判定できる場合の例外もあるため、412以外の応答なら必ず新規更新が実行された といった推測は避けます。

根拠:RFC 9110 §13.1.1 — If-Match、§15.5.13 — 412。

If-Matchで重要なのは「強い比較」

ETagには、引用符で囲まれた通常のタグのほか、先頭にW/が付く 弱いETag があります。弱いETagは表現が意味的に等価であることを示す用途などに使われますが、バイト単位の一致を要求する用途とは区別されます。

If-Matchの判定では 強い比較 を使用します。したがって、次の2つはIf-Matchで一致しません。

ETag: "v7"
If-Match: W/"v7"

見た目の識別文字列が同じでも、W/が付いたタグを強いETagと同一視できません。APIの更新制御にIf-Matchを使うなら、GETで返すETagの強弱と、サーバーの比較実装を確かめる必要があります。If-Matchに弱いETagを渡して412になったとき、単純に引用符だけを取り除く、W/を勝手に削除する、といった加工はしません。現在の表現を再取得し、APIが指定する正しい検証子を使います。

なお If-Match: * は「現在の表現が存在する」という条件を表せますが、特定の版と一致することは要求しません。存在確認と同時編集の版衝突検出は別問題です。更新の喪失を防ぐ目的で安易にアスタリスクへ置き換えるべきではありません。

根拠:RFC 9110 §8.8.3 — ETag、§13.1.1 — If-Match。

412を受けたら「同じPUTを再送」しない

412が出たときに危険なのは、競合を解消せず条件だけを最新ETagへ付け替え、同じ古い本文を送り直すことです。これでは本来守りたかったAさんの変更を、意図せず上書きするおそれがあります。

安全な業務フローでは、まずGETで最新版とそのETagを取得します。次に自分が編集した内容との差分を確認し、変更の衝突があるなら人に選択させるか、APIが定義するマージ処理を使います。マージ後の内容が確定した時点で、取得した最新版のETagを付けて更新します。更新の成功応答を受け取っても、APIが返す新しい版情報を保存し、必要なら読み戻して確認します。

たとえばAさんが「承認者」を変更し、Bさんが「説明文」を変更した場合、両方の修正を残せるかもしれません。一方、同じ「承認者」フィールドを別々の値に変更していたら、自動で片方を選ぶのは危険です。412は競合を検出するための入口であり、正しいマージ方針まで決める仕組みではありません。

409や304とは何が違う?

409 Conflict は、現在のリソース状態との競合により要求を完了できないときの、より一般的な応答です。たとえば業務ルール上の状態遷移が許されない場合など、ETagの一致とは別の理由で使われます。どのステータスを返すかはAPI設計にも依存しますが、「If-Matchの前提条件が偽」という具体的なケースには412が対応します。

304 Not Modified は、主としてキャッシュの再検証で使われる応答です。たとえばGETでIf-None-Matchを使う場合の意味と、PUTでIf-Matchを使う場合の意味を混同しないようにします。ETagという共通の検証子を利用しても、ヘッダーとメソッドの組み合わせによって目的は変わります。

「ETagがあるから常に304」「古いETagだから428」という覚え方では障害を切り分けられません。どのHTTPメソッドで、どの条件ヘッダーを送ったか を先に確かめるのが近道です。

実務でAPIを設計するときの確認事項

共有文書や設定管理のような更新APIなら、条件なしのPUTを許すか、428で拒否するかをAPI契約として明記します。条件付き更新を採用するなら、GET応答で利用可能なETagを返し、PUT時にはIf-Matchを検査し、更新が成立したら次の版情報を返す設計が考えられます。更新と比較が一体として成立すること、データベース更新時の競合が漏れないことも確認が必要です。

障害調査では、次の順序で記録を整理すると原因が見えます。

  1. 更新前のGETでどのETagを受け取ったか。

  2. 更新要求にIf-Matchが付いていたか。

  3. 送信した値は強いETagか、弱いETagか。

  4. 応答が428・412・409のどれだったか。

  5. 同時刻に別の更新が存在したか。

  6. 更新失敗後に古い本文を自動再送していないか。

運用ログへETagや文書内容を無条件に大量記録する必要はありません。機微情報を含むAPIでは、相関ID、ステータス、匿名化した版識別情報など必要最小限の情報を残し、認証情報や文書本文をログへ出さない方針を採ります。ETagは認証・認可の代替ではなく、アクセス権の検査は別途必要です。

また、HTTPの条件付き更新は、複数リソースをまたぐ業務トランザクション全体の整合性までは保証しません。1件の文書を安全に更新できても、別の文書との相互関係、通知の重複、外部システムへの副作用などはアプリ側の設計課題として残ります。

1か所変えて考える

最初の例でBさんのIf-Matchをv7からv8へ変えれば、現在の版と一致する条件になります。ただし、Bさんの本文を編集し直さずに条件だけを変えてはいけません。これは条件の意味を理解するための思考実験であり、推奨する再試行手順ではありません。

もう一つ、If-Matchそのものを削除するとどうなるでしょうか。条件付き更新を必須とするAPIなら428が想定されますが、無条件更新を許すAPIでは上書きが成功する可能性があります。つまり428は「条件を要求するサーバーの方針」、412は「送った条件の評価結果」という別の段階を表しています。

状態モデルで確認した境界値

2026年10月28日に、本文の分岐をPython 3.12.14のインメモリ状態モデルで再検算しました。これは実在サービスへの通信試験ではなく、ETagとIf-Matchの条件だけを切り出した確認です。

条件期待した結果観測
条件必須の更新でIf-Matchなし428、本文は不変一致
現在の強いETag "v7"200、本文を更新一致
更新後に古い "v7" を再利用412、先行更新を保持一致
弱いETag W/"v8"412、本文は不変一致
最新版を再取得し "v8" で更新200、マージ後本文へ更新一致
サーバーが条件を必須にしない無条件更新を受理し得る一致

6ケースはすべて期待どおりでした。ただし、HTTPサーバー、データベースの原子的更新、プロキシ、実在APIのETag生成規則までは確認していません。モデルの成功を、そのまま対象システムの実装保証には使えません。

まとめ

HTTPの428は条件付き要求が必要であることを示し、412は送った条件が満たされなかったことを示します。If-Matchと強いETagを使えば、読んだ版が変わっていないときだけ更新する設計を実現できます。ただし、競合後のマージ、認可、複数リソースの整合性まで自動解決するわけではありません。

更新が競合したら、条件を外して再送するのではなく、最新版を取得して差分を確認する。この一手間が、HTTPのステータスコードを実際のデータ保護へつなげます。

公式情報・一次情報

※通信例は説明用に構成したものです。Pythonの状態モデル検算以外に、実在サービスへの更新試験、レスポンス採取、WordPress描画確認は行っていません。

文書情報

記事タイトル
HTTP 428と412の違い――If-Matchで同時更新の上書きを防ぐ
作成日
更新日
Source URL
https://papanda925.com/?p=18251

ライセンス: 本記事のうち、当サイトが権利を有する本文・自作図表は、特記なき限り CC BY 4.0 で利用できます。生成AIを活用して作成・編集した内容を含みます。コードについて、別途ライセンス表示またはリンク先GitHubリポジトリのライセンスがある場合は、その条件を優先します。引用・第三者資料・画像・商標等は本ライセンスの対象外です。 利用ポリシー

タイトルとURLをコピーしました