Files
template-fastapi/{{project_slug}}/middlewares/exception_handler.py.jinja
Aleksei Sokol 53f14a8624
All checks were successful
Run linters on applied template / Python 3.13 lint and build (push) Successful in 1m40s
Version 0.4.0
Changes:
- put ObservabilityMiddleware before ExceptionHandlerMiddleware to avoid repetative code
- add application startup and last metrics update metrics along with CPU usage metric and threads count
- move host and port to new uvicorn section at config along with new reload and forwarded_allow_ips
- add request_id and remove trace_id/span_id generation if tracing is disabled
- move logging logic from utils to observability
- pass trace_id/span_id in HEX form
2026-01-03 16:29:58 +03:00

125 lines
4.8 KiB
Django/Jinja

"""Exception handling middleware is defined here."""
import itertools
import traceback
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from opentelemetry import trace
from opentelemetry.sdk.metrics import Counter
from opentelemetry.semconv.attributes import exception_attributes, http_attributes, url_attributes
from starlette.exceptions import HTTPException
from starlette.middleware.base import BaseHTTPMiddleware
from {{project_slug}}.dependencies import logger_dep
from {{project_slug}}.exceptions.mapper import ExceptionMapper
from {{project_slug}}.observability.utils import URLsMapper
class ExceptionHandlerMiddleware(BaseHTTPMiddleware): # pylint: disable=too-few-public-methods
"""Handle exceptions, so they become http response code 500 - Internal Server Error.
If debug is activated in app configuration, then stack trace is returned, otherwise only a generic error message.
Message is sent to logger error stream anyway.
"""
def __init__(
self,
app: FastAPI,
*,
debug: bool,
exception_mapper: ExceptionMapper,
urls_mapper: URLsMapper,
errors_metric: Counter,
): # pylint: disable=too-many-arguments
"""Passing debug as a list with single element is a hack to be able to change the value on the fly."""
super().__init__(app)
self._debug = debug
self._exception_mapper = exception_mapper
self._urls_mapper = urls_mapper
self._metric = errors_metric
async def dispatch(self, request: Request, call_next):
try:
return await call_next(request)
except Exception as exc: # pylint: disable=broad-except
status_code = 500
detail = "exception occured"
if isinstance(exc, HandlerNotFoundError):
exc = exc.__cause__
cause = exc
if isinstance(exc, HTTPException):
status_code = exc.status_code # pylint: disable=no-member
detail = exc.detail # pylint: disable=no-member
if exc.__cause__ is not None:
cause = exc.__cause__
self._metric.add(
1,
{
http_attributes.HTTP_REQUEST_METHOD: request.method,
url_attributes.URL_PATH: self._urls_mapper.map(request.method, request.url.path),
"error_type": type(cause).__qualname__,
http_attributes.HTTP_RESPONSE_STATUS_CODE: status_code,
},
)
span = trace.get_current_span()
logger = logger_dep.from_request(request)
is_known = self._exception_mapper.is_known(cause)
span.record_exception(cause, {"is_known": is_known})
if is_known:
log_func = logger.aerror
else:
log_func = logger.aexception
await log_func("failed to handle request", error_type=type(cause).__name__)
span.set_status(trace.StatusCode.ERROR)
span.set_attributes(
{
exception_attributes.EXCEPTION_TYPE: type(cause).__name__,
exception_attributes.EXCEPTION_MESSAGE: repr(cause),
http_attributes.HTTP_RESPONSE_STATUS_CODE: status_code,
}
)
if self._debug:
if (res := self._exception_mapper.apply_if_known(exc)) is not None:
response = res
else:
response = JSONResponse(
{
"code": status_code,
"detail": detail,
"error": str(exc),
"error_type": type(exc).__name__,
"path": request.url.path,
"query_params": request.url.query,
"tracebacks": _get_tracebacks(exc),
},
status_code=status_code,
)
else:
response = self._exception_mapper.apply(exc)
return response
class HandlerNotFoundError(Exception):
"""Exception to raise on FastAPI 404 handler (only for situation when no handler was found for request).
Guranteed to have `.__cause__` as its parent exception.
"""
def _get_tracebacks(exc: Exception) -> list[list[str]]:
tracebacks: list[list[str]] = []
while exc is not None:
tracebacks.append(
list(itertools.chain.from_iterable(map(lambda x: x.split("\n"), traceback.format_tb(exc.__traceback__))))
)
tracebacks[-1].append(f"{exc.__class__.__module__}.{exc.__class__.__qualname__}: {exc}")
exc = exc.__cause__
return tracebacks