/
liquid-g
/
liquid-code
Обзор
Документация
Войти
/
liquid-g
/
liquid-code
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop-0.4
src/liquidcode/middleware/error_handling.py
231 строка
9 KB
User
0.4.20 - refactor: централизация обработки исключений в ErrorHandlingMiddleware (T-003)
07 июл 2026, 22:54
07 июл 2026, 22:54
26d8fe2
Код
Авторство
О чём код?
""" Middleware для обработки ошибок с поддержкой HTML и JSON. Перехватывает исключения, возникшие в цепочке обработки запроса, диспатчит событие ExceptionEvent, вызывает хуки _exception_hooks, логирует ошибку и возвращает клиенту структурированный ответ в формате JSON или HTML в зависимости от заголовка Accept. """ import logging import traceback from typing import Callable, Dict from .base import Middleware from ..contracts import Request, Response from ..events import ExceptionEvent from ..http import HttpResponse from ..template import TemplateManager logger = logging.getLogger(__name__) class ErrorHandlingMiddleware(Middleware): """ Централизованный обработчик исключений. Должен быть первым в цепочке middleware, чтобы перехватывать все исключения, включая те, что возникли в других middleware. При перехвате исключения: 1. Диспатчит событие ExceptionEvent (если доступен event_dispatcher). 2. Вызывает все зарегистрированные хуки _exception_hooks. 3. Логирует ошибку (с traceback в режиме debug). 4. Формирует ответ в JSON или HTML. """ def __init__( self, container=None, prefer_json: bool = False, debug: bool = False, kernel=None, ): """ Инициализирует middleware. Args: container: DI-контейнер (для получения TemplateManager). prefer_json: Предпочитать JSON-ответ HTML даже если Accept заголовок не указывает явное предпочтение. debug: Режим отладки (включает traceback в ответ). kernel: Экземпляр ядра (для доступа к event_dispatcher и хукам). """ self.container = container self.prefer_json = prefer_json self.debug = debug self.kernel = kernel self._template_manager = None # Пытаемся получить TemplateManager из контейнера if container: try: self._template_manager = container.get(TemplateManager) logger.info( "ErrorHandlingMiddleware: TemplateManager получен из контейнера" ) except Exception as e: logger.warning( f"ErrorHandlingMiddleware: не удалось получить TemplateManager: {e}" ) def __call__( self, request: Request, call_next: Callable[[Request], Response] ) -> Response: """ Обрабатывает запрос, перехватывая все исключения. Args: request: Объект запроса. call_next: Следующий middleware в цепочке. Returns: Response: Ответ (успешный или с ошибкой). Raises: Исключения не пробрасываются — все перехватываются и преобразуются в ответ об ошибке. """ try: return call_next(request) except Exception as e: # Определяем HTTP-статус из исключения (если есть атрибут status_code) status = getattr(e, "status_code", 500) # 1. Диспатчим событие ExceptionEvent (если ядро и диспетчер доступны) if self.kernel and self.kernel.event_dispatcher: exc_event = ExceptionEvent(request, e) self.kernel.event_dispatcher.dispatch(exc_event) # Если событие вернуло готовый ответ — используем его if exc_event.response is not None: return exc_event.response # 2. Вызываем хуки _exception_hooks из ядра if self.kernel: for hook in self.kernel._exception_hooks: hook_response = hook(request, e) if hook_response is not None: return hook_response # 3. Логирование ошибки if self.debug: logger.exception(f"Ошибка: {request.get_method()} {request.get_path()}") else: logger.error( f"Ошибка: {request.get_method()} {request.get_path()} — {e}" ) # 4. Формируем ответ об ошибке return self._error_response(request, status, e) def _parse_accept(self, accept_header: str) -> Dict[str, float]: """ Парсит заголовок Accept и возвращает словарь {mime_type: weight}. Формат: "application/json, text/html;q=0.9, */*;q=0.8" q-значение по умолчанию — 1.0. Args: accept_header: Строка заголовка Accept. Returns: Словарь с mime-типами и их весами. """ result = {} if not accept_header: return result for part in accept_header.split(","): part = part.strip() if ";q=" in part: mime_type, q_part = part.split(";q=") try: q_value = float(q_part.strip()) except ValueError: q_value = 1.0 result[mime_type.strip()] = q_value else: result[part] = 1.0 return result def _error_response( self, request: Request, status: int, exception: Exception ) -> Response: """ Формирует ответ об ошибке в JSON или HTML. Выбор формата: - Если prefer_json=True — JSON. - Иначе анализируется заголовок Accept: JSON предпочтительнее, если его вес выше, чем у HTML. - По умолчанию — HTML. Args: request: Объект запроса. status: HTTP-статус ошибки. exception: Исключение-причина. Returns: HttpResponse: Ответ с ошибкой. """ accept = request.get_headers().get("Accept", "text/html") q_values = self._parse_accept(accept) # Определяем предпочтительный формат is_json = self.prefer_json or q_values.get( "application/json", 0 ) > q_values.get("text/html", 0) if is_json: # JSON-ответ content = { "error": str(exception), "status": status, "path": request.get_path(), } if self.debug and status >= 500: # В режиме отладки добавляем traceback (для внутренних ошибок) content["traceback"] = traceback.format_exc().split("\n") return HttpResponse( content=content, status=status, headers={"Content-Type": "application/json"}, ) # HTML-ответ # Пытаемся использовать шаблон ошибки, если доступен TemplateManager if self._template_manager: try: html = self._template_manager.render( f"errors/{status}.html.j2", status=status, error=str(exception), path=request.get_path(), method=request.get_method(), ) return HttpResponse( html.encode("utf-8"), status=status, headers={"Content-Type": "text/html"}, ) except Exception as e: logger.warning(f"Не удалось отрендерить шаблон ошибки: {e}") # Встроенный fallback HTML html = f""" <!DOCTYPE html> <html> <head><title>Ошибка {status}</title></head> <body> <h1>Ошибка {status}</h1> <p>{exception}</p> <p>Метод: {request.get_method()}, Путь: {request.get_path()}</p> </body> </html> """ return HttpResponse( html.encode("utf-8"), status=status, headers={"Content-Type": "text/html"}, )