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
|
||||
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
|
||||
async def _sync_ghost_membership():
|
||||
from services.ghost_membership import sync_all_membership_from_ghost
|
||||
from shared.db.session import get_session
|
||||
try:
|
||||
async with get_session() as s:
|
||||
await sync_all_membership_from_ghost(s)
|
||||
await s.commit()
|
||||
print("[account] Ghost membership sync complete")
|
||||
except Exception as e:
|
||||
print(f"[account] Ghost membership sync failed (non-fatal): {e}")
|
||||
async def _schedule_ghost_membership_sync():
|
||||
import asyncio
|
||||
async def _sync():
|
||||
from services.ghost_membership import sync_all_membership_from_ghost
|
||||
from shared.db.session import get_session
|
||||
try:
|
||||
async with get_session() as s:
|
||||
await sync_all_membership_from_ghost(s)
|
||||
await s.commit()
|
||||
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
|
||||
|
||||
|
||||
@@ -33,6 +33,30 @@ class AppError(ValueError):
|
||||
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 _info(e):
|
||||
return {
|
||||
@@ -119,11 +143,11 @@ def errors(app):
|
||||
f"<p class='text-sm text-red-600'>Service <b>{escape(service)}</b> is unavailable.</p>",
|
||||
503,
|
||||
)
|
||||
html = await render_template(
|
||||
"_types/root/exceptions/error.html",
|
||||
service_name=service,
|
||||
)
|
||||
return await make_response(html, 503)
|
||||
# Raw HTML — cannot use render_template here because the context
|
||||
# processor would try to fetch fragments again → infinite loop.
|
||||
return await make_response(_error_page(
|
||||
f"The <b>{escape(service)}</b> service is currently unavailable. It may be restarting."
|
||||
), 503)
|
||||
|
||||
@app.errorhandler(Exception)
|
||||
async def error(e):
|
||||
@@ -134,11 +158,12 @@ def errors(app):
|
||||
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)
|
||||
# Raw HTML — avoids context processor / fragment loop on errors.
|
||||
return await make_response(_error_page(
|
||||
"WELL THIS IS EMBARRASSING…"
|
||||
), status)
|
||||
|
||||
Reference in New Issue
Block a user