← Назад к вопросам
Какие HTTP методы не являются идемпотентными?
1.6 Junior🔥 191 комментариев
#REST API и HTTP
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Какие HTTP методы не являются идемпотентными
Идемпотентность — это свойство операции, при котором выполнение её несколько раз даёт тот же результат, что и выполнение один раз. В контексте HTTP это критически важно для надёжности и безопасности API.
Определение идемпотентности
Идемпотентная операция:
Операция() = Результат 1
Операция() = Результат 1 (тот же)
Операция() = Результат 1 (тот же)
Неидемпотентная операция:
Операция() = Результат 1
Операция() = Результат 2 (может быть другим)
Операция() = Результат 3 (может быть другим)
Таблица: Идемпотентность HTTP методов
| Метод | Идемпотентный | Безопасный | Описание |
|---|---|---|---|
| GET | Да | Да | Только чтение, ничего не меняет |
| HEAD | Да | Да | Как GET, но без тела ответа |
| OPTIONS | Да | Да | Получить доступные методы |
| PUT | Да | Нет | Замена ресурса, можно повторять |
| DELETE | Да | Нет | Удаление, повторное удаление = 404 |
| PATCH | Нет | Нет | Частичное изменение, результат зависит от состояния |
| POST | Нет | Нет | Создание, каждый вызов = новый ресурс |
| TRACE | Да | Да | Тестирование маршрута |
| CONNECT | - | Нет | Туннелирование |
Неидемпотентные методы: POST и PATCH
1. POST — явно неидемпотентный
POST используется для создания новых ресурсов. Каждый запрос создаёт новый ресурс.
import requests
# ❌ POST создаёт новый ресурс КАЖДЫЙ РАЗ
response1 = requests.post('https://api.example.com/users', json={
'name': 'John',
'email': 'john@example.com'
})
print(response1.json()) # {"id": 1, ...}
response2 = requests.post('https://api.example.com/users', json={
'name': 'John',
'email': 'john@example.com'
})
print(response2.json()) # {"id": 2, ...} ← ДРУГОЙ ID!
response3 = requests.post('https://api.example.com/users', json={
'name': 'John',
'email': 'john@example.com'
})
print(response3.json()) # {"id": 3, ...} ← ЕЩЁ ДРУГОЙ ID!
# Результат: 3 разных пользователя созданы
Проблема при сетевой ошибке:
def create_payment(amount: float, user_id: int):
"""Создать платёж."""
try:
response = requests.post(
'https://payment-api.com/payments',
json={'amount': amount, 'user_id': user_id},
timeout=5
)
return response.json()['transaction_id']
except requests.Timeout:
# Ошибка! Но платёж может быть уже обработан
# Если повторим запрос, создадим второй платёж
# ФИНАНСОВАЯ ПОТЕРЯ!
raise
# Безопаснее:
def create_payment_safe(amount: float, user_id: int, idempotency_key: str):
"""Создать платёж с ключом идемпотентности."""
response = requests.post(
'https://payment-api.com/payments',
json={'amount': amount, 'user_id': user_id},
headers={'Idempotency-Key': idempotency_key}
)
# Сервер гарантирует: одинаковый Idempotency-Key
# = один платёж, даже если запрос повторён
return response.json()['transaction_id']
2. PATCH — неидемпотентный (обычно)
PATCH используется для частичного изменения ресурса. Результат может зависеть от текущего состояния.
# ❌ PATCH без идемпотентности
# Пример: инкремент счётчика
response1 = requests.patch(
'https://api.example.com/users/123',
json={'views': 10} # Увеличить на 10
)
# Состояние было: {"id": 123, "views": 0}
# Стало: {"id": 123, "views": 10}
response2 = requests.patch(
'https://api.example.com/users/123',
json={'views': 10} # Увеличить ещё на 10
)
# Состояние было: {"id": 123, "views": 10}
# Стало: {"id": 123, "views": 20} ← ДРУГОЕ!
response3 = requests.patch(
'https://api.example.com/users/123',
json={'views': 10}
)
# Состояние было: {"id": 123, "views": 20}
# Стало: {"id": 123, "views": 30} ← ЕЩЁ ДРУГОЕ!
# Результат: разные значения каждый раз
Идемпотентный PATCH:
# ✅ PATCH с абсолютными значениями (идемпотентный)
response1 = requests.patch(
'https://api.example.com/users/123',
json={'status': 'active', 'email': 'newemail@example.com'}
)
# Стало: {"status": "active", "email": "newemail@example.com"}
response2 = requests.patch(
'https://api.example.com/users/123',
json={'status': 'active', 'email': 'newemail@example.com'}
)
# Стало: {"status": "active", "email": "newemail@example.com"} ← ТО ЖЕ!
response3 = requests.patch(
'https://api.example.com/users/123',
json={'status': 'active', 'email': 'newemail@example.com'}
)
# Стало: {"status": "active", "email": "newemail@example.com"} ← ТО ЖЕ!
Почему важна идемпотентность
Проблема 1: Сетевые сбои
Клиент Сервер
│ │
│ ──POST (создать) ──→ │
│ ← (разорвана связь) ←──│
│ (клиент не знает, создался ли ресурс)
│ ──POST (повторить) → │
│ ← успешно ←── │
│
Результат: 2 ресурса создано вместо 1!
Проблема 2: Таймауты
try:
# Отправляем платёж
requests.post(
'https://bank.api/transfer',
json={'to': 'account123', 'amount': 1000},
timeout=3
)
except requests.Timeout:
print("Timeout! Повторяем...")
# Но платёж мог быть уже обработан!
# Без идемпотентности: потеря денег
Как обеспечить идемпотентность
1. Использовать Idempotency-Key (для POST)
import uuid
def create_order_safely(items: list[dict], user_id: int) -> dict:
"""Создать заказ с гарантией идемпотентности."""
idempotency_key = str(uuid.uuid4())
headers = {
'Idempotency-Key': idempotency_key,
'Content-Type': 'application/json'
}
response = requests.post(
'https://api.example.com/orders',
json={'items': items, 'user_id': user_id},
headers=headers
)
# Сервер гарантирует:
# - Если запрос повторён с тем же ключом
# - Вернётся тот же ответ (без нового заказа)
return response.json()
2. Реализация на сервере
from fastapi import FastAPI, Header
from typing import Optional
import uuid
app = FastAPI()
idempotency_store = {} # {идентификатор -> результат}
@app.post("/orders")
async def create_order(
items: list[dict],
user_id: int,
idempotency_key: Optional[str] = Header(None)
):
"""Создать заказ с проверкой идемпотентности."""
# Если ключ не предоставлен, генерируем новый
if not idempotency_key:
idempotency_key = str(uuid.uuid4())
# Проверяем: был ли уже такой запрос
if idempotency_key in idempotency_store:
# Возвращаем старый результат
return idempotency_store[idempotency_key]
# Создаём новый заказ
order = {
'id': generate_order_id(),
'user_id': user_id,
'items': items,
'total': sum(item['price'] for item in items)
}
# Сохраняем в БД
db.orders.insert(order)
# Сохраняем результат для идемпотентности
idempotency_store[idempotency_key] = order
return order
3. Использовать PUT вместо PATCH
# ❌ Неидемпотентный PATCH
response = requests.patch(
'https://api.example.com/users/123',
json={'active': True, 'views': '+1'} # +1 это инкремент!
)
# ✅ Идемпотентный PUT
response = requests.put(
'https://api.example.com/users/123',
json={
'name': 'John',
'email': 'john@example.com',
'active': True,
'views': 42 # Абсолютное значение
}
)
Практический пример: Обработка платежей
class PaymentService:
def __init__(self, db):
self.db = db
def process_payment(self, user_id: int, amount: float,
idempotency_key: str) -> dict:
"""
Обработать платёж с гарантией идемпотентности.
"""
# Проверяем: уже ли обработан платёж с этим ключом
existing = self.db.transactions.find_one({
'idempotency_key': idempotency_key,
'user_id': user_id
})
if existing:
# Возвращаем результат старого запроса
return {
'status': 'success',
'transaction_id': existing['id'],
'cached': True # Был кеширован
}
# Новый платёж
try:
transaction_id = self._charge_card(user_id, amount)
# Сохраняем в БД с idempotency_key
self.db.transactions.insert_one({
'id': transaction_id,
'user_id': user_id,
'amount': amount,
'idempotency_key': idempotency_key,
'status': 'completed',
'created_at': datetime.now()
})
return {
'status': 'success',
'transaction_id': transaction_id,
'cached': False # Новый платёж
}
except PaymentError as e:
return {
'status': 'error',
'message': str(e),
'cached': False
}
Неидемпотентные методы POST и PATCH требуют особого внимания при проектировании API. Всегда используй Idempotency-Key для критичных операций (платежи, заказы, переводы денег).