From f1b7fdd37d3af7ebbd65802e6efe7986a1fe2b90 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 28 Feb 2026 18:36:11 +0000 Subject: [PATCH] Make rich 404 resilient to cross-service failures Build a minimal context directly instead of relying on get_template_context() which runs the full context processor chain including cross-service fragment fetches. Each step (base_context, fragments, post hydration) is independently try/excepted so the page renders with whatever is available. Co-Authored-By: Claude Opus 4.6 --- shared/browser/app/errors.py | 101 +++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 41 deletions(-) diff --git a/shared/browser/app/errors.py b/shared/browser/app/errors.py index 4b8e0e4..379381a 100644 --- a/shared/browser/app/errors.py +++ b/shared/browser/app/errors.py @@ -83,53 +83,72 @@ async def _rich_error_page(errnum: str, message: str, image: str | None = None) return None try: - # If the app's url_value_preprocessor didn't run (no route match), - # manually extract the first path segment as a candidate slug and - # try to hydrate post data so context processors can use it. + from shared.sexp.jinja_bridge import render + from shared.sexp.helpers import full_page, call_url + + # Build a minimal context — avoid get_template_context() which + # calls cross-service fragment fetches that may fail. + from shared.infrastructure.context import base_context + try: + ctx = await base_context() + except Exception: + ctx = {"base_title": "Rose Ash", "asset_url": "/static"} + + # Try to fetch fragments, but don't fail if they're unreachable + try: + from shared.infrastructure.fragments import fetch_fragments + from shared.infrastructure.cart_identity import current_cart_identity + user = getattr(g, "user", None) + ident = current_cart_identity() + cart_params = {} + if ident["user_id"] is not None: + cart_params["user_id"] = ident["user_id"] + if ident["session_id"] is not None: + cart_params["session_id"] = ident["session_id"] + cart_mini_html, auth_menu_html, nav_tree_html = await fetch_fragments([ + ("cart", "cart-mini", cart_params or None), + ("account", "auth-menu", {"email": user.email} if user else None), + ("blog", "nav-tree", {"app_name": current_app.name, "path": request.path}), + ]) + ctx["cart_mini_html"] = cart_mini_html + ctx["auth_menu_html"] = auth_menu_html + ctx["nav_tree_html"] = nav_tree_html + except Exception: + ctx.setdefault("cart_mini_html", "") + ctx.setdefault("auth_menu_html", "") + ctx.setdefault("nav_tree_html", "") + + # Try to hydrate post data from slug if not already available segments = [s for s in request.path.strip("/").split("/") if s] slug = segments[0] if segments else None + post_data = getattr(g, "post_data", None) - if slug and not getattr(g, "post_data", None): - # Set g.post_slug / g.page_slug so app context processors - # (inject_post, etc.) can pick it up. - from shared.infrastructure.data_client import fetch_data - raw = await fetch_data( - "blog", "post-by-slug", - params={"slug": slug}, - required=False, - ) - if raw: - g.post_slug = slug - g.page_slug = slug # cart uses page_slug - g.post_data = { - "post": { - "id": raw["id"], - "title": raw["title"], - "slug": raw["slug"], - "feature_image": raw.get("feature_image"), - "status": raw["status"], - "visibility": raw["visibility"], - }, - } - # Also set page_post for cart app - from shared.contracts.dtos import PostDTO, dto_from_dict - post_dto = dto_from_dict(PostDTO, raw) - if post_dto and getattr(post_dto, "is_page", False): - g.page_post = post_dto - - # Build template context (runs app context processors → fragments) - from shared.sexp.page import get_template_context - ctx = await get_template_context() - - # Build headers: root header + post header if available - from shared.sexp.jinja_bridge import render - from shared.sexp.helpers import ( - root_header_html, call_url, full_page, - ) + if slug and not post_data: + try: + from shared.infrastructure.data_client import fetch_data + raw = await fetch_data( + "blog", "post-by-slug", + params={"slug": slug}, + required=False, + ) + if raw: + post_data = { + "post": { + "id": raw["id"], + "title": raw["title"], + "slug": raw["slug"], + "feature_image": raw.get("feature_image"), + }, + } + except Exception: + pass + # Root header (site nav bar) + from shared.sexp.helpers import root_header_html hdr = root_header_html(ctx) - post = ctx.get("post") or {} + # Post breadcrumb if we resolved a post + post = (post_data or {}).get("post") or ctx.get("post") or {} if post.get("slug"): title = (post.get("title") or "")[:160] feature_image = post.get("feature_image")