Сегодня мы перейдем к интересной теме: пользовательские ошибки. Это те ошибки, которые возникают не из-за работы веб-сервера, а потому что ваш пользователь либо не выполнил бизнес-логику, либо допустил ошибку (ну или просто решил сломать ваш API, кто знает?).
Прежде чем погрузиться в код, давайте немного поговорим о том, что в принципе означает термин "пользовательская ошибка".
Представьте супермаркеты в реальной жизни (те самые, где есть кассы и тележки). Если покупатель подходит к кассе с банковской картой, на которой недостаточно средств, это не "ошибка кассы". Это ошибка покупателя по бизнес-логике системы – покупка невозможна. Перенеся эту логику в веб-приложение, мы можем выдать пользователю ошибку вида: "Недостаточно средств на счету".
В FastAPI, пользовательские ошибки позволяют нам:
- Управлять логикой приложения.
- Сообщать пользователю, что именно пошло не так (человеческим языком).
- Гарантировать возврат предсказуемого формата ошибки.
Таким образом, пользовательские ошибки – это не страшные сообщения об авариях, а полезные сигналы, которые помогают пользователю и разработчику понять, что конкретно произошло.
Обработка пользовательских ошибок в 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"}
Здесь мы:
- Создали пользовательское исключение
BusinessLogicError. - Настроили его обработчик с помощью
@app.exception_handler. - Возвращаем понятный пользователю ответ с 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.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