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

Что должно содержаться в запросе для поддержки stateless взаимодействия?

2.0 Middle🔥 71 комментариев
#Python Core#Soft Skills#Архитектура и паттерны

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

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

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

Stateless взаимодействие: что должно содержаться в запросе

Stateless архитектура — это фундаментальный принцип REST. Разберу подробно, что должно включаться в запрос и почему это важно.

Что такое stateless взаимодействие?

Stateless означает, что каждый запрос содержит всю необходимую информацию для обработки. Сервер не хранит контекст запроса между вызовами.

Противоположность: Stateful взаимодействие

# Плохо: Stateful (сервер помнит состояние)

session = {}

@app.post("/login")
def login(credentials):
    # Сохраняем состояние на сервере
    session['user_id'] = 123
    session['username'] = 'john'
    session['role'] = 'admin'
    return {"message": "Logged in"}

@app.get("/profile")
def get_profile():
    # Полагаемся на состояние, сохраненное ранее
    user_id = session.get('user_id')
    if not user_id:
        return {"error": "Not logged in"}
    return {"user_id": user_id}

# Проблемы:
# - Нельзя масштабировать (если 2+ сервера)
# - Если сервер перезагрузится, сессия потеряется
# - Сложно с клиентской стороны отследить состояние

Что должно содержаться в stateless запросе?

1. Идентификация (Authentication)

# Запрос должен содержать информацию о том, кто совершает запрос

from fastapi import FastAPI, Header, HTTPException
import jwt

app = FastAPI()
SECRET_KEY = "your-secret-key"

# Вариант 1: Bearer Token (рекомендуется)
@app.get("/api/v1/profile")
def get_profile(authorization: str = Header(None)):
    """
    Запрос должен содержать:
    Authorization: Bearer <JWT_TOKEN>
    
    Сервер декодирует JWT и узнает:
    - user_id
    - username
    - role
    - expiration
    """
    if not authorization:
        raise HTTPException(status_code=401, detail="Missing token")
    
    try:
        token = authorization.replace("Bearer ", "")
        payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
        user_id = payload['user_id']
        return {"user_id": user_id, "username": payload['username']}
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail="Invalid token")

# Вариант 2: API Key
@app.get("/api/v1/data")
def get_data(api_key: str = Header(None)):
    """
    Запрос должен содержать:
    X-API-Key: sk_live_abc123def456
    """
    if not api_key or not validate_api_key(api_key):
        raise HTTPException(status_code=401, detail="Invalid API key")
    return {"data": "sensitive information"}

2. Контекст операции

# Запрос должен содержать всю информацию о том, что нужно сделать

@app.post("/api/v1/orders")
def create_order(
    authorization: str = Header(...),
    body: dict  # JSON body
):
    """
    Полный запрос для создания заказа:
    
    POST /api/v1/orders
    Authorization: Bearer <token>
    Content-Type: application/json
    
    {
        "user_id": 123,
        "items": [
            {
                "product_id": 456,
                "quantity": 2,
                "price": 29.99
            }
        ],
        "delivery_address": "123 Main St",
        "delivery_method": "express",
        "payment_method": "credit_card"
    }
    
    Сервер получает ВСЕ данные и может обработать заказ,
    не обращаясь к памяти сессии или другим источникам контекста
    """
    user_id = extract_user_from_token(authorization)
    
    order = {
        'user_id': user_id,
        'items': body['items'],
        'delivery_address': body['delivery_address'],
        'delivery_method': body['delivery_method']
    }
    
    # Обработка заказа на основе полной информации из запроса
    save_order(order)
    return {"order_id": 789}

3. Параметры и фильтры

# Все параметры фильтрации должны быть в запросе

from fastapi import Query

@app.get("/api/v1/orders")
def list_orders(
    authorization: str = Header(...),
    skip: int = Query(0),
    limit: int = Query(10),
    status: str = Query(None),
    sort_by: str = Query("created_at"),
    sort_order: str = Query("desc")
):
    """
    GET /api/v1/orders?skip=0&limit=10&status=pending&sort_by=created_at&sort_order=desc
    
    Все параметры фильтрации и сортировки включены в URL.
    Сервер не помнит предыдущий запрос.
    Если клиент отправит этот же URL, результат будет идентичен.
    """
    user_id = extract_user_from_token(authorization)
    
    filters = {
        'user_id': user_id,
        'skip': skip,
        'limit': limit,
        'status': status,
        'sort_by': sort_by,
        'sort_order': sort_order
    }
    
    return get_orders_filtered(filters)

4. Идемпотентность (Idempotency Key)

import uuid
from datetime import datetime

# Для операций, которые нельзя выполнять дважды

@app.post("/api/v1/payments")
def process_payment(
    authorization: str = Header(...),
    idempotency_key: str = Header(...),  # Важно для stateless!
    body: dict
):
    """
    POST /api/v1/payments
    Authorization: Bearer <token>
    Idempotency-Key: unique-key-12345
    
    {
        "amount": 100.00,
        "currency": "USD",
        "payment_method": "credit_card"
    }
    
    Проблема: если клиент отправит запрос дважды,
    платеж может быть обработан дважды.
    
    Решение: сохраняем idempotency_key и результат.
    Если видим уже сохраненный ключ, возвращаем старый результат.
    """
    # Проверка: был ли уже обработан этот запрос?
    existing_payment = get_payment_by_idempotency_key(idempotency_key)
    if existing_payment:
        return existing_payment  # Возвращаем сохраненный результат
    
    # Новый платеж
    user_id = extract_user_from_token(authorization)
    payment = {
        'user_id': user_id,
        'amount': body['amount'],
        'idempotency_key': idempotency_key,
        'created_at': datetime.utcnow()
    }
    
    result = charge_payment(payment)
    save_payment_result(idempotency_key, result)
    
    return result

