← Назад к вопросам
Как можно сделать POST запрос идемпотентным?
2.4 Senior🔥 131 комментариев
#REST API и HTTP#Архитектура и паттерны
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как сделать POST запрос идемпотентным
Идемпотентность означает, что повторное выполнение операции даёт тот же результат, что и однократное выполнение. POST запросы по определению не идемпотентны (они создают новый ресурс), но можно реализовать идемпотентность на уровне приложения.
Проблема неидемпотентности
# Без идемпотентности
POST /api/v1/orders
body: {"amount": 100}
# Клиент отправляет запрос
# Ответ теряется из-за сетевой ошибки
# Клиент повторяет запрос
# Результат: две транзакции вместо одной!
Решение 1: Idempotency Key (Ключ идемпотентности)
Это наиболее распространённый и рекомендуемый подход. Клиент отправляет уникальный ключ, сервер гарантирует, что запрос выполнится только один раз.
# Django/FastAPI пример
from uuid import uuid4
from django.db import models
from rest_framework.response import Response
from rest_framework.decorators import api_view
class IdempotencyKey(models.Model):
"""Хранит результаты идемпотентных операций"""
key = models.CharField(max_length=255, unique=True, db_index=True)
request_body = models.JSONField()
response = models.JSONField()
status_code = models.IntegerField()
created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField()
class Order(models.Model):
user = models.ForeignKey('User', on_delete=models.CASCADE)
amount = models.DecimalField(max_digits=10, decimal_places=2)
idempotency_key = models.CharField(max_length=255, unique=True)
created_at = models.DateTimeField(auto_now_add=True)
@api_view(['POST'])
def create_order(request):
"""Идемпотентное создание заказа"""
idempotency_key = request.headers.get('Idempotency-Key')
if not idempotency_key:
return Response(
{'error': 'Idempotency-Key header required'},
status=400
)
# Проверка: был ли уже такой запрос?
try:
cached = IdempotencyKey.objects.get(key=idempotency_key)
# Запрос уже обработан, возвращаем закешированный ответ
return Response(cached.response, status=cached.status_code)
except IdempotencyKey.DoesNotExist:
pass
try:
# Обработка запроса
order = Order.objects.create(
user=request.user,
amount=request.data['amount'],
idempotency_key=idempotency_key
)
response_data = {'id': order.id, 'amount': order.amount}
response_status = 201
# Сохраняем результат
IdempotencyKey.objects.create(
key=idempotency_key,
request_body=request.data,
response=response_data,
status_code=response_status,
expires_at=timezone.now() + timedelta(hours=24)
)
return Response(response_data, status=response_status)
except Exception as e:
# Сохраняем ошибку
error_response = {'error': str(e)}
response_status = 400
IdempotencyKey.objects.create(
key=idempotency_key,
request_body=request.data,
response=error_response,
status_code=response_status,
expires_at=timezone.now() + timedelta(hours=24)
)
return Response(error_response, status=response_status)
# Клиентская сторона
import requests
import uuid
def create_order(amount):
idempotency_key = str(uuid.uuid4())
response = requests.post(
'https://api.example.com/api/v1/orders',
json={'amount': amount},
headers={'Idempotency-Key': idempotency_key}
)
# Даже если сетевая ошибка, мы сохраняем ключ
# и можем повторить с тем же ключом
return response.json()
Решение 2: Использование UNIQUE constraint
Добавить уникальное ограничение на бизнес-логику.
# Пример: платёжная система
class Payment(models.Model):
order_id = models.IntegerField()
transaction_id = models.CharField(max_length=255, unique=True)
amount = models.DecimalField(max_digits=10, decimal_places=2)
status = models.CharField(max_length=20) # pending, success, failed
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ['order_id', 'transaction_id']
@api_view(['POST'])
def process_payment(request):
try:
payment = Payment.objects.create(
order_id=request.data['order_id'],
transaction_id=request.data['transaction_id'],
amount=request.data['amount'],
status='pending'
)
# Обработка платежа
payment.status = 'success'
payment.save()
return Response({'status': 'success'})
except IntegrityError:
# Дублирующийся платёж — возвращаем существующий
payment = Payment.objects.get(
order_id=request.data['order_id'],
transaction_id=request.data['transaction_id']
)
return Response({'status': payment.status})
Решение 3: Использование PUT вместо POST
Утверждение ресурса идемпотентно по определению.
# Вместо
POST /api/v1/users
body: {"id": 123, "name": "John"}
# Используй
PUT /api/v1/users/123
body: {"name": "John"}
# Третий запрос PUT не создаст дублирующийся ресурс
# Если ресурс уже существует, он обновится
@api_view(['PUT'])
def upsert_user(request, user_id):
"""Идемпотентное обновление или создание пользователя"""
user, created = User.objects.update_or_create(
id=user_id,
defaults=request.data
)
status_code = 201 if created else 200
return Response(UserSerializer(user).data, status=status_code)
Решение 4: Использование state machine (конечный автомат)
Разрешить переходы только в определённые состояния.
class Order(models.Model):
STATUS_CHOICES = [
('pending', 'Pending'),
('confirmed', 'Confirmed'),
('shipped', 'Shipped'),
('delivered', 'Delivered'),
('cancelled', 'Cancelled')
]
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
def confirm(self):
if self.status != 'pending':
raise ValueError(f"Cannot confirm order in {self.status} state")
self.status = 'confirmed'
self.save()
# Клиент повторяет запрос подтверждения
POST /api/v1/orders/123/confirm
# Первый запрос: order.status переходит pending → confirmed
# Второй запрос: order.status уже confirmed, операция отклонена
# (или операция игнорируется и возвращается 200 OK)
Решение 5: Версионирование ресурса
class Document(models.Model):
content = models.TextField()
version = models.IntegerField(default=1)
updated_at = models.DateTimeField(auto_now=True)
@api_view(['POST'])
def update_document(request, doc_id):
try:
document = Document.objects.select_for_update().get(id=doc_id)
# Проверка версии
if document.version != request.data.get('version'):
return Response(
{'error': 'Version mismatch'},
status=409 # Conflict
)
document.content = request.data['content']
document.version += 1
document.save()
return Response({'version': document.version})
except Document.DoesNotExist:
return Response({'error': 'Document not found'}, status=404)
Лучшие практики
# 1. Используй UUID для идемпотентности ключей
idempotency_key = uuid.uuid4()
# 2. Убедись, что ключ уникален в системе
class IdempotencyKey(models.Model):
key = models.CharField(max_length=255, unique=True, db_index=True)
# 3. Используй SELECT FOR UPDATE при обновлении
with transaction.atomic():
obj = Model.objects.select_for_update().get(id=1)
obj.field = new_value
obj.save()
# 4. Кэшируй результаты на определённое время
IdempotencyKey.objects.create(
key=idempotency_key,
response=response_data,
expires_at=timezone.now() + timedelta(hours=24)
)
# 5. Логируй все операции
import logging
logger = logging.getLogger(__name__)
logger.info(f"Processing idempotent request: {idempotency_key}")
Тестирование идемпотентности
import pytest
from django.test import Client
@pytest.mark.django_db
def test_idempotent_create_order():
client = Client()
idempotency_key = 'unique-key-123'
# Первый запрос
response1 = client.post(
'/api/v1/orders',
{'amount': 100},
HTTP_IDEMPOTENCY_KEY=idempotency_key
)
assert response1.status_code == 201
order_id_1 = response1.json()['id']
# Второй запрос с тем же ключом
response2 = client.post(
'/api/v1/orders',
{'amount': 100},
HTTP_IDEMPOTENCY_KEY=idempotency_key
)
assert response2.status_code == 201
order_id_2 = response2.json()['id']
# Проверка: оба ответа идентичны
assert order_id_1 == order_id_2
assert Order.objects.count() == 1 # Только один заказ создан
Вывод: Идемпотентность в POST запросах достигается через:
- Idempotency Key в заголовках — самый надёжный метод
- Уникальные ограничения на уровне БД
- Использование PUT вместо POST
- Стейт-машины для контроля переходов
- Версионирование ресурсов
Выбор метода зависит от специфики приложения и требуемого уровня надёжности.