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

Какие знаешь идемпотентные HTTP-методы?

1.0 Junior🔥 251 комментариев
#REST API и HTTP

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

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

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

Идемпотентные HTTP-методы: когда повторение безопасно

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

Что такое идемпотентность

Идемпотентный метод: f(f(x)) = f(x)

Примеры:
- GET /users/1 → всегда вернёт одного и того же пользователя
- HEAD /users/1 → всегда вернёт одинаковые заголовки
- DELETE /users/1 → первый раз удалит, второй раз вернёт 404 (без ошибки)
- PUT /users/1 → обновит на одно и то же значение (идемпотентно)

Не идемпотентные:
- POST /users → каждый раз создаёт нового пользователя
- PATCH /users/1 → может изменить что-то в зависимости от текущего состояния

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

1. GET — безопасно читать

# GET всегда идемпотентен
import requests

response1 = requests.get("http://api.example.com/users/1")
response2 = requests.get("http://api.example.com/users/1")
# response1 == response2 (одинаковые данные)

# Применение в FastAPI
from fastapi import FastAPI

app = FastAPI()

@app.get("/api/v1/users/{user_id}")
async def get_user(user_id: int):
    user = db.get_user(user_id)
    return user

# Вызвать 1000 раз → вернёт один и тот же результат

2. HEAD — как GET, но без body

GET /resource → возвращает заголовки + body
HEAD /resource → возвращает только заголовки (идентичные GET)

Использование:
- Проверить существует ли ресурс
- Получить размер файла (Content-Length)
- Проверить Last-Modified для кэширования

3. PUT — заменить ресурс (идемпотентно)

# PUT: полная замена ресурса
from fastapi import FastAPI

app = FastAPI()

@app.put("/api/v1/users/{user_id}")
async def update_user(user_id: int, data: UserUpdate):
    # PUT должен быть идемпотентен
    # Первый вызов: обновляет пользователя
    # Второй вызов с теми же данными: не меняет ничего (идемпотентно)
    user = User(id=user_id, name=data.name, email=data.email)
    db.update_user(user_id, user)
    return user

# Практика:
user_data = {"name": "Alice", "email": "alice@example.com"}

# Первый запрос
response1 = requests.put(
    "http://api.example.com/users/1",
    json=user_data
)  # Обновляет

# Второй запрос с теми же данными
response2 = requests.put(
    "http://api.example.com/users/1",
    json=user_data
)  # Никаких изменений, результат идентичен

4. DELETE — удалить ресурс (идемпотентно)

@app.delete("/api/v1/users/{user_id}")
async def delete_user(user_id: int):
    # DELETE идемпотентен
    # Первый раз: удаляет (возвращает 200 или 204)
    # Второй раз: ресурс уже удалён (возвращает 404)
    # Оба случая безопасны
    db.delete_user(user_id)
    return {"status": "deleted"}

# Практика:
# Первый запрос
response1 = requests.delete("http://api.example.com/users/1")  # 200

# Второй запрос (network retry)
response2 = requests.delete("http://api.example.com/users/1")  # 404
# Оба случая приемлемы

5. OPTIONS — получить информацию о методах

OPTIONS /resource → возвращает Allow: GET, POST, PUT, DELETE

Идемпотентен: просто информирует, ничего не меняет

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

1. POST — создать ресурс (НЕ идемпотентен)

@app.post("/api/v1/users")
async def create_user(data: UserCreate):
    # POST НЕ идемпотентен
    # Каждый вызов создаёт нового пользователя
    user = User(name=data.name, email=data.email)
    db.create_user(user)
    return user

# Практика:
user_data = {"name": "Bob", "email": "bob@example.com"}

# Первый запрос
response1 = requests.post(
    "http://api.example.com/users",
    json=user_data
)  # Создаёт пользователя #1

# Второй запрос (network retry)
response2 = requests.post(
    "http://api.example.com/users",
    json=user_data
)  # Создаёт пользователя #2 (дублирование!)

# Проблема: у нас появились два пользователя
# Это может быть ошибкой при нестабильной сети

