# app/exceptions/error_renderer.py """ Error Page Renderer Renders context-aware error pages using Jinja2 templates. Handles fallback logic and context-specific customization. """ import logging from pathlib import Path from typing import Any from fastapi import Request from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates from app.modules.enums import FrontendType from middleware.frontend_type import get_frontend_type logger = logging.getLogger(__name__) class ErrorPageRenderer: """Renders context-aware error pages using Jinja2 templates.""" # Map status codes to friendly names STATUS_CODE_NAMES = { 400: "Bad Request", 401: "Unauthorized", 403: "Forbidden", 404: "Not Found", 422: "Validation Error", 429: "Too Many Requests", 500: "Internal Server Error", 502: "Bad Gateway", 503: "Service Unavailable", } # Map status codes to user-friendly messages STATUS_CODE_MESSAGES = { 400: "The request could not be processed due to invalid data.", 401: "You need to be authenticated to access this resource.", 403: "You don't have permission to access this resource.", 404: "The page you're looking for doesn't exist or has been moved.", 422: "The submitted data contains validation errors.", 429: "Too many requests. Please slow down and try again later.", 500: "Something went wrong on our end. We're working to fix it.", 502: "Unable to reach the service. Please try again later.", 503: "The service is temporarily unavailable. Please try again later.", } @staticmethod def get_templates_dir() -> Path: """Get the templates directory path.""" # Assuming templates are in app/templates/ base_dir = Path(__file__).resolve().parent.parent return base_dir / "templates" @staticmethod def render_error_page( request: Request, status_code: int, error_code: str, message: str, details: dict[str, Any] | None = None, show_debug: bool = False, ) -> HTMLResponse: """ Render appropriate error page based on request frontend type. Template Selection Priority: 1. Frontend-specific error page: {frontend}/errors/{status_code}.html 2. Frontend-specific generic: {frontend}/errors/generic.html 3. Shared fallback error page: shared/{status_code}-fallback.html 4. Shared fallback generic: shared/generic-fallback.html Args: request: FastAPI request object status_code: HTTP status code error_code: Application error code message: Error message details: Additional error details show_debug: Whether to show debug information Returns: HTMLResponse with rendered error page """ # Get frontend type frontend_type = get_frontend_type(request) # Prepare template data template_data = ErrorPageRenderer._prepare_template_data( request=request, frontend_type=frontend_type, status_code=status_code, error_code=error_code, message=message, details=details, show_debug=show_debug, ) # Find and render template templates_dir = ErrorPageRenderer.get_templates_dir() templates = Jinja2Templates(directory=str(templates_dir)) # Try to find appropriate template template_path = ErrorPageRenderer._find_template( frontend_type=frontend_type, status_code=status_code, ) logger.info( f"Rendering error page: {template_path} for {status_code} in {frontend_type.value} frontend", extra={ "status_code": status_code, "error_code": error_code, "frontend": frontend_type.value, "template": template_path, }, ) try: # Render template return templates.TemplateResponse( template_path, { "request": request, **template_data, }, status_code=status_code, ) except Exception as e: logger.error( f"Failed to render error template {template_path}: {e}", exc_info=True ) # Return basic HTML as absolute fallback return ErrorPageRenderer._render_basic_html_fallback( status_code=status_code, error_code=error_code, message=message, ) @staticmethod def _find_template( frontend_type: FrontendType, status_code: int, ) -> str: """ Find appropriate error template based on frontend type and status code. Priority: 1. {frontend}/errors/{status_code}.html 2. {frontend}/errors/generic.html 3. shared/{status_code}-fallback.html 4. shared/generic-fallback.html """ templates_dir = ErrorPageRenderer.get_templates_dir() # Map frontend type to folder name frontend_folders = { FrontendType.ADMIN: "admin", FrontendType.STORE: "store", FrontendType.STOREFRONT: "storefront", FrontendType.PLATFORM: "fallback", # Platform uses fallback templates } frontend_folder = frontend_folders.get(frontend_type, "fallback") # Try frontend-specific status code template specific_template = f"{frontend_folder}/errors/{status_code}.html" if (templates_dir / specific_template).exists(): return specific_template # Try frontend-specific generic template generic_template = f"{frontend_folder}/errors/generic.html" if (templates_dir / generic_template).exists(): return generic_template # Try shared fallback status code template fallback_specific = f"shared/{status_code}-fallback.html" if (templates_dir / fallback_specific).exists(): return fallback_specific # Use shared fallback generic template (must exist) return "shared/generic-fallback.html" @staticmethod def _prepare_template_data( request: Request, frontend_type: FrontendType, status_code: int, error_code: str, message: str, details: dict[str, Any] | None, show_debug: bool, ) -> dict[str, Any]: """Prepare data dictionary for error template.""" # Get friendly status name status_name = ErrorPageRenderer.STATUS_CODE_NAMES.get( status_code, f"Error {status_code}" ) # Get default user-friendly message default_message = ErrorPageRenderer.STATUS_CODE_MESSAGES.get( status_code, "An error occurred while processing your request." ) # Use provided message or default user_message = message if message else default_message # Determine if we should show debug info # Only show to admins (we can check user role if available) display_debug = show_debug and ErrorPageRenderer._is_admin_user(request) # Get frontend-specific data frontend_data = ErrorPageRenderer._get_frontend_data(request, frontend_type) return { "status_code": status_code, "status_name": status_name, "error_code": error_code, "message": user_message, "details": details or {}, "show_debug": display_debug, "frontend_type": frontend_type.value, "path": request.url.path, **frontend_data, } @staticmethod def _get_frontend_data( request: Request, frontend_type: FrontendType ) -> dict[str, Any]: """Get frontend-specific data for error templates.""" data = {} # Add store information if available (for storefront frontend) if frontend_type == FrontendType.STOREFRONT: store = getattr(request.state, "store", None) if store: # Pass minimal store info for templates data["store"] = { "id": store.id, "name": store.name, "subdomain": store.subdomain, "logo": getattr(store, "logo", None), } # Add theme information if available theme = getattr(request.state, "theme", None) if theme: # Theme can be a dict or object, handle both if isinstance(theme, dict): data["theme"] = theme else: # If it's an object, convert to dict data["theme"] = { "colors": getattr(theme, "colors", {}), "fonts": getattr(theme, "fonts", {}), "branding": getattr(theme, "branding", {}), "custom_css": getattr(theme, "custom_css", None), } # Calculate base_url for storefront links store_context = getattr(request.state, "store_context", None) access_method = ( store_context.get("detection_method", "unknown") if store_context else "unknown" ) base_url = "/" if access_method == "path" and store: # Use the full_prefix from store_context to determine which pattern was used full_prefix = ( store_context.get("full_prefix", "/store/") if store_context else "/store/" ) base_url = f"{full_prefix}{store.subdomain}/" data["base_url"] = base_url return data @staticmethod def _is_admin_user(request: Request) -> bool: """ Check if current user is an admin (for debug info display). This is a placeholder - implement based on your auth system. """ # TODO: Implement actual admin check based on JWT/session # For now, check if we're in admin frontend frontend_type = get_frontend_type(request) return frontend_type == FrontendType.ADMIN @staticmethod def _render_basic_html_fallback( status_code: int, error_code: str, message: str, ) -> HTMLResponse: """ Render a basic HTML error page as absolute fallback. Used when template rendering fails. """ html_content = f""" {status_code} - Error

{status_code}

Error

{message}

Error Code: {error_code}
""" return HTMLResponse(content=html_content, status_code=status_code)