Как использовать If-Match и 412, чтобы API не перезаписал чужое изменение

Разбираем optimistic concurrency в API: как связать ETag и If-Match с атомарным обновлением, корректно вернуть 412 и не потерять чужую правку.

Если два клиента читают один ресурс, а затем по очереди сохраняют свои версии, второй запрос может незаметно стереть изменение первого. Защита строится так: API возвращает сильный ETag, клиент передаёт его в If-Match, а сервер сравнивает версию и выполняет запись как одну атомарную операцию.

Если ресурс успел измениться, сервер не применяет устаревшее обновление и отвечает 412 Precondition Failed. Это не повод автоматически повторять запрос: клиенту нужно заново получить ресурс, показать конфликт или безопасно объединить изменения.

Откуда берётся потерянное изменение

Представим карточку заказа. Оператор А и интеграция Б получили состояние с версией 17. Оператор изменил адрес доставки и сохранил версию 18. Интеграция всё ещё отправляет тело, подготовленное по версии 17, но меняет только комментарий.

При обычной записи «последний запрос победил» интеграция может вернуть старый адрес вместе с новым комментарием. HTTP-ответ будет успешным, хотя часть данных уже потеряна.

Условный запрос меняет правило: «примени изменение, только если ресурс всё ещё тот, который я читал». В RFC 9110 этот сценарий прямо связан с защитой от lost update при параллельной работе клиентов.

Как выглядит контракт с If-Match

Сначала клиент получает ресурс и валидатор его текущей версии:

~~~http GET /api/orders/42

HTTP/1.1 200 OK ETag: "order-42-v17" Content-Type: application/json ~~~

Затем изменяющий запрос возвращает тот же валидатор в заголовке If-Match:

~~~http PATCH /api/orders/42 If-Match: "order-42-v17" Content-Type: application/json

{"comment":"Позвонить перед доставкой"} ~~~

Сервер должен сравнить If-Match с актуальным сильным ETag до изменения ресурса. Если версия по-прежнему 17, запись выполняется, версия увеличивается, а ответ содержит новый ETag. Если уже появилась версия 18, тело запроса не применяется и сервер возвращает 412.

Проверка и запись должны быть атомарными. Схема «сначала SELECT, потом отдельный UPDATE» оставляет окно для гонки: другой запрос может изменить строку между этими действиями. Практический вариант — транзакционная блокировка либо условное обновление вида UPDATE ... WHERE id = ? AND version = ?, где отсутствие изменённой строки разбирается как возможный конфликт версии.

Что делать клиенту после 412

Правильная обработка состоит из четырёх шагов:

  1. Не отправляйте устаревшее тело повторно с новым ETag автоматически.
  2. Получите свежую версию ресурса и её ETag.
  3. Сравните пользовательские изменения с текущими данными.
  4. Объедините независимые правки или попросите пользователя выбрать нужный вариант, затем отправьте осознанное обновление с новым If-Match.

Слепой повтор особенно опасен для полной замены ресурса через PUT: старое представление может содержать много полей, которые клиент не собирался менять. PATCH уменьшает объём передаваемых изменений, но сам по себе не устраняет гонку.

Каким должен быть ETag

Для If-Match стандарт требует сильное сравнение. Слабый валидатор с префиксом W/ подходит для иных сценариев проверки представления, но не подтверждает полное отсутствие изменений, необходимое для защиты записи.

ETag можно строить на версии строки, неизменяемом идентификаторе ревизии или хеше канонического состояния. Важно не название алгоритма, а контракт:

  • валидатор меняется при каждом значимом изменении ресурса;
  • один ETag однозначно относится к одной версии;
  • все экземпляры приложения вычисляют его одинаково;
  • сравнение связано с той же записью, которую обновляет запрос;
  • новый ETag возвращается после успешного изменения.

Не включайте в ETag секреты и не полагайтесь на время приложения, если два обновления могут получить одинаковую метку. Версионный счётчик базы данных обычно проще проверять и тестировать.

Чем 412 отличается от 409

412 означает, что условие из заголовка запроса оказалось ложным: клиент предъявил версию, которая уже не актуальна. 409 Conflict шире и описывает конфликт с текущим состоянием ресурса, который пользователь потенциально может разрешить.

Если API принял If-Match и валидатор не совпал, 412 точнее объясняет причину. 409 остаётся уместным для бизнес-конфликтов, не выраженных HTTP-предусловием: например, запрещённого перехода статуса или несовместимых изменений нескольких сущностей.

Не смешивайте 412 с сетевой ошибкой и не запускайте общий retry-механизм для всех ответов 4xx. Повторы после тайм-аута решают другую задачу — защиту от дублей; она разобрана отдельно в статье о безопасном повторе API-запроса.

Минимальная матрица тестов

Проверьте не только счастливый путь:

  • актуальный If-Match разрешает изменение и возвращает новый ETag;
  • устаревший If-Match даёт 412, а данные в базе не меняются;
  • два параллельных запроса с одним ETag не могут оба успешно записать разные версии;
  • отсутствующий If-Match обрабатывается по документированному контракту, а не молча отключает защиту;
  • неподходящий валидатор другого ресурса не принимается;
  • после 412 клиент получает свежие данные и не зацикливает повтор;
  • авторизация проверяется независимо: знание ETag не даёт права менять ресурс.

В журнале полезны идентификатор ресурса, метод, request ID, ожидаемая и текущая версии. Токены, cookie, полные тела запросов и персональные данные туда попадать не должны.

Где заканчивается мониторинг

Web-Puls может регулярно проверять безопасный публичный GET- или health-endpoint и сообщать о недоступности API. Такая проверка не выполняет авторизованное редактирование и не доказывает, что защита от конкурентной записи работает: для этого нужны интеграционные тесты, наблюдаемая версия ресурса и проверка фактического состояния базы.

Сначала добавьте тест с двумя параллельными обновлениями и убедитесь, что проигравший запрос получает 412 без изменения данных. Если потерянные правки уже возникают или границу транзакции трудно определить, отправьте через форму профессиональной поддержки публичный адрес API, безопасный пример последовательности запросов и время события без токенов и персональных данных.

Проверьте свой сайт прямо сейчас

Введите адрес сайта: Web-Puls покажет HTTP-код, время ответа и базовую диагностику. Для постоянного контроля можно подключить мониторинг.

Нужна помощь с диагностикой ошибки?

Опишите симптомы в короткой заявке: URL, код ответа, время появления и что менялось перед сбоем. Специалист поддержки оценит задачу и предложит формат работ.