Короткий ответ
Клиент должен передавать идемпотентный ключ (например, заголовок Idempotency-Key), а сервер — сохранять связь между этим ключом и результатом первой успешной обработки, чтобы при повторном запросе с тем же ключом вернуть сохранённый результат вместо повторного создания заказа.
Как это работает подробнее
При первом запросе с новым Idempotency-Key сервер атомарно (например, через уникальный индекс в БД на этот ключ) резервирует ключ, выполняет операцию и сохраняет результат (например, order_id и тело ответа) вместе с ключом. При повторном запросе с тем же ключом сервер находит сохранённую запись и возвращает тот же результат без повторного выполнения бизнес-логики.
Пример кода
example.py
from fastapi import FastAPI, Header, HTTPException
app = FastAPI()
idempotency_store = {} # в реальности - таблица БД с уникальным индексом на key
@app.post("/orders")
def create_order(payload: dict, idempotency_key: str = Header()):
if idempotency_key in idempotency_store:
return idempotency_store[idempotency_key]
order = {"id": len(idempotency_store) + 1, **payload}
idempotency_store[idempotency_key] = order
return order
# Два одинаковых POST с одним и тем же Idempotency-Key
# должны вернуть один и тот же order id, а не два разных заказа.Что использовать на практике
- Практика. Важно учитывать конкурентный повтор «почти одновременно» (например, клиент шлёт повтор ещё до завершения первой обработки) — для этого ключ резервируют до выполнения операции, используя уникальность на уровне БД, а не только проверку-потом-запись в коде приложения, чтобы избежать гонки. Ключ обычно должен жить ограниченное время (например, 24 часа), а не бесконечно.
Подводные камни
- Риск. Реализуют идемпотентность только проверкой «а нет ли уже такого заказа с такими же полями» — это ненадёжно при похожих, но не идентичных легитимных заказах; не обрабатывают случай параллельного повтора запроса, полагаясь только на то, что полная обработка первого запроса уже завершится раньше второго.