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

Является ли запрос по шаблону idempotent?

2.0 Middle🔥 301 комментариев
#Безопасность#Тестирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Идемпотентность запросов (Idempotency)

Идемпотентный запрос — это запрос, который может быть выполнен несколько раз подряд с одинаковым результатом, без изменения состояния сервера или получения разных ответов. Это критически важное понятие для проектирования надёжных API и обработки сетевых ошибок.

Определение идемпотентности

Запрос идемпотентен, если:

  1. Первое выполнение даёт результат X
  2. Второе выполнение даёт результат X
  3. N-е выполнение даёт результат X
  4. Состояние сервера остаётся неизменным

HTTP методы и идемпотентность

Идемпотентные методы:

# GET — только читает данные, ничего не меняет
GET /users/123
GET /users/123  # Второй раз вернёт то же самое

# PUT — заменяет ресурс полностью (предсказуемый результат)
PUT /users/123
{
    "name": "John",
    "email": "john@example.com"
}
# Выполнено 1 раз или 5 раз — результат одинаковый

# DELETE — удаляет ресурс (идемпотентен)
DELETE /users/123
DELETE /users/123  # Второй раз вернёт 404, но состояние сервера не изменится

# HEAD — только проверяет, ничего не меняет
HEAD /users/123

Не идемпотентные методы:

# POST — создаёт новый ресурс каждый раз
POST /users
{
    "name": "John"
}
# Первый раз создаёт пользователя с id=1
# Второй раз создаёт пользователя с id=2
# Результаты разные!

# PATCH — изменяет ресурс частично (может быть неидемпотентным)
PATCH /users/123
{
    "age": +1  # Увеличить возраст на 1
}
# Первый раз age становится 31
# Второй раз age становится 32
# Результаты разные!

Примеры в коде

Идемпотентный API (FastAPI):

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()
users_db = {}

class User(BaseModel):
    id: int
    name: str

# GET — идемпотентен
@app.get("/users/{user_id}")
def get_user(user_id: int):
    if user_id not in users_db:
        raise HTTPException(status_code=404)
    return users_db[user_id]

# PUT — идемпотентен (полностью заменяет)
@app.put("/users/{user_id}")
def update_user(user_id: int, user: User):
    users_db[user_id] = user
    return users_db[user_id]

# DELETE — идемпотентен (удаляет и всё)
@app.delete("/users/{user_id}")
def delete_user(user_id: int):
    if user_id in users_db:
        del users_db[user_id]
    return {"deleted": True}

# POST — НЕ идемпотентен (создаёт каждый раз)
@app.post("/users")
def create_user(user: User):
    users_db[user.id] = user
    return users_db[user.id]

Проблема: как сделать POST идемпотентным

В реальных системах часто нужно сделать POST идемпотентным (например, при повторе платежа). Решение — использовать Idempotency Key:

from fastapi import Header, HTTPException
from typing import Optional

idempotency_store = {}  # В реальности — база данных

@app.post("/payments")
def create_payment(
    amount: float,
    idempotency_key: Optional[str] = Header(None)
):
    # Если мы уже видели этот ключ — вернуть кэшированный результат
    if idempotency_key in idempotency_store:
        return idempotency_store[idempotency_key]
    
    # Иначе обработать платёж
    result = {
        "payment_id": 12345,
        "amount": amount,
        "status": "completed"
    }
    
    # Кэшировать результат
    if idempotency_key:
        idempotency_store[idempotency_key] = result
    
    return result

Клиент отправляет Idempotency Key:

import requests
import uuid

idempotency_key = str(uuid.uuid4())

# Первый запрос
response1 = requests.post(
    "http://api.example.com/payments",
    json={"amount": 100},
    headers={"Idempotency-Key": idempotency_key}
)

# Второй запрос с тем же ключом
response2 = requests.post(
    "http://api.example.com/payments",
    json={"amount": 100},
    headers={"Idempotency-Key": idempotency_key}
)

# response1.json() == response2.json()
assert response1.json() == response2.json()

Практическое применение

Обработка сетевых ошибок:

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

# Идемпотентные запросы можно безопасно повторять
session = requests.Session()
retry = Retry(
    total=3,
    backoff_factor=1,
    status_forcelist=[500, 502, 503, 504]
)
Adapter = HTTPAdapter(max_retries=retry)
session.mount('http://', Adapter)
session.mount('https://', Adapter)

# GET безопасно повторять
response = session.get("/api/users/123")

# POST нужно как-то защищать (Idempotency Key)
response = session.post("/api/users", json={"name": "John"})

Таблица идемпотентности

МетодИдемпотентенПримеры
GETДаПолучение данных
PUTДаОбновление всего ресурса
DELETEДаУдаление ресурса
PATCHНет*Частичное обновление
POSTНет**Создание нового
HEADДаПроверка доступности
  • PATCH может быть идемпотентным, если замещает поля целиком ** POST может быть идемпотентным с Idempotency Key

Заключение

Понимание идемпотентности критично для:

  • Проектирования надёжных API
  • Обработки сетевых сбоев
  • Безопасности платёжных систем
  • Распределённых систем

Повсегда проверяйте: безопасно ли повторить данный запрос?