Короткий ответ
Клиент передаёт уникальный ключ идемпотентности (например, в заголовке Idempotency-Key), сервер сохраняет связь "ключ → результат" и при повторном запросе с тем же ключом возвращает сохранённый результат, не выполняя операцию повторно.
Как это работает подробнее
при первом запросе с ключом сервер атомарно (например, через уникальный индекс в БД) резервирует этот ключ, выполняет операцию и сохраняет результат (включая статус-код и тело ответа) привязанным к ключу; при повторном запросе с тем же ключом сервер находит существующую запись и возвращает тот же результат без повторного выполнения бизнес-логики.
Пример кода
example.py
from fastapi import FastAPI, Header, HTTPException
app = FastAPI()
@app.post("/payments")
def create_payment(payload: dict, idempotency_key: str = Header(...)):
existing = idempotency_store.get_if_exists(idempotency_key) # атомарная проверка/резервирование
if existing is not None:
return existing["response_body"]
result = process_payment(payload)
idempotency_store.save(idempotency_key, result)
return resultЧто использовать на практике
- Рекомендация. ключ должен генерировать клиент (а не сервер) до первой попытки отправки, чтобы повторная отправка при таймауте использовала тот же ключ. Время жизни записи об идемпотентности нужно ограничивать (например, 24 часа), чтобы не накапливать их бесконечно. Важно защититься от гонки: если два запроса с одинаковым ключом приходят почти одновременно, нужна атомарная операция "вставить, если не существует", а не read-then-write.
Подводные камни
- Риски. генерировать ключ идемпотентности на сервере или выводить его из содержимого запроса без согласия клиента — тогда клиент не может переиспользовать его при реальном retry; проверять наличие ключа и сохранять результат в два отдельных, не атомарных шага, что создаёт окно для двойного выполнения операции при параллельных повторных запросах.