Files
rose-ash/shared/browser/app/errors.py
giles f42042ccb7
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m5s
Monorepo: consolidate 7 repos into one
Combines shared, blog, market, cart, events, federation, and account
into a single repository. Eliminates submodule sync, sibling model
copying at build time, and per-app CI orchestration.

Changes:
- Remove per-app .git, .gitmodules, .gitea, submodule shared/ dirs
- Remove stale sibling model copies from each app
- Update all 6 Dockerfiles for monorepo build context (root = .)
- Add build directives to docker-compose.yml
- Add single .gitea/workflows/ci.yml with change detection
- Add .dockerignore for monorepo build context
- Create __init__.py for federation and account (cross-app imports)
2026-02-24 19:44:17 +00:00

127 lines
3.9 KiB
Python

from werkzeug.exceptions import HTTPException
from shared.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 <ul><li>...</li></ul> snippet
lis = "".join(
f"<li>{escape(m)}</li>"
for m in messages if m
)
html = (
"<ul class='list-disc pl-5 space-y-1 text-sm text-red-600'>"
f"{lis}"
"</ul>"
)
return await make_response(html, status)
# Non-HTMX: show a nicer page with error messages
html = await render_template(
"_types/root/exceptions/app_error.html",
messages=messages,
)
return await make_response(html, status)
@app.errorhandler(Exception)
async def error(e):
current_app.logger.exception("Exception %s", _info(e))
status = 500
if isinstance(e, HTTPException):
status = e.code or 500
if request.headers.get("HX-Request") == "true":
# Generic message for unexpected/untrusted errors
return await make_response(
"Something went wrong. Please try again.",
status,
)
html = await render_template("_types/root/exceptions/error.html")
return await make_response(html, status)