← Назад к вопросам

Какие 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 для критичных операций (платежи, заказы, переводы денег).