ТестированиеТестирование API и интеграцийИнженер по автоматизации тестирования интеграций

Практическая ситуация: два клиента читают один ресурс, затем один из них сохраняет изменения раньше другого...

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

GET /accounts/42 HTTP/1.1

HTTP/1.1 200 OK
ETag: "v7"

PATCH /accounts/42 HTTP/1.1
If-Match: "v7"

HTTP/1.1 412 Precondition Failed
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

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

Исторический контекст

Обычная запись по идентификатору не различает актуальное и устаревшее состояние ресурса. Поэтому два клиента могли прочитать одну версию, изменить её независимо, а более поздняя запись молча затёрла результат первой.

Условные HTTP-запросы появились как общий механизм проверки версии ресурса перед операцией. Валидатором может быть, например, ETag; он позволяет серверу сравнить состояние, которое видел клиент, с состоянием на момент записи.

Постановка проблемы

Клиент A и клиент B получают ресурс с тегом "v7". Затем клиент A сохраняет изменения, и сервер выдаёт ресурсу новую версию "v8". Если клиент B отправит обычный PATCH без проверки версии, его устаревшее состояние может перезаписать изменения клиента A.

Интеграционный тест должен проверять не только код ответа, но и отсутствие побочного изменения. Успешный ответ при устаревшем If-Match означает нарушение контракта конкурентного обновления.

Подробное решение

Сервер возвращает ETag в ответе на чтение ресурса. Клиент сохраняет это значение и передаёт его в If-Match при изменении. Сервер сравнивает переданный тег с текущим тегом ресурса до применения операции.

GET /accounts/42 HTTP/1.1 HTTP/1.1 200 OK ETag: "v7" PATCH /accounts/42 HTTP/1.1 If-Match: "v7" HTTP/1.1 412 Precondition Failed

Если текущий тег уже "v8", сервер отклоняет запрос с 412 и не выполняет частичную запись. Тест должен после отказа перечитать ресурс и убедиться, что значения, записанные клиентом A, сохранились.

Проверка должна учитывать точное значение валидатора: If-Match не должен считаться успешным только потому, что совпадает часть строки или бизнес-поле. Для операций, где требуется защита от случайной перезаписи, нельзя подменять проверку версии сравнением времени, если точность и согласованность такой метки не гарантированы.

Такой контракт защищает от потерянного обновления, но не решает конфликты автоматически. Клиенту нужно получить свежую версию, повторно применить свои изменения или показать конфликт пользователю. При массовых обновлениях также важно определить, распространяется ли проверка версии на весь ресурс или на отдельные поля.

Ситуация из практики

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

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

Что кандидаты часто упускают

1. Достаточно ли проверить только ответ 412?

Нет. Нужно проверить инвариант отсутствия мутации: после отказа ресурс должен иметь прежнее актуальное состояние, а связанные события или побочные операции не должны быть опубликованы. Иначе сервер может вернуть правильный статус уже после частичного применения изменения.

2. Чем отличается If-Match от If-None-Match в этом сценарии?

If-Match подтверждает, что клиент работает с конкретной известной версией, поэтому подходит для защиты записи от устаревшего состояния. If-None-Match обычно применяют для условного получения или создания, когда операция допустима только при отсутствии совпадающей версии. Замена одного заголовка другим меняет смысл условия и может не защитить обновление от потери данных.

3. Можно ли считать HTTP-контракт выполненным, если сервер возвращает 409 Conflict вместо 412?

Это зависит от заранее определённого контракта. Для отказа именно из-за невыполненного условного заголовка стандартным семантическим ответом является 412 Precondition Failed; 409 может описывать конфликт состояния приложения, но не должен использоваться случайно. Интеграционный тест обязан фиксировать выбранную семантику: статус, формат ошибки, отсутствие мутации и возможность безопасно получить актуальную версию ресурса.