Fix error page loop + account startup timeout
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m11s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m11s
- Error handlers for FragmentError and generic Exception now return self-contained HTML (no render_template) to avoid the infinite loop where context processor → fetch_fragments → error → render_template → context processor → fetch_fragments → error ... - Account Ghost membership sync moved to background task so it doesn't block Hypercorn's startup timeout (was causing crash-loop). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -82,18 +82,22 @@ def create_app() -> "Quart":
|
|||||||
from bp.data.routes import register as register_data
|
from bp.data.routes import register as register_data
|
||||||
app.register_blueprint(register_data())
|
app.register_blueprint(register_data())
|
||||||
|
|
||||||
# --- Ghost membership sync at startup ---
|
# --- Ghost membership sync at startup (background) ---
|
||||||
|
# Runs as a background task to avoid blocking Hypercorn's startup timeout.
|
||||||
@app.before_serving
|
@app.before_serving
|
||||||
async def _sync_ghost_membership():
|
async def _schedule_ghost_membership_sync():
|
||||||
from services.ghost_membership import sync_all_membership_from_ghost
|
import asyncio
|
||||||
from shared.db.session import get_session
|
async def _sync():
|
||||||
try:
|
from services.ghost_membership import sync_all_membership_from_ghost
|
||||||
async with get_session() as s:
|
from shared.db.session import get_session
|
||||||
await sync_all_membership_from_ghost(s)
|
try:
|
||||||
await s.commit()
|
async with get_session() as s:
|
||||||
print("[account] Ghost membership sync complete")
|
await sync_all_membership_from_ghost(s)
|
||||||
except Exception as e:
|
await s.commit()
|
||||||
print(f"[account] Ghost membership sync failed (non-fatal): {e}")
|
print("[account] Ghost membership sync complete")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[account] Ghost membership sync failed (non-fatal): {e}")
|
||||||
|
asyncio.get_event_loop().create_task(_sync())
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,30 @@ class AppError(ValueError):
|
|||||||
self.status_code = status_code
|
self.status_code = status_code
|
||||||
|
|
||||||
|
|
||||||
|
def _error_page(message: str) -> str:
|
||||||
|
"""Self-contained error page HTML. Bypasses Jinja/context processors."""
|
||||||
|
return (
|
||||||
|
"<!DOCTYPE html><html><head><meta charset='utf-8'>"
|
||||||
|
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
|
||||||
|
"<title>Error</title>"
|
||||||
|
"<style>"
|
||||||
|
"body{margin:0;min-height:100vh;display:flex;align-items:center;"
|
||||||
|
"justify-content:center;font-family:system-ui,sans-serif;"
|
||||||
|
"background:#fafafa;color:#333}"
|
||||||
|
".box{text-align:center;padding:2rem;max-width:480px}"
|
||||||
|
".box h1{font-size:1.5rem;color:#ef4444;margin:0 0 1rem}"
|
||||||
|
".box p{margin:0 0 1.5rem;line-height:1.6}"
|
||||||
|
".box a{color:#3b82f6;text-decoration:none}"
|
||||||
|
".box img{max-width:300px;margin:1rem auto}"
|
||||||
|
"</style></head><body>"
|
||||||
|
"<div class='box'>"
|
||||||
|
f"<h1>{message}</h1>"
|
||||||
|
"<img src='/static/errors/error.gif' width='300' height='300'>"
|
||||||
|
"<p><a href='javascript:location.reload()'>Reload</a></p>"
|
||||||
|
"</div></body></html>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def errors(app):
|
def errors(app):
|
||||||
def _info(e):
|
def _info(e):
|
||||||
return {
|
return {
|
||||||
@@ -119,11 +143,11 @@ def errors(app):
|
|||||||
f"<p class='text-sm text-red-600'>Service <b>{escape(service)}</b> is unavailable.</p>",
|
f"<p class='text-sm text-red-600'>Service <b>{escape(service)}</b> is unavailable.</p>",
|
||||||
503,
|
503,
|
||||||
)
|
)
|
||||||
html = await render_template(
|
# Raw HTML — cannot use render_template here because the context
|
||||||
"_types/root/exceptions/error.html",
|
# processor would try to fetch fragments again → infinite loop.
|
||||||
service_name=service,
|
return await make_response(_error_page(
|
||||||
)
|
f"The <b>{escape(service)}</b> service is currently unavailable. It may be restarting."
|
||||||
return await make_response(html, 503)
|
), 503)
|
||||||
|
|
||||||
@app.errorhandler(Exception)
|
@app.errorhandler(Exception)
|
||||||
async def error(e):
|
async def error(e):
|
||||||
@@ -134,11 +158,12 @@ def errors(app):
|
|||||||
status = e.code or 500
|
status = e.code or 500
|
||||||
|
|
||||||
if request.headers.get("HX-Request") == "true":
|
if request.headers.get("HX-Request") == "true":
|
||||||
# Generic message for unexpected/untrusted errors
|
|
||||||
return await make_response(
|
return await make_response(
|
||||||
"Something went wrong. Please try again.",
|
"Something went wrong. Please try again.",
|
||||||
status,
|
status,
|
||||||
)
|
)
|
||||||
|
|
||||||
html = await render_template("_types/root/exceptions/error.html")
|
# Raw HTML — avoids context processor / fragment loop on errors.
|
||||||
return await make_response(html, status)
|
return await make_response(_error_page(
|
||||||
|
"WELL THIS IS EMBARRASSING…"
|
||||||
|
), status)
|
||||||
|
|||||||
Reference in New Issue
Block a user