Почему не работает глобальный обработчик Exception в FastAPI
При разработке на FastAPI часто возникает ситуация, когда нужно перехватывать все необработанные исключения. Однако стандартный обработчик для базового класса Exception может не срабатывать, хотя кастомные обработчики для HTTPException или своих классов работают отлично. В этой статье мы разберём причины такого поведения и покажем правильное решение.
Проблема: обработчик Exception не вызывается
Разработчики часто регистрируют глобальный обработчик так:
app.add_exception_handler(Exception, global_exception_handler)И ожидают, что он перехватит любое исключение, например raise Exception или деление на ноль. Но на практике FastAPI просто возвращает стандартную ошибку 500 Internal Server Error, а пользовательский обработчик игнорируется.
Почему это происходит
FastAPI построен на Starlette. В Starlette порядок регистрации обработчиков имеет значение. Когда вы регистрируете обработчик для Exception, он должен быть добавлен после всех других обработчиков и до запуска приложения. Однако основная причина - ошибки времени выполнения (runtime errors), такие как деление на ноль, не являются экземплярами Exception в том смысле, который ожидает обработчик. На самом деле, FastAPI внутренне использует ServerErrorMiddleware, который перехватывает все исключения, не обработанные другими middleware. Если вы не заменили или не настроили этот middleware, ваш обработчик может быть проигнорирован.
Правильное решение: используйте middleware
Чтобы гарантированно перехватывать любые необработанные исключения, лучше использовать middleware. Вот пример рабочего решения:
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
class ExceptionHandlerMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
try:
response = await call_next(request)
return response
except Exception as exc:
return JSONResponse(
status_code=500,
content={"error": "Internal server error"}
)
app.add_middleware(ExceptionHandlerMiddleware)Этот middleware перехватит любое исключение, возникшее в процессе обработки запроса, включая деление на ноль, raise Exception и другие ошибки времени выполнения.
Альтернатива: замена ServerErrorMiddleware
Если вы хотите оставить обработчик через add_exception_handler, вам нужно заменить стандартный ServerErrorMiddleware на свою версию. Это делается так:
from starlette.middleware.errors import ServerErrorMiddleware
class CustomServerErrorMiddleware(ServerErrorMiddleware):
async def __call__(self, scope, receive, send):
try:
await self.app(scope, receive, send)
except Exception as exc:
response = JSONResponse(
status_code=500,
content={"error": "Internal server error"}
)
await response(scope, receive, send)
app.add_middleware(CustomServerErrorMiddleware)Однако этот способ сложнее и менее гибок, поэтому рекомендуется использовать первый вариант с middleware.
Почему кастомный обработчик работает
Кастомные обработчики, такие как custom_exception_handler для CustomException, работают потому, что они зарегистрированы для конкретного типа исключения, который не перехватывается стандартным middleware. FastAPI вызывает их напрямую, когда встречает raise CustomException. Это не конфликтует с ServerErrorMiddleware, так как последний обрабатывает только неперехваченные исключения.
Проверка порядка регистрации
Если вы всё же решили использовать add_exception_handler, убедитесь, что обработчик Exception зарегистрирован последним:
app.add_exception_handler(CustomException, custom_exception_handler)
app.add_exception_handler(Exception, global_exception_handler) # последнимНо даже это не гарантирует работу для всех типов ошибок, как описано выше.
Выводы
Глобальный обработчик Exception в FastAPI может не работать из-за особенностей внутреннего middleware. Надёжное решение - использовать собственный middleware, который перехватывает все исключения. Это обеспечит единую точку обработки ошибок и позволит возвращать клиенту структурированный JSON-ответ.