Coverage for nexios\exception_handler.py: 95%
55 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-21 20:31 +0100
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-21 20:31 +0100
1from __future__ import annotations
2from shutil import ReadError
3import typing
4from nexios.exceptions import HTTPException
5from nexios.http import Request, Response
6from nexios.types import ExceptionHandlerType
7from nexios.config import get_config
8import traceback
9from nexios.auth.exceptions import AuthenticationFailed, AuthErrorHandler
10from nexios.exceptions import NotFoundException
11from nexios.handlers.not_found import handle_404_error
12from nexios import logging
14logger = logging.getLogger("nexios")
17def _lookup_exception_handler(
18 exc_handlers: typing.Dict[typing.Any[int, Exception], ExceptionHandlerType],
19 exc: Exception,
20):
22 for cls in type(exc).__mro__:
23 if cls in exc_handlers: # type: ignore
24 return exc_handlers[cls]
25 return None
28async def wrap_http_exceptions(
29 request: Request,
30 response: Response,
31 call_next: typing.Callable[..., typing.Awaitable[Response]],
32 exception_handlers: typing.Dict[type[Exception], ExceptionHandlerType],
33 status_handlers: typing.Dict[int, ExceptionHandlerType],
34) -> typing.Any:
36 try:
37 exception_handlers, status_handlers = exception_handlers, status_handlers
38 except KeyError:
39 exception_handlers, status_handlers = {}, {}
41 try:
42 return await call_next()
43 except Exception as exc:
44 handler: typing.Any[ExceptionHandlerType, None] = None # type: ignore
46 if isinstance(exc, HTTPException):
47 handler: typing.Optional[ExceptionHandlerType] = status_handlers.get(exc.status_code) # type: ignore
49 if handler:
50 return await handler(request, response, exc) # type: ignore
52 if handler is None: # type: ignore
53 handler = _lookup_exception_handler(exception_handlers, exc)
54 if not handler:
55 error = traceback.format_exc()
56 logger.error(error)
57 raise exc
58 return await handler(request, response, exc)
61class ExceptionMiddleware:
62 def __init__(self) -> None:
63 self.debug = (
64 get_config().debug or False
65 ) # TODO: We ought to handle 404 cases if debug is set.
66 self._status_handlers: typing.Dict[int, ExceptionHandlerType] = {}
67 self._exception_handlers: dict[
68 typing.Type[Exception], typing.Callable[..., typing.Awaitable[None]]
69 ] = {
70 HTTPException: self.http_exception,
71 AuthenticationFailed: AuthErrorHandler,
72 NotFoundException: handle_404_error,
73 }
75 def add_exception_handler(
76 self,
77 exc_class_or_status_code: typing.Union[int, type[Exception]],
78 handler: ExceptionHandlerType,
79 ) -> None:
80 if isinstance(exc_class_or_status_code, int):
81 self._status_handlers[exc_class_or_status_code] = handler
82 else:
83 assert issubclass(exc_class_or_status_code, Exception)
84 self._exception_handlers[exc_class_or_status_code] = handler # type:ignore
86 async def __call__(
87 self,
88 request: Request,
89 response: Response,
90 call_next: typing.Callable[[], typing.Awaitable[Response]],
91 ):
93 return await wrap_http_exceptions(
94 request=request,
95 response=response,
96 call_next=call_next,
97 exception_handlers=self._exception_handlers,
98 status_handlers=self._status_handlers,
99 )
101 async def http_exception(
102 self, request: Request, response: Response, exc: HTTPException
103 ) -> typing.Any:
104 assert isinstance(exc, HTTPException)
105 if exc.status_code in {204, 304}: # type:ignore
106 return response.empty(status_code=exc.status_code, headers=exc.headers)
107 return response.json(
108 exc.detail, status_code=exc.status_code, headers=exc.headers
109 )