Ядро¶
Слой core самодостаточен: его можно использовать без декоратора и без анализа. Он даёт четыре вещи — класс ошибки BaseError, модель ответа ErrorResponse, конструктор error_models() для responses={} и обработчик handle_base_error.
BaseError¶
Каждая ошибка — подкласс BaseError, параметризованный своим кодом. Код передаётся как Literal в дженерик-параметр; в теле класса объявляется только http_status.
from enum import StrEnum
from http import HTTPStatus
from typing import Literal
from fastapi_typed_errors import BaseError
class ErrorCode(StrEnum):
NOT_FOUND = "NOT_FOUND"
class NotFoundError(BaseError[Literal[ErrorCode.NOT_FOUND]]):
http_status = HTTPStatus.NOT_FOUND
description = "Сущность не найдена" # (1)!
description: ClassVar[str | None]— необязательный; см. detail по умолчанию.
Метакласс BaseErrorMeta при создании класса извлекает код из дженерик-параметра и выводит два атрибута класса:
| Атрибут | Что это |
|---|---|
NotFoundError.error_code |
значение кода (ErrorCode.NOT_FOUND) |
NotFoundError.model |
параметризованная модель ответа ErrorResponse[Literal[NOT_FOUND]] |
BaseError наследует fastapi.HTTPException, поэтому raise NotFoundError(...) работает как обычное исключение FastAPI.
Коды: StrEnum или голый Literal¶
Код — это любой член StrEnum или голая строка в Literal. Bound T: str покрывает оба варианта:
class NotFoundError(BaseError[Literal[ErrorCode.NOT_FOUND]]): ... # член StrEnum
class BareError(BaseError[Literal["BARE_CODE"]]): ... # голая строка
Ошибка параметризации ловится сразу
Параметризация чем-либо, кроме TypeVar (промежуточная база) или Literal с ровно одной строкой, падает с TypeError в момент определения класса, а не опасной 500-й на запросе:
detail по умолчанию¶
Если при подъёме не передать detail, он берётся из description, а при его отсутствии — из фразы HTTP-статуса.
NotFoundError().detail # -> "Сущность не найдена" (из description)
ForbiddenError().detail # -> "Forbidden" (фраза статуса, если description нет)
NotFoundError("нет").detail # -> "нет" (явный detail побеждает)
Заголовки прокидываются в HTTPException:
ErrorResponse¶
Тело ответа — модель ErrorResponse[T] с двумя полями:
to_response() строит её из экземпляра ошибки — это и делает обработчик:
Чистые заголовки в OpenAPI
Pydantic по умолчанию встраивает в тайтл параметризованной модели repr() enum-члена — с угловыми скобками. Пакет ставит model_title_generator, который рендерит ErrorResponse[NOT_FOUND] вместо ErrorResponse[Literal[<ErrorCode.NOT_FOUND: 'NOT_FOUND'>]].
error_models()¶
error_models() строит модель для ключа "model" в responses={} роута.
- Один класс → его параметризованная модель напрямую.
- Несколько классов → discriminated
oneOf-union по полюcode(Swagger покажет выбор по коду).
@app.get(
"/items/{item_id}",
responses={
404: {"model": error_models(NotFoundError)}, # одна модель
403: {"model": error_models(ForbiddenError, GoneError)}, # oneOf-union
},
)
def get_item(item_id: int) -> Item: ...
Union из двух ошибок на одном статусе даёт в OpenAPI oneOf с дискриминатором — Swagger UI покажет выпадающий выбор варианта по коду:
{
"oneOf": [
{ "$ref": "#/components/schemas/ErrorResponse_Literal_FORBIDDEN__" },
{ "$ref": "#/components/schemas/ErrorResponse_Literal_GONE__" }
],
"discriminator": {
"propertyName": "code",
"mapping": {
"FORBIDDEN": "#/components/schemas/ErrorResponse_Literal_FORBIDDEN__",
"GONE": "#/components/schemas/ErrorResponse_Literal_GONE__"
}
}
}
Повторяющиеся классы дедуплицируются. А вот один код у двух разных моделей внутри union невозможен — дискриминатор требует уникальных значений:
error_models(NotFoundError, AnotherWithSameCode)
# TypeError: error code 'NOT_FOUND' is shared by multiple distinct response models
Ручной способ всегда доступен
Слой core не требует декоратора. error_models() можно вписывать в responses={} руками — это и есть «core-only» режим. Декоратор (with_errors) просто избавляет от этой ручной работы.
Обработчик¶
handle_base_error — единый обработчик для всех BaseError. Регистрируется явно:
from fastapi_typed_errors import BaseError, handle_base_error
app.add_exception_handler(BaseError, handle_base_error)
Он отдаёт JSONResponse со статусом, телом {code, detail} и заголовками ошибки.
Почему сигнатура (Request, Exception)
Starlette типизирует обработчик как (Request, Exception) -> ..., а параметры функций контравариантны — узкий BaseError заставил бы вешать # type: ignore на строку регистрации у каждого потребителя. Поэтому сигнатура широкая, а неправильная регистрация (на чужой тип исключения) ловится в рантайме внятным TypeError.
Плоский HTTPException не перехватывается: Starlette идёт по __mro__, и обработчик BaseError перекрывает дефолтный только для своих подклассов.
BaseErrorMeta¶
Метакласс публичен — на случай, если вы смешиваете BaseError с ABC или другим кастомным метаклассом (иначе будет конфликт метаклассов). Он намеренно не реэкспортируется из корня пакета; импортируйте из core.base:
from abc import ABCMeta
from fastapi_typed_errors.core.base import BaseErrorMeta
class Meta(BaseErrorMeta, ABCMeta): ...
Подробности — в разделе Кастомизация.
Дальше: декоратор — как перестать писать responses={} руками.