2. PATCH — частичное обновление (НЕ идемпотентен в общем случае)

@app.patch("/api/v1/users/{user_id}")
async def patch_user(user_id: int, data: dict):
    # PATCH НЕ гарантированно идемпотентен
    # Зависит от того, как реализовано
    
    # Пример 1: НЕ идемпотентен
    user = db.get_user(user_id)
    user.age += data["age_increment"]  # Инкремент
    db.update_user(user_id, user)
    return user
    # Первый раз: age = 30 + 5 = 35
    # Второй раз: age = 35 + 5 = 40 (не идемпотентно!)
    
    # Пример 2: Более идемпотентный подход
    user = db.get_user(user_id)
    if "email" in data:
        user.email = data["email"]  # Присваивание
    db.update_user(user_id, user)
    return user
    # Первый раз: email = "new@example.com"
    # Второй раз: email = "new@example.com" (идемпотентно)

Почему идемпотентность важна

1. Сетевая надёжность

Клиент отправляет DELETE /users/1
                    ↓
        Сервер получил, удалил
                    ↓
Ответ теряется в сети
                    ↓
Клиент не получил ответ, повторяет DELETE /users/1
                    ↓
        Сервер: ресурс уже удалён → 404

Идемпотентность: оба случая безопасны, нет проблем

2. Retry-логика

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

session = requests.Session()

# Настроим автоматические повторы
retry = Retry(
    total=3,
    backoff_factor=0.5,
    status_forcelist=[500, 502, 503, 504],
)
adapter = HTTPAdapter(max_retries=retry)
session.mount('http://', adapter)

# Безопасно повторять идемпотентные методы
response = session.get("http://api.example.com/users/1")  # Безопасно
response = session.put("http://api.example.com/users/1", json=data)  # Безопасно

# Не повторяй POST
# response = session.post("...")  # Опасно без проверки

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

Метод    | Идемпотентный | Кэшируемый | Применение
---------|---------------|-----------|----------------------------------
GET      | Да            | Да        | Получить ресурс
HEAD     | Да            | Да        | Получить метаданные
OPTIONS  | Да            | Нет       | Узнать доступные методы
TRACE    | Да            | Нет       | Отладка маршрута запроса
PUT      | Да            | Нет       | Заменить весь ресурс
DELETE   | Да            | Нет       | Удалить ресурс
POST     | Нет           | Нет       | Создать ресурс
PATCH    | Нет (обычно)  | Нет       | Частичное обновление

Лучшие практики

from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse

app = FastAPI()

# ✅ Идемпотентный DELETE
@app.delete("/api/v1/items/{item_id}")
async def delete_item(item_id: int):
    item = db.get_item(item_id)
    if not item:
        return JSONResponse(status_code=404, content={"detail": "Not found"})
    db.delete_item(item_id)
    return JSONResponse(status_code=204)  # No Content

# ✅ Идемпотентный PUT
@app.put("/api/v1/items/{item_id}")
async def replace_item(item_id: int, new_item: ItemSchema):
    # Полная замена, не зависит от текущего состояния
    db.update_item(item_id, new_item)
    return new_item

# ❌ НЕ идемпотентный POST
@app.post("/api/v1/items")
async def create_item(item: ItemSchema):
    new_item = db.create_item(item)
    return JSONResponse(status_code=201, content=new_item)

# Идемпотентность в асинхронном коде
import asyncio

async def safe_delete_with_retry(item_id: int, max_retries: int = 3):
    for attempt in range(max_retries):
        try:
            result = await delete_item(item_id)  # Идемпотентно
            return result
        except Exception as e:
            if attempt == max_retries - 1:
                raise
            await asyncio.sleep(2 ** attempt)  # Exponential backoff

Заключение

Идемпотентность — это фундаментальный принцип надёжных систем. GET, HEAD, PUT, DELETE идемпотентны и безопасны для повторения. POST и PATCH не идемпотентны и требуют осторожного обращения. При проектировании API всегда думай о том, что сеть может потеряться, и идемпотентность помогает сделать систему надёжной.

Какие знаешь идемпотентные HTTP-методы? | PrepBro