Что такое идемпотентность запросов http?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Идемпотентность HTTP запросов
Определение
Идемпотентность — это свойство операции, при котором повторное выполнение той же операции даёт тот же результат, что и первое выполнение.
В контексте HTTP: идемпотентный запрос можно выполнить несколько раз без каких-либо побочных эффектов или изменений на сервере.
Первый запрос: GET /user/1 → возвращает Иванова
Второй запрос: GET /user/1 → возвращает Иванова
Третий запрос: GET /user/1 → возвращает Иванова
^ это идемпотентно (результат не меняется)
---
Первый запрос: POST /transfer {from: A, to: B, amount: 100} → $100 переведено
Второй запрос: POST /transfer {from: A, to: B, amount: 100} → $100 переведено ЕЩЁ РАЗ
Третий запрос: POST /transfer {from: A, to: B, amount: 100} → $100 переведено ЕЩЁ РАЗ
^ это НЕ идемпотентно (результат меняется с каждым запросом)
Идемпотентные HTTP методы
GET — идемпотентный
import requests
# GET не меняет состояние сервера
response1 = requests.get("https://api.example.com/user/1")
response2 = requests.get("https://api.example.com/user/1")
response3 = requests.get("https://api.example.com/user/1")
# Все три запроса вернут одинаковый результат
print(response1.json() == response2.json() == response3.json()) # True
DELETE — идемпотентный
# DELETE идемпотентен
response1 = requests.delete("https://api.example.com/user/1") # Удаляет пользователя
# Status: 200 OK
response2 = requests.delete("https://api.example.com/user/1") # Пользователь уже удален
# Status: 404 Not Found (идемпотентное поведение)
response3 = requests.delete("https://api.example.com/user/1") # Всё ещё нет
# Status: 404 Not Found
# Результат: пользователь удален после первого запроса
# Последующие запросы не меняют состояние (идемпотентность)
PUT — идемпотентный
# PUT полностью заменяет ресурс (идемпотентно)
data = {"name": "Ivan", "age": 30}
response1 = requests.put(
"https://api.example.com/user/1",
json=data
) # Создаёт/обновляет пользователя с этими данными
# Status: 200 OK или 201 Created
response2 = requests.put(
"https://api.example.com/user/1",
json=data
) # Тот же запрос — тот же результат
# Status: 200 OK
response3 = requests.put(
"https://api.example.com/user/1",
json=data
) # Опять тот же результат
# Status: 200 OK
print(response1.json() == response2.json()) # True
Неидемпотентные методы
POST — НЕ идемпотентный
# POST часто создаёт новые ресурсы (каждый раз новый)
data = {"message": "Hello", "timestamp": "2024-03-23"}
response1 = requests.post(
"https://api.example.com/messages",
json=data
) # Создаёт сообщение с ID=1
# {"id": 1, "message": "Hello", "timestamp": "2024-03-23"}
response2 = requests.post(
"https://api.example.com/messages",
json=data
) # Создаёт НОВОЕ сообщение с ID=2
# {"id": 2, "message": "Hello", "timestamp": "2024-03-23"}
response3 = requests.post(
"https://api.example.com/messages",
json=data
) # Создаёт ЕЩЁ НОВОЕ сообщение с ID=3
# {"id": 3, "message": "Hello", "timestamp": "2024-03-23"}
# Результаты разные! POST не идемпотентен
print(response1.json() == response2.json()) # False (разные ID)
PATCH — НЕ идемпотентный
# PATCH обновляет ЧАСТь ресурса (может быть неидемпотентным)
update = {"age": 31} # Увеличиваем возраст на 1
response1 = requests.patch(
"https://api.example.com/user/1",
json=update
) # age: 30 → 31
response2 = requests.patch(
"https://api.example.com/user/1",
json=update
) # age: 31 → 32 (меняется!)
response3 = requests.patch(
"https://api.example.com/user/1",
json=update
) # age: 32 → 33 (меняется ещё!)
# PATCH неидемпотентен, потому что обновляет ЧАСТЬ ресурса
print(response1.json()["age"]) # 31
print(response2.json()["age"]) # 32
print(response3.json()["age"]) # 33
Таблица идемпотентности
| Метод | Идемпотентен | Кэшируемый | Безопасный |
|---|---|---|---|
| GET | ✓ Да | ✓ Да | ✓ Да |
| HEAD | ✓ Да | ✓ Да | ✓ Да |
| OPTIONS | ✓ Да | ✗ Нет | ✓ Да |
| DELETE | ✓ Да | ✗ Нет | ✗ Нет |
| PUT | ✓ Да | ✗ Нет | ✗ Нет |
| POST | ✗ Нет | ✗ Нет | ✗ Нет |
| PATCH | ✗ Нет | ✗ Нет | ✗ Нет |
Проблема сетевых сбоев
Идемпотентность критична для надёжности:
# Сценарий: пользователь отправляет платёж
response = requests.post(
"https://api.example.com/transfer",
json={"from": "A", "to": "B", "amount": 100},
timeout=5
)
# Но сеть не стабильна!
# Запрос был отправлен, но ответ не пришёл
# Клиент не знает: платёж прошёл или нет?
# ❌ Неправильное решение: повторить запрос
# Может быть 200$, 300$, 400$ и больше переведено!
# ✅ Правильное решение: использовать идемпотентный ключ
headers = {
"Idempotency-Key": "transfer-2024-03-23-001" # Уникальный ключ
}
response = requests.post(
"https://api.example.com/transfer",
json={"from": "A", "to": "B", "amount": 100},
headers=headers
)
# Повторный запрос с тем же ключом:
response_retry = requests.post(
"https://api.example.com/transfer",
json={"from": "A", "to": "B", "amount": 100},
headers=headers # Тот же ключ!
)
# Сервер видит, что это повторение
# Возвращает ТОТ ЖЕ результат, не выполняя операцию ещё раз
Реализация идемпотентности на сервере
from fastapi import FastAPI, Header
from typing import Optional
import uuid
app = FastAPI()
# БД для отслеживания идемпотентных ключей
IDEMPOTENT_KEYS = {} # {key: result}
@app.post("/transfer")
async def transfer(
from_account: str,
to_account: str,
amount: float,
idempotency_key: Optional[str] = Header(None)
):
"""Идемпотентный трансфер денег"""
# Если нет ключа, генерируем
if not idempotency_key:
idempotency_key = str(uuid.uuid4())
# Проверяем, был ли уже такой запрос
if idempotency_key in IDEMPOTENT_KEYS:
# Возвращаем сохранённый результат
return {"status": "success", "result": IDEMPOTENT_KEYS[idempotency_key]}
# Первый раз: выполняем операцию
try:
result = {
"from": from_account,
"to": to_account,
"amount": amount,
"transaction_id": str(uuid.uuid4())
}
# Выполняем трансфер (в реальности в БД)
# db.transfer(from_account, to_account, amount)
# Сохраняем результат
IDEMPOTENT_KEYS[idempotency_key] = result
return {"status": "success", "result": result}
except Exception as e:
# В случае ошибки НЕ сохраняем ключ
# Клиент может повторить
return {"status": "error", "error": str(e)}
Best Practices для идемпотентности
✓ Используйте правильные HTTP методы:
- GET для чтения
- PUT для полного обновления
- DELETE для удаления
- POST только если нужно создать новый ресурс
✓ Для критических операций используйте Idempotency-Key:
headers = {"Idempotency-Key": "unique-key-per-request"}
✓ На сервере сохраняйте результаты идемпотентных запросов:
# Проверяем кеш перед выполнением
if key in cache:
return cache[key]
# Выполняем
result = execute()
cache[key] = result
return result
✓ Очищайте старые ключи:
# Идемпотентные ключи можно хранить 24 часа
# Потом удалять (экономия памяти)
✓ Документируйте идемпотентность в API:
POST /transfer
- Идемпотентен: нет
- Требует: Idempotency-Key header
Реальные примеры
Stripe API — платёжная система (очень критична идемпотентность):
# https://stripe.com/docs/api/idempotent_requests
headers = {"Idempotency-Key": "py_test_abc123"}
response = requests.post(
"https://api.stripe.com/v1/charges",
headers=headers,
data={...}
)
AWS API — облачные сервисы:
# AWS требует ClientRequestToken для идемпотентности
response = client.run_instances(
ImageId='ami-12345678',
ClientToken='my-unique-token-2024-03-23'
)
# Второй запрос с тем же токеном вернёт существующие инстансы
Итог
Идемпотентность — это критическое свойство для надёжных API:
- GET, DELETE, PUT — идемпотентны (безопасны для повторения)
- POST, PATCH — не идемпотентны (каждое выполнение может быть разным)
- Idempotency-Key — решение для критических операций (платежи, трансферы)
- Сохранение результатов — на сервере нужно помнить результаты идемпотентных запросов
Если не учитывать идемпотентность, при сетевых сбоях могут возникнуть дублирования операций (двойное списание денег, создание дубликатов и т.д.).