From adbc60bfc3b6f7a10b9352597de872f47f0d241c Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Sun, 28 Jun 2026 17:47:58 -0700 Subject: [PATCH] Add friendly crash page --- boker/app.py | 19 +++++++++++++++++- static/css/style.css | 15 ++++++++++++++ templates/500.html | 20 +++++++++++++++++++ tests/test_errors.py | 47 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 templates/500.html create mode 100644 tests/test_errors.py diff --git a/boker/app.py b/boker/app.py index 3247e99..3465e12 100644 --- a/boker/app.py +++ b/boker/app.py @@ -3,7 +3,10 @@ from __future__ import annotations import click import os -from flask import Flask, render_template +from uuid import uuid4 + +from flask import Flask, render_template, request +from werkzeug.exceptions import HTTPException from boker.auth import current_user_id, is_logged_in, is_site_admin, normalize_email from boker.config import DEFAULT_SECRET_KEY, Config, ProductionConfig @@ -64,6 +67,20 @@ def create_app(config_overrides: dict | None = None) -> Flask: def forbidden(e): return render_template("403.html"), 403 + @app.errorhandler(Exception) + def unexpected_error(error): + if isinstance(error, HTTPException): + return error + + crash_id = uuid4().hex[:12] + app.logger.exception( + "Unhandled exception [%s] during %s %s", + crash_id, + request.method, + request.path, + ) + return render_template("500.html", crash_id=crash_id), 500 + @app.cli.command("init-db") def init_db_command() -> None: if not database_extensions_available() or db is None: diff --git a/static/css/style.css b/static/css/style.css index 2aa6683..09da5f2 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -5925,6 +5925,9 @@ select.control { cursor: pointer; } .error-pg--403 .error-pg__suit { color: rgba(155,140,240,.06); } +.error-pg--500 .error-pg__suit { + color: rgba(224,177,92,.07); +} .error-pg__body { position: relative; @@ -5971,6 +5974,18 @@ select.control { cursor: pointer; } justify-content: center; } +.error-pg__trace { + margin: -12px 0 28px; + padding: 6px 10px; + border: 1px solid var(--line); + border-radius: 6px; + color: var(--faint); + background: var(--panel); + font-family: var(--font-mono); + font-size: 12px; + line-height: 1.4; +} + /* ================================================================ LANDING — hero kicker (replaces pill eyebrow in hero only) ================================================================ */ diff --git a/templates/500.html b/templates/500.html new file mode 100644 index 0000000..24ece0f --- /dev/null +++ b/templates/500.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% block title %}500 · Something Went Wrong · myboker.org{% endblock %} +{% block page_class %}page--error{% endblock %} +{% block content %} + +
+ +
+ 500 +

Something went wrong.

+

The app hit an unexpected error. Try again in a minute, or contact support if it keeps happening.

+

Incident {{ crash_id }}

+ +
+
+ +{% endblock %} diff --git a/tests/test_errors.py b/tests/test_errors.py new file mode 100644 index 0000000..ea0d844 --- /dev/null +++ b/tests/test_errors.py @@ -0,0 +1,47 @@ +import re +import tempfile +import unittest +from pathlib import Path + +from app import create_app +from boker.db import db + + +class ErrorPageTests(unittest.TestCase): + def setUp(self): + self.tmpdir = tempfile.TemporaryDirectory() + db_path = Path(self.tmpdir.name) / "test.sqlite3" + self.app = create_app( + { + "TESTING": True, + "PROPAGATE_EXCEPTIONS": False, + "SQLALCHEMY_DATABASE_URI": f"sqlite:///{db_path}", + "WTF_CSRF_ENABLED": False, + } + ) + + @self.app.get("/explode") + def explode(): + raise RuntimeError("boom") + + self.client = self.app.test_client() + + def tearDown(self): + with self.app.app_context(): + db.session.remove() + db.drop_all() + db.engine.dispose() + self.tmpdir.cleanup() + + def test_unexpected_errors_render_crash_page_with_incident_id(self): + response = self.client.get("/explode") + + self.assertEqual(response.status_code, 500) + self.assertIn(b"Something went wrong.", response.data) + self.assertIn(b"Incident", response.data) + self.assertNotIn(b"RuntimeError", response.data) + self.assertRegex(response.get_data(as_text=True), re.compile(r"Incident [a-f0-9]{12}")) + + +if __name__ == "__main__": + unittest.main()