Обработка ошибок и HTTPException
HTTPException — это способ прервать обработку и вернуть клиенту корректный HTTP-статус с понятным сообщением; кастомные обработчики позволяют единообразно превращать любые исключения в ответы.
Ошибка — это тоже часть API-контракта. Клиент должен получить не «500 что-то сломалось», а осмысленный код (404, 403, 409) и структурированное тело с описанием проблемы.
Когда ресурс не найден, прав не хватает или данные конфликтуют, нужно сообщить об этом честно. Бросать обычное Exception нельзя — оно превратится в 500. Вместо этого FastAPI предлагает HTTPException с нужным кодом и деталями. Фреймворк поймает его и сформирует корректный ответ с правильным статусом и JSON-телом.
from fastapi import FastAPI, HTTPException, status
app = FastAPI()
@app.get("/users/{user_id}")
async def get_user(user_id: int):
user = db.get(user_id)
if user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Пользователь {user_id} не найден",
)
return user
# кастомный обработчик исключения домена
from fastapi.responses import JSONResponse
class OutOfStock(Exception):
pass
@app.exception_handler(OutOfStock)
async def out_of_stock_handler(request, exc):
return JSONResponse(status_code=409, content={"detail": "товар закончился"})
Два уровня работы с ошибками. Точечно — raise HTTPException(...) прямо в обработчике для конкретного случая. Системно — @app.exception_handler(SomeError) ловит исключения определённого типа по всему приложению и единообразно превращает их в ответы. Второй подход хорош для доменных исключений: бизнес-логика бросает OutOfStock, не зная про HTTP, а обработчик переводит это в код 409.
Как работает под капотом
FastAPI оборачивает обработку запроса в перехват исключений: поймал HTTPException — взял его status_code и detail, собрал JSON-ответ; поймал тип с зарегистрированным обработчиком — вызвал его. Смоделируем диспетчеризацию ошибок на stdlib:
class HTTPException(Exception):
def __init__(self, status_code, detail):
self.status_code, self.detail = status_code, detail
class OutOfStock(Exception):
pass
handlers = {OutOfStock: lambda exc: (409, {"detail": "товар закончился"})}
def process(handler):
try:
return handler()
except HTTPException as e: # встроенный перехват
return e.status_code, {"detail": e.detail}
except Exception as e:
for typ, fn in handlers.items(): # кастомные обработчики
if isinstance(e, typ):
return fn(e)
return 500, {"detail": "Internal Server Error"}
print(process(lambda: (_ for _ in ()).throw(HTTPException(404, "не найдено"))))
print(process(lambda: (_ for _ in ()).throw(OutOfStock())))
print(process(lambda: {"ok": True}))
Попробуй сам ▶ Три исхода: HTTPException → точный статус, доменное исключение → кастомный обработчик, успех → результат. Так FastAPI и маршрутизирует ошибки.
Частые ошибки
Первая — бросать «голое» Exception вместо HTTPException, отдавая клиенту 500 вместо осмысленного кода. Вторая — возвращать ошибку в произвольном формате, ломая единый контракт (клиент ждёт поле detail). Третья — раскрывать в detail внутренние подробности (трассировки, SQL), что небезопасно. Четвёртая — использовать неподходящие коды (404 там, где нужен 403, или 400 вместо 422).
Best practices
- Точечные случаи —
HTTPExceptionс правильным кодом и человекочитаемымdetail. - Доменные исключения переводите в HTTP через
@app.exception_handler, не смешивая бизнес-логику с HTTP. - Не раскрывайте внутренние детали в ответах об ошибке.
- Подбирайте коды по семантике: 404 — нет ресурса, 403 — нет прав, 409 — конфликт, 422 — невалидные данные.
Глобальная нормализация ошибок
Зрелое API отдаёт все ошибки в едином формате, чтобы клиент мог обрабатывать их одинаково. Для этого переопределяют обработчики не только своих доменных исключений, но и встроенных: обработчик ошибок валидации (RequestValidationError) и общий обработчик HTTPException. Так вы приводите и 422 от Pydantic, и ваши 404/403, и неожиданные сбои к одной структуре — например, { "error": { "code": ..., "message": ..., "details": ... } }. Это резко упрощает фронтенд, которому больше не нужно угадывать форму ответа по коду. Отдельно стоит позаботиться о непредвиденных исключениях: их ловят на верхнем уровне, логируют с трассировкой (но не показывают её клиенту) и отдают обезличенную 500. Грамотная обработка ошибок — это не разрозненные raise по коду, а продуманная политика, где каждый класс проблем имеет понятный, единообразный и безопасный ответ.
Итог: ошибки — часть контракта. HTTPException даёт точечные корректные ответы, а кастомные обработчики единообразно переводят доменные исключения в HTTP. Подбирайте коды осмысленно и не раскрывайте внутренности.