# 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 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. 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 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. shared/{status_code}-fallback.html 4. shared/generic-fallback.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 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, 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), } # Calculate base_url for shop links vendor_context = getattr(request.state, "vendor_context", None) access_method = vendor_context.get('detection_method', 'unknown') if vendor_context else 'unknown' base_url = "/" if access_method == "path" and vendor: # Use the full_prefix from vendor_context to determine which pattern was used full_prefix = vendor_context.get('full_prefix', '/vendor/') if vendor_context else '/vendor/' base_url = f"{full_prefix}{vendor.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 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""" {status_code} - Error

{status_code}

Error

{message}

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