5. Информация об API версии

# Запрос должен указывать, какую версию API использует

@app.get("/api/v1/users/{user_id}")
def get_user_v1(user_id: int, authorization: str = Header(...)):
    """
    Версия указана в URL: /api/v1/
    
    Если выпустим v2 с другим форматом,
    старые клиенты продолжат использовать v1.
    """
    return {"user_id": user_id, "name": "John"}

@app.get("/api/v2/users/{user_id}")
def get_user_v2(user_id: int, authorization: str = Header(...)):
    """
    Новая версия API с другим форматом
    """
    return {
        "id": user_id,
        "profile": {"name": "John", "email": "john@example.com"}
    }

6. Временные метки и версионирование

from datetime import datetime

@app.put("/api/v1/users/{user_id}")
def update_user(
    user_id: int,
    authorization: str = Header(...),
    body: dict
):
    """
    PUT /api/v1/users/123
    
    {
        "name": "Jane",
        "email": "jane@example.com",
        "version": 2  # Оптимистичная блокировка
    }
    
    Запрос содержит версию объекта для обнаружения конфликтов.
    Если на сервере уже есть версия 3, обновление отклоняется.
    Это позволяет избежать потери изменений в распределённой системе.
    """
    user_id_from_token = extract_user_from_token(authorization)
    
    # Проверка версии
    current_user = get_user(user_id)
    if current_user['version'] != body.get('version'):
        return {
            "error": "Conflict",
            "current_version": current_user['version'],
            "status_code": 409
        }
    
    # Обновление
    updated_user = {
        **current_user,
        **body,
        'version': current_user['version'] + 1,
        'updated_at': datetime.utcnow()
    }
    
    save_user(updated_user)
    return updated_user

Пример полного stateless запроса

# Реальный пример: работа с заказами

from fastapi import FastAPI, Header, HTTPException
import jwt

app = FastAPI()
SECRET_KEY = "secret"

@app.post("/api/v1/users/{user_id}/orders")
def create_order(
    user_id: int,
    authorization: str = Header(...),
    idempotency_key: str = Header(...),
    body: dict
):
    """
    POST /api/v1/users/123/orders
    Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc...
    Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
    Content-Type: application/json
    
    {
        "items": [
            {"product_id": 456, "quantity": 2},
            {"product_id": 789, "quantity": 1}
        ],
        "delivery_address": "123 Main St",
        "delivery_date": "2024-03-30",
        "gift_message": "Happy birthday!"
    }
    
    Запрос ПОЛНОСТЬЮ автономен:
    1. Authorization содержит информацию о пользователе
    2. Idempotency-Key гарантирует идемпотентность
    3. Body содержит все данные для создания заказа
    4. URL указывает на конкретного пользователя и v1 API
    5. Сервер НЕ полагается на память сессии
    
    Результат: сервер может быть перезагружен, масштабирован,
    клиент может повторить запрос и получить идентичный результат.
    """
    
    # 1. Валидация токена
    try:
        token = authorization.replace("Bearer ", "")
        payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
        token_user_id = payload['user_id']
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail="Invalid token")
    
    # 2. Проверка совпадения user_id
    if token_user_id != user_id:
        raise HTTPException(status_code=403, detail="Forbidden")
    
    # 3. Проверка идемпотентности
    existing_order = get_order_by_idempotency_key(idempotency_key)
    if existing_order:
        return existing_order
    
    # 4. Создание заказа
    order = {
        'user_id': user_id,
        'items': body['items'],
        'delivery_address': body['delivery_address'],
        'delivery_date': body['delivery_date'],
        'gift_message': body.get('gift_message'),
        'idempotency_key': idempotency_key,
        'status': 'pending'
    }
    
    order_id = save_order(order)
    
    return {
        "order_id": order_id,
        "status": "pending",
        "created_at": datetime.utcnow().isoformat()
    }

Чеклист для stateless запроса

stateless_checklist = {
    "authentication": {
        "✓": "Запрос содержит credentials (token, API key)",
        "✗": "Сервер ищет сессию в памяти"
    },
    
    "complete_context": {
        "✓": "Все данные для обработки в самом запросе",
        "✗": "Сервер должен искать что-то в БД (кроме валидации)"
    },
    
    "idempotency": {
        "✓": "Есть Idempotency-Key для операций изменения",
        "✗": "Одинаковый запрос даст разные результаты"
    },
    
    "versioning": {
        "✓": "API версия указана в URL или заголовке",
        "✗": "Клиент и сервер полагаются на подразумеваемую версию"
    },
    
    "parameters": {
        "✓": "Все фильтры и параметры в URL/body/headers",
        "✗": "Состояние фильтрации хранится на сервере"
    },
    
    "reproducibility": {
        "✓": "Одинаковый запрос → одинаковый результат",
        "✗": "Результат зависит от истории предыдущих запросов"
    }
}

Почему stateless важен?


benefits = {
    "scalability": "Любой сервер может обработать любой запрос",
    "reliability": "Сбой одного сервера не потеряет контекст",
    "caching": "Легко кэшировать (каждый запрос независим)",
    "distribution": "Работает с load balancer и multiple servers",
    "testing": "Легко тестировать (нет зависимостей от порядка)",
    "debugging": "Логи одного запроса полностью описывают операцию"
}

Заключение

Stateless запрос должен содержать:

  1. Аутентификацию (кто совершает запрос)
  2. Контекст операции (что нужно сделать)
  3. Все параметры (все фильтры и настройки)
  4. Идемпотентность (для небезопасных операций)
  5. Версию API (для обратной совместимости)
  6. Всё необходимое — сервер ничего не помнит

Это основа масштабируемых, надёжных REST API.