Add friendly crash page
This commit is contained in:
4 files changed
+100
-1
No files matched your search
+18
-1
@@ -3,7 +3,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
import os
|
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.auth import current_user_id, is_logged_in, is_site_admin, normalize_email
|
||||||
from boker.config import DEFAULT_SECRET_KEY, Config, ProductionConfig
|
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):
|
def forbidden(e):
|
||||||
return render_template("403.html"), 403
|
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")
|
@app.cli.command("init-db")
|
||||||
def init_db_command() -> None:
|
def init_db_command() -> None:
|
||||||
if not database_extensions_available() or db is None:
|
if not database_extensions_available() or db is None:
|
||||||
|
|||||||
@@ -5925,6 +5925,9 @@ select.control { cursor: pointer; }
|
|||||||
.error-pg--403 .error-pg__suit {
|
.error-pg--403 .error-pg__suit {
|
||||||
color: rgba(155,140,240,.06);
|
color: rgba(155,140,240,.06);
|
||||||
}
|
}
|
||||||
|
.error-pg--500 .error-pg__suit {
|
||||||
|
color: rgba(224,177,92,.07);
|
||||||
|
}
|
||||||
|
|
||||||
.error-pg__body {
|
.error-pg__body {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -5971,6 +5974,18 @@ select.control { cursor: pointer; }
|
|||||||
justify-content: center;
|
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)
|
LANDING — hero kicker (replaces pill eyebrow in hero only)
|
||||||
================================================================ */
|
================================================================ */
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}500 · Something Went Wrong · myboker.org{% endblock %}
|
||||||
|
{% block page_class %}page--error{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="error-pg error-pg--500">
|
||||||
|
<div class="error-pg__suit" aria-hidden="true">♦</div>
|
||||||
|
<div class="error-pg__body">
|
||||||
|
<span class="error-pg__code">500</span>
|
||||||
|
<h1 class="error-pg__title">Something went wrong.</h1>
|
||||||
|
<p class="error-pg__sub">The app hit an unexpected error. Try again in a minute, or contact support if it keeps happening.</p>
|
||||||
|
<p class="error-pg__trace">Incident {{ crash_id }}</p>
|
||||||
|
<div class="error-pg__actions">
|
||||||
|
<a class="btn btn--primary btn--lg" href="{{ url_for('public.home') }}">Go home →</a>
|
||||||
|
<a class="btn btn--ghost btn--lg" href="{{ url_for('public.help') }}#contact">Contact support</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -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()
|
||||||
Reference in new issue
Block a user