from werkzeug.exceptions import HTTPException from utils import hx_fragment_request from quart import ( request, render_template, make_response, current_app ) from markupsafe import escape class AppError(ValueError): """ Base class for app-level, client-safe errors. Behaves like ValueError so existing except ValueError: still works. """ status_code: int = 400 def __init__(self, message, *, status_code: int | None = None): # Support a single message or a list/tuple of messages if isinstance(message, (list, tuple, set)): self.messages = [str(m) for m in message] msg = self.messages[0] if self.messages else "" else: self.messages = [str(message)] msg = str(message) super().__init__(msg) if status_code is not None: self.status_code = status_code def errors(app): def _info(e): return { "exception": e, "method": request.method, "url": str(request.url), "base_url": str(request.base_url), "root_path": request.root_path, "path": request.path, "full_path": request.full_path, "endpoint": request.endpoint, "url_rule": str(request.url_rule) if request.url_rule else None, "headers": {k: v for k, v in request.headers.items() if k.lower().startswith("x-forwarded") or k in ("Host",)}, } @app.errorhandler(404) async def not_found(e): current_app.logger.warning("404 %s", _info(e)) if hx_fragment_request(): html = await render_template( "_types/root/exceptions/hx/_.html", errnum='404' ) else: html = await render_template( "_types/root/exceptions/_.html", errnum='404', ) return await make_response(html, 404) @app.errorhandler(403) async def not_allowed(e): current_app.logger.warning("403 %s", _info(e)) if hx_fragment_request(): html = await render_template( "_types/root/exceptions/hx/_.html", errnum='403' ) else: html = await render_template( "_types/root/exceptions/_.html", errnum='403', ) return await make_response(html, 403) @app.errorhandler(AppError) async def app_error(e: AppError): # App-level, client-safe errors current_app.logger.info("AppError %s", _info(e)) status = getattr(e, "status_code", 400) messages = getattr(e, "messages", [str(e)]) if request.headers.get("HX-Request") == "true": # Build a little styled