Повтор запроса
Зачем это нужно
Ваша CRM отправила «оформить бронь», и связь оборвалась. Бронь создалась или нет? Если отправить запрос ещё раз — не будет ли двух броней и двух счетов родителю?
Для этого каждый POST, который что-то создаёт или меняет, несёт заголовок Idempotency-Key —
ключ повтора. Лагерь запоминает результат первого запроса с этим ключом. Повтор с тем же ключом
не выполняется заново, а получает сохранённый ответ: ту же бронь, тот же счёт.
Code
Правила
- Ключ — случайная строка, своя на каждое действие. Лучше всего UUID. До 255 знаков.
- Одно действие — один ключ. «Оформить бронь по заявке 10452» — один ключ на все попытки этого оформления. Следующее действие (выставить счёт по этой брони) — новый ключ.
- Повтор — с тем же ключом и тем же телом. Ответ будет тот же, что у первого запроса, с
заголовком
Idempotent-Replayed: true. - Тот же ключ с другим телом — отказ
422 idempotency_mismatch. Ключ нельзя переиспользовать для другого действия. - Ключи хранятся двое суток. После этого тот же ключ снова свободен.
- Ключи у каждого подключения свои: ваш ключ не пересечётся с ключом другого отдела.
- Два одновременных запроса с одним ключом не выполнятся дважды: второй дождётся первого и получит его результат.
Нет заголовка — 400 idempotency_key_required; длиннее 255 знаков — 400 idempotency_key_too_long.
Где ключ обязателен
Нужен Idempotency-Key | Не нужен |
|---|---|
POST /bookings, POST /bookings/batch | POST /quote — ничего не создаёт |
POST /bookings/{id}/cancel, /restore, /move, /together | POST /bookings/check — ничего не создаёт |
POST /bookings/{id}/invoices, POST /invoices/{id}/cancel | POST /webhooks/{id}/test — ничего не меняет |
POST /webhooks, POST /webhooks/{id}/rotate-secret, POST /webhooks/{id}/redeliver | PATCH и DELETE — их повтор и так безопасен |
Обязателен ли заголовок у конкретного адреса — видно и в его параметрах в справочнике.
Что запоминается, а что нет
| Ответ на первый запрос | Повтор с тем же ключом |
|---|---|
2xx — выполнено | Тот же ответ, ничего не выполняется заново |
4xx (кроме 429) — отказ | Тот же отказ, запрос не выполняется заново |
429 — превышен лимит | Выполняется заново (подождите Retry-After) |
5xx — сбой сервера | Выполняется заново или дожидается начатого (см. ниже) |
| Обрыв связи, ответа нет | Получите результат первого запроса, если он дошёл; иначе запрос выполнится |
Исправили данные — новый ключ
Отказ 4xx запоминается вместе с ключом. Если оформление получило 400 birth_date_required, а вы
добавили дату рождения — это уже другой запрос: отправьте его с новым ключом. С прежним
ключом и новым телом получите 422 idempotency_mismatch, с прежним телом — снова тот же отказ.
Ответ 503 timeout — не значит «не выполнено»
Если лагерь не успел ответить за 30 секунд, придёт 503 с кодом timeout. Но начатое действие
доводится до конца и без вас: бронь может оформиться через секунду после этого ответа.
Поэтому на 503 (и на обрыв связи, и на 500) — повторите с тем же ключом. Если действие ещё
выполняется, повтор дождётся его и вернёт результат. Если уже выполнилось — вернёт сохранённый ответ.
Не создавайте новый ключ на такой повтор — иначе можно получить вторую бронь (или
409 duplicate_child, если первая успела оформиться).
Как устроить у себя
- Создайте ключ до отправки и сохраните его в своей базе рядом с действием — например, в задаче «оформить бронь по заявке 10452». Так ключ переживёт и перезапуск вашей CRM.
- Отправьте запрос.
- Обрыв связи, таймаут,
429,500,503— подождите и повторите с тем же ключом. Пауза:Retry-After, если он есть, иначе 1, 2, 4, 8, 16 секунд. - Получили
2xx— действие выполнено, отметьте задачу. - Получили другой
4xx— показывайтеmessageменеджеру; повторять бесполезно. - Новый ключ — только для нового действия.
Ставьте таймаут вашего HTTP-клиента не меньше 35–40 секунд: лагерь может отвечать до 30 секунд.
Вторая защита: свой номер записи
Ключ повтора защищает от повтора одного и того же запроса. Если ваша CRM по ошибке создаст
новое действие для той же сделки (новый ключ), бронь оформится второй раз. От этого защищает
external_id: передавайте номер сделки при оформлении. Вторая бронь с тем же external_id не
оформится — ответ 409 external_id_taken с номером уже существующей брони.
If-Match: не затереть чужую правку
If-Match с версией записи (version) принимают ровно эти операции: PATCH /bookings/{id},
POST /bookings/{id}/cancel и PATCH /families/{id}. Остальные операции заголовок не смотрят.
- Прочитайте запись — запомните её
version(например,2027-05-14T10:02:45.918201Z). - Отправьте правку с заголовком
If-Match: "2027-05-14T10:02:45.918201Z"(кавычки можно не ставить). - Если запись изменили после вашего чтения — ответ
412 version_mismatch. Перечитайте её, покажите менеджеру новые данные и решите заново.
version сравнивайте только на равенство и передавайте как есть, не разбирая: её вид может
поменяться.