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

AI-агенты ошибаются при повторах быстрее людей. Человек, увидев тайм-аут, может остановиться, проверить очередь заявок и разобраться, что произошло. Агент часто видит исключение, следует инструкции повторить запрос и отправляет вторую запись, пока первый запрос еще обрабатывается где-то за пределами сетевой границы.
Так появляются дублирующие заявки, повторные попытки платежа, повторные приглашения пользователей и два развертывания одного изменения. Решение не в том, чтобы попросить агента быть внимательнее. Нужен контракт API, который сохраняет идентичность одного задуманного действия при повторах, замечает изменения, скрытые за повторно использованным идентификатором, и запрашивает новое подтверждение человека, когда последствия этого требуют.
Повторы обычны, дублирующие эффекты нет
Тайм-аут не означает, что сервер ничего не сделал. Сервер мог создать заявку, а ответ потерялся по дороге назад. Он все еще может обрабатывать запрос. Балансировщик нагрузки мог принять соединение, тогда как вышестоящий сервис так и не увидел байты. Клиент не может определить результат по одной ошибке сокета.
Поэтому обычная инструкция агенту «повторяй при сетевых ошибках» неполна. Она считает любой неопределенный результат неудачным действием. Для конечных точек записи неопределенный результат имеет три состояния:
- сервер не получил запрос;
- сервер принял запрос и завершил работу;
- сервер принял запрос, но еще не завершил работу.
Один и тот же повтор должен быть безопасен во всех трех состояниях. Если в завершенном состоянии он создает второй эффект, граница повторов у конечной точки небезопасна.
HTTP не решает эту задачу за вас. RFC 9110 определяет идемпотентные методы как методы, предполагаемый эффект которых остается тем же после одного или нескольких одинаковых запросов. К ним относятся PUT, DELETE и безопасные методы. RFC также говорит, что клиент может повторить идемпотентный запрос после сбоя связи. Это полезно, но не делает безопасной любую конечную точку, использующую маршрут PUT. Сервер может привязать к обработчику PUT отправку писем, начисление кредита или запуск развертывания и повторить этот побочный эффект, если не предусмотрит защиту.
Для POST нужно отдельное соглашение. Многие API используют POST для действий, потому что сервер назначает идентификаторы ресурсов или потому, что запрос означает «выполнить эту бизнес-операцию». Агент может повторить такой запрос только тогда, когда API объясняет, как распознать одну операцию среди нескольких попыток.
Разделяйте транспортные и бизнес-повторы. Транспортный повтор повторно отправляет ту же операцию, потому что ее результат остается неизвестным. Бизнес-повтор запускает другую операцию, потому что первая завершилась с известной окончательной ошибкой. Смешение этих понятий приводит к классическому отчету об инциденте: агент успешно повторил действие дважды.
Токен идемпотентности идентифицирует одно задуманное действие
Токен идемпотентности это созданный клиентом непрозрачный идентификатор, который означает: «все запросы с этим значением являются попытками выполнить одно действие». Агент создает его до первого запроса, сохраняет вместе с состоянием задачи и отправляет одно и то же значение при каждом повторе.
Токен должен относиться к логическому действию, а не к попытке HTTP-запроса. Если агент создает заявку в службу поддержки, теряет ответ и отправляет новый запрос с новым токеном, API не сможет распознать повтор. Он должен создать вторую заявку, потому что вызывающая сторона сообщила ему, что это вторая операция.
Используйте случайное значение с высокой энтропией. Часто применяют UUID, но подойдет любой формат, если вызывающие стороны не могут угадывать значения, а сервер воспринимает токен как непрозрачный. Передавайте его в заголовке Idempotency-Key или в документированном поле запроса. Заголовок отделяет идентичность операции от бизнес-данных и упрощает передачу токена через промежуточное ПО, журналы и трассировку.
Практический запрос выглядит так:
curl -X POST https://api.example.test/v1/tickets \\
-H 'Authorization: Bearer $TOKEN' \\
-H 'Content-Type: application/json' \\
-H 'Idempotency-Key: 81b59b1a-9e75-4de7-a53b-1bb50969c83c' \\
-d '{"project":"ops","title":"Rotate staging certificate","priority":"high"}'
При первом принятом вызове сервер сохраняет токен, канонический отпечаток запроса, состояние операции и, в конечном счете, ответ для повторной выдачи. Если в более позднем запросе приходят тот же токен и тот же отпечаток, сервер возвращает прежний результат, а не создает вторую заявку.
Клиенту нужно надежное место для хранения токена. Агент, который держит его только в текущем промпте или памяти процесса, потеряет идентичность операции после перезапуска. Храните токен рядом с записью задачи, задания или контрольной точкой рабочего процесса. Если человек просит агента создать вторую, намеренно отдельную заявку с тем же текстом, агент должен выпустить новый токен: человек выразил новое намерение.
Не делайте токеном изменяемое имя задачи, метку времени или запрос на естественном языке. Такие значения сталкиваются, меняются между повторами или раскрывают информацию в журналах. Непрозрачные идентификаторы скучны. Именно поэтому они работают.
Отпечаток замечает изменившийся повтор
Токен показывает, утверждает ли вызывающая сторона, что два запроса относятся к одной операции. Отпечаток запроса показывает, означают ли эти запросы на самом деле одно и то же. Нужны оба механизма.
Представьте, что агент сначала просит API развертывания отправить коммит a1b2c3 в staging. Происходит тайм-аут, агент читает новую заметку в задаче и повторяет запрос с тем же токеном, но уже с коммитом d4e5f6. Если сервер без проверки вернет первый ответ, он скроет ошибку агента. Если выполнит второе тело запроса, то позволит одному идентификатору операции авторизовать два разных развертывания.
Канонизируйте значимые части запроса и хешируйте результат. Обычно API включает HTTP-метод, нормализованный маршрут, аутентифицированный аккаунт или тенант и каноническое тело JSON. Иногда в отпечаток включают отдельные заголовки, если они меняют бизнес-эффект. Исключайте изменчивые заголовки трассировки, метаданные соединения и сам заголовок идемпотентности.
С JSON нужна аккуратность. Хеширование исходных байтов дает разные результаты, если эквивалентный JSON использует другой порядок свойств или пробелы. Каноническое представление сортирует свойства объектов, сохраняет порядок массивов, применяет заданный формат чисел и исключает поля, которые назначает сервер. Еще лучше строить отпечаток после разбора значений по умолчанию и отклонения неизвестных полей, когда API уже проверил объект команды. Тогда отпечаток соответствует операции, которую выполнит сервер, а не произвольному способу записи входных данных.
Например, этот псевдокод сохраняет дайджест после проверки:
command = validate_create_ticket(request.body)
canonical = canonical_json({
"method": "POST",
"route": "/v1/tickets",
"account_id": authenticated_account.id,
"command": command
})
fingerprint = sha256(canonical)
Если токен уже существует, сравните отпечатки до возврата результата или ожидания предыдущей операции. При различии отклоните запрос с ответом о конфликте. Включите сохраненные идентификатор и состояние операции, но не возвращайте защищенные сведения запроса неавторизованному вызывающему.
Сам по себе отпечаток не обнаруживает дубликаты. Два пользователя могут законно создать две одинаковые заявки. Сервис расчета зарплаты может законно отправить одинаковые платежи двум сотрудникам. Хеширование тела и устранение всех совпадений приведет к незаметной потере правильной работы. Ограничьте дедупликацию токеном идемпотентности, а бизнес-правила уникальности применяйте там, где они действительно нужны предметной области.
Криптографические хеши делают случайные коллизии практически невозможными при использовании современной функции вроде SHA-256. Но они не доказывают намерение вызывающей стороны. Намерение передает токен, а отпечаток требует согласованности. Команды, которые считают эти механизмы взаимозаменяемыми, обычно получают правило дедупликации, которое не могут объяснить, когда оно отклоняет законный запрос.
Сервер должен захватить токен до выполнения действия
Таблица идемпотентности, сохраняющая результат только после завершения побочного эффекта, все еще допускает гонку. Два параллельных повтора могут одновременно проверить таблицу, не найти запись, создать две заявки, а затем начать конкурировать за сохранение результата. Мне доводилось видеть, как это выдавали за нестабильную проблему агента, хотя настоящей причиной было отсутствие ограничения уникальности.
Сервер должен атомарно захватить токен до выполнения необратимой работы. Установите уникальное ограничение на область действия и токен, обычно на идентификатор аккаунта и токен идемпотентности. В одной транзакции попытайтесь вставить строку с отпечатком и состоянием in_progress. Запрос, который победил, получает право на выполнение. Все остальные запросы читают существующую строку.
Упрощенная таблица может содержать такие поля:
create table idempotency_operations (
account_id text not null,
token text not null,
fingerprint text not null,
state text not null,
response_status integer,
response_body jsonb,
created_at timestamptz not null,
primary key (account_id, token)
);
Первичный ключ здесь действительно выполняет работу. Код, который сначала проверяет, а потом вставляет запись, оставляет достаточно большую брешь для параллельных работников, повторной доставки из очереди и нетерпеливых повторов.
После захвата обработчик выполняет бизнес-действие и записывает окончательный ответ в строку операции. Последующие совпадающие запросы получают сохраненные статус и тело. Так вызывающий получает стабильный ответ, даже если исходный обработчик уже завершился успешно, но соединение оборвалось до отправки ответа.
Сложный случай это запрос, владеющий строкой, но прервавшийся посреди работы. Не удаляйте строку только потому, что работник завершился по тайм-ауту. Другой работник может все еще завершать операцию, либо внешний провайдер уже мог принять ее. Пометьте операцию как ожидающую или неизвестную, сохраните достаточно данных для расследования и дайте вызывающим возможность запросить ее статус. Задание восстановления может разрешать устаревшие записи только тогда, когда понимает состояние внешней системы.
Для работы, которая пересекает границу между базой данных и внешним API, используйте паттерн outbox или токен идемпотентности на стороне провайдера. Транзакция базы данных не может отменить письмо, платеж или облачное развертывание после выхода из вашего процесса. Запишите намерение и событие outbox в одной локальной транзакции, а затем поручите работнику отправить событие со стабильным идентификатором операции во внешнюю систему. Так у кода восстановления появляется конкретная операция для повтора, а не повод изобретать второе действие.
Подтверждение должно быть привязано к точной операции
Подтверждение человека предотвращает другую проблему: агент может иметь право на действие, но предложенная операция оказаться неожиданной, слишком широкой или повторной после изменения контекста. Общая кнопка «разрешить развертывание» этого не решает. Она позволяет агенту подменить одно развертывание другим под тем же одобрением.
Хорошее подтверждение называет цель, операцию, последствия и идентификатор операции. Для развертывания в production покажите окружение, артефакт или ссылку на коммит, затронутый сервис и возможность отката. Для платежа покажите получателя, сумму, валюту и номер счета. Для заявки покажите целевой проект и заголовок.
Запись подтверждения должна быть привязана к отпечатку запроса и истекать, когда предложение перестает быть актуальным. Если агент меняет тело после одобрения, отпечаток меняется, и система должна запросить подтверждение снова. Использование одобрения после изменения запроса это тихая форма повышения привилегий, даже если никто не ставил такой цели.
Не заставляйте человека подтверждать каждый повтор с низким риском. Иначе правильная идемпотентность превратится в усталость от одобрений. Первое подтверждение может разрешать одну операцию с определенным отпечатком, а совпадающие повторы могут использовать это одобрение, потому что они не меняют ее смысл. Изменившееся тело требует нового решения.
Некоторые команды полагаются на сообщение в чате вроде «Продолжить?» и считают ответ одобрением. В напряженной ситуации это не работает: в записи часто нет точных параметров, а агент может принять поздний ответ за согласие на более ранний запрос. Внесите идентификатор операции в запись подтверждения и заставьте исполнитель проверить его перед отправкой записи.
Простая структура одобрения делает привязку видимой:
{
"operation_id": "op_3f8c",
"idempotency_token": "81b59b1a-9e75-4de7-a53b-1bb50969c83c",
"fingerprint": "e5c7...",
"expires_at": "2025-06-14T15:30:00Z",
"approved_by": "user_42"
}
Считайте подтверждение авторизацией конкретной команды, а не разрешением импровизировать вокруг целой категории команд. Это делает повтор безопасным и не дает агенту пустое одобрение, которое можно использовать позже.
Для систем заявок нужна дополнительная проверка на уровне бизнеса
Токены идемпотентности останавливают дублирующие транспортные попытки, но у систем заявок есть другой источник повторов: агенты могут запускать отдельные операции, описывающие одну и ту же проблему. Оповещение мониторинга приходит дважды, два запуска агента читают один канал инцидента или планировщик после сбоя повторяет задачу без исходного состояния.
Не решайте это дедупликацией по тексту заголовка. Заголовки заявок достаточно различаются, чтобы пропускать дубликаты, а одинаковые заголовки могут относиться к разным инцидентам. Сначала определите, что означает идентичность в вашей предметной области. Это может быть идентификатор события оповещения, идентификатор инцидента, ссылка на issue в репозитории или составное значение вроде сервиса, отпечатка оповещения и интервала инцидента.
Сделайте этот бизнес-идентификатор явным в API:
{
"source_event_id": "alert-7c91",
"project": "operations",
"title": "Certificate expiry alert",
"description": "Alert event alert-7c91 crossed its threshold."
}
Сервис заявок может обеспечить уникальность source_event_id в нужной области. Тогда второй запуск агента получит идентификатор существующей заявки, а не добавит еще один элемент в очередь. Это отдельная задача от идемпотентности. У двух вызовов могут быть разные токены идемпотентности, потому что они пришли от разных процессов агента, но они все равно могут представлять одно исходное событие.
Агенту стоит искать существующую запись перед созданием только тогда, когда результат поиска содержит стабильный идентификатор, которому можно доверять. Поиск по заголовку кажется удобным, потому что не требует изменений API. Но он ломается при задержке индексации, изменении ранжирования или перефразировании заголовка агентом. Разместите правило уникальности там, где выполняется запись, и возвращайте ясный ответ: API создал заявку или использовал существующую.
Осторожнее с автоматическими комментариями и изменениями статуса. Операция, нашедшая существующую заявку, может все равно добавить повторный комментарий или снова открыть закрытый инцидент. Дайте каждому значимому поддействию собственный идентификатор либо опишите в команде все требуемое итоговое состояние. Расплывчатые конечные точки вроде «обновить эту заявку» трудно безопасно повторять, потому что никто не знает, какая часть обновления уже выполнилась.
Для платежных записей нужен запрос результата, а не надежда
К платежным действиям требования строже: двойное списание вредит клиенту, даже если позже вы вернете деньги. Приложение должно отправлять платежному провайдеру один стабильный токен идемпотентности и сохранять ссылку на транзакцию провайдера вместе с локальной записью операции.
При тайм-ауте клиент должен считать платеж неизвестным. Нужно запросить операцию по ссылке провайдера, ссылке продавца или токену идемпотентности, если провайдер поддерживает такой поиск. Нельзя начинать вторую попытку платежа только потому, что агент не получил успешный ответ.
Есть две операции, которые часто ошибочно объединяют: создание платежного намерения и списание средств. Они могут иметь разную логику повторов. Сервис может безопасно создать или получить один платежный объект по токену, а затем потребовать отдельного явного списания после прохождения проверок. Описывайте бизнес-состояния открыто, вместо того чтобы прятать их за одной конечной точкой, которая пытается сделать все при каждом вызове.
Суммы нужно канонизировать до вычисления отпечатка. Преобразуйте значения в минимальные единицы поддерживаемой валюты или другое точное представление до того, как запрос попадет в слой дедупликации. Не хешируйте число с плавающей точкой в экранном формате и не ожидайте надежного сравнения эквивалентных вычислений. Платежный запрос также должен включать ссылку на счет или заказ, если она есть в предметной области: это даст сотрудникам способ распознать дублирующее намерение помимо сетевых повторов.
Функция идемпотентности провайдера не освобождает ваш API от ответственности. Приложение все равно должно остановить две задачи агента, которые создают два разных запроса провайдеру для одного заказа. Установите ограничение уникальности для оплачиваемого состояния заказа, используйте локальную запись операции и заставьте агента обращаться к этой записи после неопределенного результата.
Для возвратов нужна такая же осторожность. «Повторить возврат» может означать повторить тот же запрос или начать еще один частичный возврат. Храните стабильный идентификатор для каждой инструкции возврата и фиксируйте уже запрошенную сумму. Если агенту нужно выполнить второй возврат, оформите его как новую, явно разрешенную инструкцию с новым идентификатором.
Для развертываний нужны неизменяемые ссылки и блокировка выпуска
Повтор развертывания безопасен только тогда, когда он указывает на тот же выпуск. Имена веток вроде main и изменяемые теги вроде latest этому требованию не соответствуют. После тайм-аута повтор может разрешить то же имя в другой код, а затем сообщить об успехе, развернув то, что одобряющий никогда не проверял.
Используйте неизменяемый дайджест артефакта, идентификатор коммита или версию, которую система выпуска гарантирует неизменной. Включите ее в отпечаток запроса и подтверждение. Если агент отправляет тот же токен с изменившейся ссылкой на артефакт, отклоните запрос как конфликт, а не считайте его обновленным повтором.
Нужно также правило конкурентного доступа к окружению. Две отдельные операции могут законно иметь разные токены и при этом конфликтовать, потому что обе нацелены на production. Блокировка выпуска, оптимистичная проверка версии или очередь развертываний могут сериализовать такие изменения. Идемпотентность не решает, какое из двух разных развертываний должно победить. Она только не дает одному развертыванию выполниться дважды.
Рассмотрим такую последовательность сбоя. Агент начинает развертывание dep-118 для коммита a1b2c3, и контроллер развертывания принимает его. Агент теряет ответ, считает операцию неудачной и запускает dep-119 с коммитом d4e5f6, потому что появился новый коммит. Теперь обе задачи меняют одно окружение. Токен остановил бы только настоящий повтор dep-118; блокировка выпуска или проверка ожидаемой версии окружения останавливает конфликтующий второй план.
API развертывания должен предоставлять ресурс статуса операции со состояниями: поставлена в очередь, выполняется, завершена успешно, завершилась ошибкой, отменена или неизвестна. После тайм-аута агент должен опрашивать этот статус. Нельзя выводить завершение из отсутствующего ответа или строки журнала без идентификатора операции.
Откату нужны собственные идентификатор операции и одобрение. Если считать откат повтором развертывания, важное изменение намерения исчезает. Откат может выполняться автоматически по документированному правилу безопасности, но отдельная запись все равно должна отличать его от исходного выпуска.
Инструменты агента должны сохранять идентичность операции за границей
Интерфейс инструмента агента должен делать безопасное поведение проще небезопасного. Дайте агенту одно действие, принимающее стабильный идентификатор операции, полезную нагрузку и объявленный режим повтора. Возвращайте результат, из которого понятно, создал ли сервис новую работу, повторно выдал прежний результат, обнаружил выполняющуюся операцию или отклонил изменившийся повтор.
Избегайте инструментов, которые молча создают новый токен идемпотентности при каждом вызове. В демонстрации это выглядит удобно, но первый настоящий тайм-аут все ломает. Если инструмент сам создает токен, он должен немедленно вернуть его и сохранить там, откуда его можно получить при следующем вызове. В большинстве систем токеном должен владеть слой рабочего процесса: именно он понимает, какие вызовы относятся к одному действию по запросу пользователя.
Sallyport может хранить учетные данные API за пределами агента, пока агент отправляет задуманное HTTP-действие через свое MCP-соединение. Такое разделение помогает защитить учетные данные, но нижележащему API все равно нужна идемпотентность. Защищенная учетная запись не превращает неоднозначный POST в безопасный повтор.
Сделайте правило повторов агента явной частью контракта инструмента:
if response is a known success:
record operation complete
if response is a timeout or connection failure:
query operation status using the same token
retry only with the same token if the API permits it
if response says fingerprint conflict:
stop and request a new operation or human review
if response is a known business failure:
do not retry until the task changes
Не позволяйте агенту использовать экспоненциальную задержку вместо состояния. Задержка снижает нагрузку на сервис, что важно, но не отвечает на вопрос, завершилась ли последняя запись успешно. Агент должен сохранить идентификатор операции до ожидания.
Журналы должны доказывать, что произошло после спорной записи
Когда клиент говорит, что с него списали деньги дважды, или инженер находит две заявки, нужно ответить на четыре вопроса: какой запуск агента отправил каждый запрос, какой токен он использовал, какой отпечаток вычислил сервер и какой результат вернула внешняя система. В обычных журналах запросов часто отсутствует хотя бы один из этих пунктов.
Записывайте операцию на границе, где API принимает действие. Включайте аутентифицированного субъекта, токен, отпечаток, маршрут запроса, переходы состояния операции, ссылку на ответ и ссылку на внешнего провайдера, если она есть. Не помещайте секреты и полные чувствительные тела запросов в обычные журналы. Отпечаток позволяет сравнивать запросы, не сохраняя все личные поля в каждой системе журналирования.
Журнал аудита только для добавления полезен, когда агент имеет право выполнять внешние записи. Различайте состояния «попытка», «одобрено», «отправлено», «принято», «завершено» и «повторно выдано». Это не одно и то же. Повтор, получивший сохраненный прежний ответ, должен иметь статус replayed, а не created, иначе операторы посчитают его вторым действием.
Sallyport записывает сессии агентов и отдельные действия в журналы, созданные на основе зашифрованного аудита с цепочкой хешей, а sp audit verify позволяет проверить цепочку офлайн. Это помогает установить, что прошло через шлюз действий. Сопоставляйте такие свидетельства с записями идемпотентности принимающего сервиса, потому что именно он определяет, выполнил ли бизнес-действие.
Проверьте путь спорной записи до того, как начнете ему доверять. Заставьте сервер завершить запрос и отбросить ответ. Отправьте параллельные копии с одним токеном. Перезапустите агента между попытками. Используйте токен повторно с изменившейся полезной нагрузкой. Завершите работника после захвата токена, но до сохранения результата. Система, которая переживает только чистые ответы об успехе, не решила проблему дублирующих записей.
Начните с конечной точки записи, повтор которой причиняет самый большой вред. Добавьте стабильный токен, атомарно захватывайте его до любого побочного эффекта, связывайте его с отпечатком и дайте вызывающим способ запросить статус при неизвестном результате. Затем заставьте агента сохранять этот идентификатор, пока он не докажет, что операция достигла конечного состояния.
Вопросы и ответы
Как остановить создание дубликатов записей AI-агентом после тайм-аута?
До первой отправки запроса назначьте каждой логической операции стабильный токен идемпотентности. Сервер сохраняет первый завершенный ответ для этого токена и возвращает его при последующих повторах. Не создавайте новый токен после тайм-аута: так повтор превращается в новую операцию.
Достаточно ли хеша запроса для идемпотентности API?
Нет. Отпечаток запроса показывает, что один и тот же набор данных пришел повторно, но не говорит, выражают ли два одинаковых запроса одно намерение или два разных. Токен идемпотентности от клиента фиксирует намерение, а отпечаток помогает отклонить повторное использование токена с изменившимся содержимым.
Может ли AI-агент безопасно повторять POST-запросы?
Только если у конечной точки есть документированный контракт идемпотентности, а агент сохраняет один и тот же токен при всех повторах. POST не становится идемпотентным автоматически по правилам HTTP. Безопасному агенту также нужны ограниченное число повторов, понятная обработка тайм-аутов и способ получить результат операции.
Что должен вернуть API, если один и тот же токен идемпотентности пришел дважды?
Сервер должен вернуть ответ, сохраненный для исходного принятого запроса, включая первоначальный идентификатор ресурса и статус. Он не должен повторять побочные эффекты или создавать вторую запись. Если исходный запрос еще выполняется, верните явный статус «выполняется» или дождитесь сохраненного результата.
Как токены идемпотентности предотвращают двойное списание с карты?
Используйте один токен идемпотентности для всей попытки покупки, а источником истины о списании пусть будет платежный провайдер. После неопределенного сетевого сбоя не повторяйте запрос к своему платежному API с новым токеном. Сначала запросите сохраненную операцию или транзакцию провайдера.
Предотвращают ли запросы подтверждения дублирование развертываний?
Они помогают, но не заменяют авторизацию. Подтверждение должно связывать человека с конкретным описанием действия: целью, изменением, областью действия и идентификатором операции. Иначе общее одобрение может случайно разрешить повторный запрос с другим содержимым.
Как долго API должен хранить токены идемпотентности?
Удаляйте токены после периода, в течение которого клиент реально может повторить или воспроизвести запрос. Точный срок зависит от лимита повторов клиента и бизнес-риска. Долговечную бизнес-запись стоит хранить дольше, если старый повтор все еще может навредить, например при платеже или создании внешней заявки.
Что произойдет, если у того же токена идемпотентности будет другое тело запроса?
Считайте повторное использование токена с другим отпечатком ошибкой клиента и отказывайтесь выполнять запрос. Ответ с конфликтом заставит агента остановиться и проверить собственное состояние, вместо того чтобы незаметно приписать старому идентификатору операции новый смысл.
Что должен делать агент после тайм-аута API-запроса?
Считайте результат неизвестным, а не неудачным. Запросите статус операции по тому же токену или идентификатору запроса, а затем повторяйте только с этим идентификатором, если API это разрешает. Слепая отправка нового POST приводит к дубликатам заявок и платежей.
Безопасно ли автоматически повторять PUT и DELETE?
PUT и DELETE имеют идемпотентную семантику HTTP, если сервер реализует их правильно, но после тайм-аута клиент все равно не знает, успел ли первый запрос подействовать. POST тоже можно сделать безопасным с помощью токена идемпотентности, а многим бизнес-операциям нужен именно такой явный контракт.