# 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 Optional, Dict, Any from fastapi import Request from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates from middleware.context_middleware import RequestContext, get_request_context 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: Optional[Dict[str, Any]] = None, show_debug: bool = False, ) -> HTMLResponse: """ Render appropriate error page based on request context. Template Selection Priority: 1. Context-specific error page: {context}/errors/{status_code}.html 2. Context-specific generic: {context}/errors/generic.html 3. Fallback error page: fallback/{status_code}.html 4. Fallback generic: fallback/generic.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 request context context_type = get_request_context(request) # Prepare template data template_data = ErrorPageRenderer._prepare_template_data( request=request, context_type=context_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( context_type=context_type, status_code=status_code, ) logger.info( f"Rendering error page: {template_path} for {status_code} in {context_type.value} context", extra={ "status_code": status_code, "error_code": error_code, "context": context_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( context_type: RequestContext, status_code: int, ) -> str: """ Find appropriate error template based on context and status code. Priority: 1. {context}/errors/{status_code}.html 2. {context}/errors/generic.html 3. fallback/{status_code}.html 4. fallback/generic.html """ templates_dir = ErrorPageRenderer.get_templates_dir() # Map context type to folder name context_folders = { RequestContext.ADMIN: "admin", RequestContext.VENDOR_DASHBOARD: "vendor", RequestContext.SHOP: "shop", RequestContext.API: "fallback", # API shouldn't get here, but just in case RequestContext.FALLBACK: "fallback", } context_folder = context_folders.get(context_type, "fallback") # Try context-specific status code template specific_template = f"{context_folder}/errors/{status_code}.html" if (templates_dir / specific_template).exists(): return specific_template # Try context-specific generic template generic_template = f"{context_folder}/errors/generic.html" if (templates_dir / generic_template).exists(): return generic_template # Try fallback status code template fallback_specific = f"fallback/{status_code}.html" if (templates_dir / fallback_specific).exists(): return fallback_specific # Use fallback generic template (must exist) return "fallback/generic.html" @staticmethod def _prepare_template_data( request: Request, context_type: RequestContext, status_code: int, error_code: str, message: str, details: Optional[Dict[str, Any]], 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 context-specific data context_data = ErrorPageRenderer._get_context_data(request, context_type) return { "status_code": status_code, "status_name": status_name, "error_code": error_code, "message": user_message, "details": details or {}, "show_debug": display_debug, "context_type": context_type.value, "path": request.url.path, **context_data, } @staticmethod def _get_context_data(request: Request, context_type: RequestContext) -> Dict[str, Any]: """Get context-specific data for error templates.""" data = {} # Add vendor information if available (for shop context) if context_type == RequestContext.SHOP: vendor = getattr(request.state, "vendor", None) if vendor: # Pass minimal vendor info for templates data["vendor"] = { "id": vendor.id, "name": vendor.name, "subdomain": vendor.subdomain, "logo": getattr(vendor, "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), } 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 context context_type = get_request_context(request) return context_type == RequestContext.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"""
{message}