Какие знаешь идемпотентные HTTP-методы?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Идемпотентные 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 всегда думай о том, что сеть может потеряться, и идемпотентность помогает сделать систему надёжной.