feat: extract shared infrastructure from shared_lib
Phase 1-3 of decoupling plan: - Shared DB, models, infrastructure, browser, config, utils - Event infrastructure (domain_events outbox, bus, processor) - Structured logging - Generic container concept (container_type/container_id) - Alembic migrations for all schema changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
126
browser/app/errors.py
Normal file
126
browser/app/errors.py
Normal file
@@ -0,0 +1,126 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user