Для AI-агентов: markdown этой страницы — /docs/etag-and-idempotency.mdиндекс документации — /llms.txt
ETag и безопасный повтор запросов
Обновлено
Перед изменением прочитайте объект и сохраните его ETag. Для одного намерения создайте один Idempotency-Key; при потерянном ответе повторите исходный запрос с этим же ключом и телом. ETag защищает от правки устаревшей версии, а ключ отличает повтор принятой команды от новой команды.
Это правила управления доступным бизнесом через Hub. Номер объекта остаётся адресом P3 внутри бизнеса, а права определяются credential, scopes и текущими полномочиями. Идентификаторы, авторизация, права.
Чтение и conditional GET
В деталях каталога ETag находится в HTTP header ETag и в editable_config.etag. Передавайте значение целиком, включая W/ и кавычки. Не вычисляйте его самостоятельно по времени или названию. Список editable_config.editable_fields показывает разрешённые поля; наличие поля в read-ответе ещё не разрешает его изменять.
Для повторного чтения detail используйте If-None-Match с сохранённым ETag. На этом маршруте совпадение возвращает HTTP 304 с пустым телом; клиент сохраняет предыдущий объект. Отсутствие совпадения возвращает обычный detail и актуальный ETag. Такой polling не меняет объект и не требует idempotency key.
GET <HUB_BASE_URL>/api/v1/catalog/7?zone=ru HTTP/1.1
X-Api-Key: <API_KEY>
If-None-Match: W/"57f60dd745c6591b"При совпадении версии:
HTTP/1.1 304 Not Modified
ETag: W/"57f60dd745c6591b"If-None-Match для чтения и If-Match для изменения имеют разные задачи. Не переносите это поведение на маршруты, в которых conditional headers не предусмотрены.
Пример: изменить название товара и повторить команду
Результат — новое название одного товара без изменения цены, активности или публичности. Нужны доступный бизнес, подключённый бот и разрешённая операция каталога: catalog:read для чтения и catalog:write для изменения. REST PATCH каталога использует API-key guard; Bearer здесь должен быть credential, поддерживаемым этим guard, а не произвольной customer/surface сессией. Для своего агента используйте фактически доступный typed MCP tool и его schema. Способ входа не даёт дополнительных прав.
<HUB_BASE_URL> — адрес вашего Hub без завершающего /, <API_KEY> — локальный секрет. Предполагается, что вы выбрали предназначенный для правки товар №7 и прочитали его в zone=ru. Ниже использованы обезличенные значения boundary fixture; это описание контракта, а не подтверждение живого сервера или разрешение редактировать чужой товар.
Прочитайте
GET /api/v1/catalog/7?zone=ru, проверьте бизнес, название иeditable_config.editable_fields.Для намерения «переименовать в Changed» сохраните исходный ETag
W/"57f60dd745c6591b", тело и ключcatalog-change.Отправьте следующий запрос. Цена, деньги, отправка сообщений и публикация этим телом не запрашиваются.
PATCH <HUB_BASE_URL>/api/v1/catalog/7?zone=ru HTTP/1.1
X-Api-Key: <API_KEY>
Content-Type: application/json
If-Match: W/"57f60dd745c6591b"
Idempotency-Key: catalog-change
{"title":"Changed"}Успех, HTTP 200, header ETag: W/"bc0efc7576cb13b2". Полный detail из сохранённой квитанции команды одинаков при первом успехе и точном повторе. Текущие available_providers доступны при GET, а PATCH возвращает зафиксированный результат:
{
"seq_id":7,
"title":"Changed",
"title_alt":"Course EN",
"description":"Frozen course",
"description_alt":"Frozen EN",
"is_active":true,
"is_public":false,
"is_free":false,
"groups":[],
"editable_config":{
"etag":"W/\"bc0efc7576cb13b2\"",
"editable_fields":["title","title_alt","description","mediaRefs","description_alt","is_active","is_public","prices","item_type","sku_code","measurement_unit","requires_postal_code","booking","trial","variants","ru_vat_rate","ru_price_includes_tax","intl_tax_rate_percent","intl_price_includes_tax"]
},
"prices":[
{"currency":"RUB","amount":"125.50","trial_amount":null},
{"currency":"USD","amount":"2.50","trial_amount":null},
{"currency":"EUR","amount":null,"trial_amount":null}
],
"item_type":"product",
"sku_code":null,
"measurement_unit":"шт",
"requires_postal_code":false,
"booking":{"enabled":false,"mode":"hourly","duration_value":null,"buffer_minutes":0,"resource_seq_nums":[12]},
"trial":null,
"deeplink_code":"ProductCode12345",
"ru_vat_rate":null,
"ru_price_includes_tax":null,
"intl_tax_rate_percent":null,
"intl_price_includes_tax":null,
"variants":null,
"updated_at":"2026-10-01T10:01:00.000Z"
}Если ответ потерялся, повторите исходный PATCH с
catalog-change, исходнымIf-Matchи тем же телом. Каталог сначала ищет квитанцию этой принятой команды: точный повтор возвращает сохранённый результат, даже если исходный ETag уже устарел. Он не запускает вторую правку и не заменяет исторический результат новым чтением.Новое намерение требует нового ключа. Если отправить тот же исходный ETag с новым ключом после успешной правки, HTTP
412 PRECONDITION_FAILEDозначает конфликт версии. Прочитайте товар заново, сравните изменение, затем решите, нужна ли новая правка. Не подставляйте свежий ETag вслепую.Для отмены своей правки сначала прочитайте текущий товар и выполните отдельный PATCH с его ETag, новым ключом и прежним названием. Это новая условная команда; сохранённую квитанцию предыдущей команды нельзя переписать.
Каталог связывает durable replay с бизнесом, адресом товара, command key и каноническим содержимым patch. Он использует собственную квитанцию, а не общий краткоживущий HTTP cache. При использовании того же ключа для другого содержимого возвращается HTTP 422 IDEMPOTENCY_KEY_MISMATCH. Даже если пустое тело допустимо как no-op, оно проверяет ETag и не создаёт новую durable команду для replay. Ключ каталога имеет предел 128 символов; длиннее — HTTP 400 INVALID_REQUEST.
В MCP та же подготовленная правка передаётся доступному update_catalog_item с catalog:write. Поля HTTP header становятся явными полями schema инструмента, tenant выбирается проверенным контекстом:
{"seq_id":7,"zone":"ru","etag":"W/\"57f60dd745c6591b\"","idempotency_key":"catalog-change","patch":{"title":"Changed"}}Ответ использует тот же detail каталога. Для самого tool call бизнес-ошибка передаётся как isError:true, а не как обещанный HTTP 412 на transport: MCP guide объясняет этот envelope. Правило нового ключа для нового намерения и неизменного ключа для точного повтора сохраняется.
Ошибки условной правки каталога
| HTTP / код | Когда | Восстановление |
|---|---|---|
428 PRECONDITION_REQUIRED | Нет непустого If-Match либо отсутствует Idempotency-Key | Прочитать detail и передать оба header |
412 PRECONDITION_FAILED | Версия устарела и точного replay принятой команды нет | Прочитать текущее состояние, сравнить и принять новое решение |
422 IDEMPOTENCY_KEY_MISMATCH | Принятая идентичность команды повторена с другим patch | Разделить исходный повтор и новое намерение |
422 CATALOG_FIELD_READ_ONLY | Непосредственный вызов сервиса содержит недопустимое/read-only поле | Использовать разрешённые поля и schema; REST body pipe также может отказать с 400 |
422 VALIDATION_ERROR | Правила канонического writer отклонили patch, а внутренний код не входит в публичный ErrorCode | Исправить payload по фактическому сообщению |
409 VARIANT_KEY_IMMUTABLE / VARIANT_KEY_REUSED | Нарушена идентичность варианта | Сохранить ключ существующего варианта и исправить набор |
401 / 403 | Не пройдены вход, scopes или актуальные ограничения | Проверить авторизацию и права |
В отказе 412 может присутствовать current_etag. Это подсказка для нового чтения, а не согласие на перезапись. RFC9457 problem response использует code и traceId; при обращении в поддержку передайте их без credential и личных данных. Поля ошибки зависят от конкретного пути отказа, поэтому не считайте current_etag обязательным в каждом конфликте. Справочник ошибок.
Полный контрактный пример отказа PATCH без If-Match, HTTP 428, Content-Type: application/problem+json. Route-параметр в instance показан так, как он записан в boundary fixture:
{
"type":"https://telegafirst.ru/docs/errors#PRECONDITION_REQUIRED",
"title":"Precondition Required",
"status":428,
"instance":"/api/v1/catalog/{seq_id}",
"code":"PRECONDITION_REQUIRED",
"traceId":"0123456789abcdef0123456789abcdef",
"detail":"If-Match header is required for PATCH",
"hint":{
"reason":"This write is guarded by optimistic concurrency and needs the version it is based on.",
"recovery":"Read the object first and send the write with its ETag in the If-Match header."
}
}Общая HTTP идемпотентность имеет собственное окно
Общий idempotency interceptor требует ключ для POST с непустым телом, если маршрут явно не исключён; отсутствие ключа в этом случае даёт HTTP 400 IDEMPOTENCY_KEY_REQUIRED. Переданный ключ включает общий cache и для остальных поддерживаемых запросов на не исключённых маршрутах: PATCH, PUT, DELETE и POST без непустого тела. Дополнительные обязательные требования к ключу определяет контракт конкретной операции, как у каталога с его 428 PRECONDITION_REQUIRED. Квитанция общего REST cache хранится 24 часа. В этом окне завершённый запрос повторяет сохранённые body и HTTP status; для некоторых терминальных 4xx сохраняется и отказ. 429 и 5xx не считаются таким завершённым replay и допускают предусмотренную повторную попытку.
Пока запрос выполняется, повтор с его ключом получает 409 CONFLICT. Изменённое тело общего запроса под прежним ключом даёт 422 IDEMPOTENCY_KEY_MISMATCH. У создания заказа и storefront checkout применяется собственная канонизация запроса: несовпадение даёт 409 IDEMPOTENCY_CONFLICT. Поэтому «все конфликты — 409» и «все записи — один cache» неверны. У маршрутов корзины собственная проверка версии может возвращать 428 ETAG_REQUIRED; не заменяйте её каталоговым кодом.
Общий ключ привязан к проверенному бизнесу, актору и операции; копирование строки ключа в другой бизнес не является адресом старого результата. Scope, доступность, принадлежность объекта и необходимые route preconditions продолжают проверяться до исполнения или replay. После истечения общего окна повтор не обещает возврат прежней квитанции: сначала выясните результат, особенно если операция могла иметь внешний эффект. GET-чтение, catalog durable replay и MCP-вызовы не наследуют автоматически этот 24-часовой cache.
Для примера ответа формы из идентификаторов повтор REST POST с form-1042 возвращает тот же seq_num:1042 и submission_code, пока квитанция доступна. Новая отправка с новым ключом создаёт новый ответ, даже если посетитель ещё anonymous. Привязка ранее созданного ответа к пользователю не меняет его номер и код.
Денежная операция требует собственного решения человека
Свой агент клиента через scoped Hub готовит продажу или счёт парой prepare_manual_sale → execute_manual_sale либо prepare_send_invoice → execute_send_invoice. Человек проверяет подготовленный результат и подтверждает исполнение; idempotency key не заменяет этот шаг. Используйте реальные scopes и текущую schema typed tool, не придумывайте credential provenance или фиктивное приложение. Внутренние разговорные AI-агенты только советуют и передают вопрос оператору; они не выполняют денежную мутацию. Продажи, платежи.
Успех проверки replay — сохранённый результат одной команды; успех новой правки — ожидаемое изменение после чтения свежей версии. Ни одно из этих правил не даёт права автоматически повторять продажу, отправку счёта, рассылку или оплату при неясном исходе.