JavaRush /Курсы /Модуль 4: FastAPI /Как обрабатывать пользовательские ошибки в FastAPI

Как обрабатывать пользовательские ошибки в FastAPI

Модуль 4: FastAPI
20 уровень , 2 лекция
Открыта

Сегодня мы перейдем к интересной теме: пользовательские ошибки. Это те ошибки, которые возникают не из-за работы веб-сервера, а потому что ваш пользователь либо не выполнил бизнес-логику, либо допустил ошибку (ну или просто решил сломать ваш API, кто знает?).

Прежде чем погрузиться в код, давайте немного поговорим о том, что в принципе означает термин "пользовательская ошибка".

Представьте супермаркеты в реальной жизни (те самые, где есть кассы и тележки). Если покупатель подходит к кассе с банковской картой, на которой недостаточно средств, это не "ошибка кассы". Это ошибка покупателя по бизнес-логике системы – покупка невозможна. Перенеся эту логику в веб-приложение, мы можем выдать пользователю ошибку вида: "Недостаточно средств на счету".

В FastAPI, пользовательские ошибки позволяют нам:

  1. Управлять логикой приложения.
  2. Сообщать пользователю, что именно пошло не так (человеческим языком).
  3. Гарантировать возврат предсказуемого формата ошибки.

Таким образом, пользовательские ошибки – это не страшные сообщения об авариях, а полезные сигналы, которые помогают пользователю и разработчику понять, что конкретно произошло.


Обработка пользовательских ошибок в FastAPI

FastAPI предоставляет удобный способ обработки пользовательских ошибок через механизм встроенных HTTP-исключений и его поддержку кастомных ошибок.

Использование встроенного HTTPException

FastAPI обладает встроенным исключением HTTPException, которое можно легко использовать для возврата ошибок с определенными статус-кодами и сообщениями. Вот пример:


from fastapi import FastAPI, HTTPException

app = FastAPI()

@app.get("/items/{item_id}")
async def read_item(item_id: int):
    if item_id == 42:
        raise HTTPException(
            status_code=404,
            detail="Item not found"
        )
    return {"item_id": item_id, "name": "Super Item"}

В этом коде, если пользователь попытается запросить item_id=42, мы выбросим ошибку 404 Not Found с сообщением "Item not found".

Теперь он не будет ожидаемо возиться с пустым JSON, не понимая причину, а получит лаконичное объяснение.

Что делает HTTPException под капотом?

Когда мы бросаем исключение HTTPException, FastAPI:

  • Перехватывает его.
  • Генерирует правильный HTTP-ответ с указанным статус-кодом.
  • Вставляет поле detail в JSON-ответ, чтобы пользователь понял, что пошло не так.

Добавляем кастомные данные в HTTPException

Если вам нужно отправить дополнительную информацию, такую как код ошибки или рекомендации для устранения, HTTPException позволяет это сделать:


from fastapi import FastAPI, HTTPException

app = FastAPI()

@app.get("/users/{username}")
async def get_user(username: str):
    if username != "admin":
        raise HTTPException(
            status_code=403,
            detail="Access forbidden: you need admin privileges.",
            headers={"X-Error": "AdminRequired"}
        )
    return {"username": username, "role": "Administrator"}

В данном случае, если пользователь – не админ, мы возвращаем ошибку 403 (Forbidden) с дополнительным заголовком X-Error: AdminRequired. Это полезно, если ваш фронтенд хочет анализировать заголовки ответа.


Создание собственных пользовательских ошибок

Иногда встроенных исключений недостаточно, и вам хочется кастомизировать поведение приложения. На помощь приходят пользовательские классы исключений.

Пользовательские исключения на базе Python

Пользовательские ошибки могут быть реализованы с помощью стандартного механизма исключений Python:


class BusinessLogicError(Exception):
    def __init__(self, name: str):
        self.name = name

Теперь мы можем "бросать" это исключение в коде:


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

app = FastAPI()

class BusinessLogicError(Exception):
    def __init__(self, name: str):
        self.name = name

@app.exception_handler(BusinessLogicError)
async def business_logic_error_handler(request, exc: BusinessLogicError):
    return JSONResponse(
        status_code=400,
        content={"error": f"Business logic violated: {exc.name}"}
    )

@app.get("/validate")
async def validate_action(action: str):
    if action != "allowed":
        raise BusinessLogicError(name=f"Action '{action}' is not permitted.")
    return {"msg": "Success"}

Здесь мы:

  1. Создали пользовательское исключение BusinessLogicError.
  2. Настроили его обработчик с помощью @app.exception_handler.
  3. Возвращаем понятный пользователю ответ с HTTP-статусом 400 Bad Request.

Теперь, если пользователь вызовет /validate с запрещенной командой, он получит JSON вроде:


{
    "error": "Business logic violated: Action 'disallowed' is not permitted."
}

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

А теперь создадим небольшой эндпоинт, который будет проверять, достаточно ли средств на счету.


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

app = FastAPI()

class InsufficientFundsError(Exception):
    def __init__(self, account_id: int, balance: float, required: float):
        self.account_id = account_id
        self.balance = balance
        self.required = required

@app.exception_handler(InsufficientFundsError)
async def insufficient_funds_handler(request, exc: InsufficientFundsError):
    return JSONResponse(
        status_code=400,
        content={
            "account_id": exc.account_id,
            "balance": exc.balance,
            "required": exc.required,
            "error": "Insufficient funds"
        }
    )

@app.post("/withdraw/{account_id}")
async def withdraw(account_id: int, amount: float):
    # Пример данных для счета
    fake_account_data = {"balance": 100.0}  # Вместо этого вы бы использовали базу данных

    if amount > fake_account_data["balance"]:
        raise InsufficientFundsError(
            account_id=account_id,
            balance=fake_account_data["balance"],
            required=amount
        )
    # Здесь логика для успешного снятия денег
    return {"status": "Withdrawal successful"}

Примерный ответ при недостатке средств:


{
    "account_id": 1,
    "balance": 100.0,
    "required": 200.0,
    "error": "Insufficient funds"
}

Ошибки, которые делают жизнь веселее

Важно помнить, что при бросании пользовательских ошибок мы должны:

  • Четко понимать, какой HTTP-код будет соответствовать нашей ошибке (не делайте 403 для всех ошибок).
  • Информативно называть поле detail или другие содержательные поля.
  • Логировать такие ошибки, чтобы впоследствии вы могли помочь пользователям или отладить свою бизнес-логику.

Также следите, чтобы кастомные обработчики ошибок не вводили новых багов. Например, не перехватывайте все исключения подряд, иначе вы рискуете скрыть настоящие ошибки вашей программы.


Резюме

Обработка пользовательских ошибок пригодится вам в реальной жизни, когда нужно дать человеку место "подразумевать", что он сделал не так. Это также демонстрирует зрелость вашего программного решения, ведь пользователи ценят понимание и прозрачность, а работодатели — вашу способность управлять сложной логикой через предсказуемые процессы.

На собеседованиях этот опыт может стать хорошим аргументом в пользу вашей квалификации как разработчика.

Об обработчиках ошибок можно почитать подробнее в официальной документации FastAPI.

1
Задача
Модуль 4: FastAPI, 20 уровень, 2 лекция
Недоступна
Обработка запроса с использованием HTTPException
Обработка запроса с использованием HTTPException
1
Задача
Модуль 4: FastAPI, 20 уровень, 2 лекция
Недоступна
Пользовательская ошибка для проверки доступа
Пользовательская ошибка для проверки доступа
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