exception_handlers.py
1 """ 2 Generic FastAPI exception handlers for consistent HTTP error responses. 3 4 This module provides FastAPI exception handlers that convert domain exceptions 5 into appropriate HTTP responses with consistent formatting. These handlers 6 can be used by any FastAPI application for uniform error handling. 7 """ 8 9 import logging 10 11 from fastapi import HTTPException, Request, status 12 from fastapi.responses import JSONResponse 13 from fastapi.exceptions import RequestValidationError 14 from pydantic import ValidationError as PydanticValidationError 15 from starlette.exceptions import HTTPException as StarletteHTTPException 16 17 from .exceptions import ( 18 WebUIBackendException, 19 ValidationError, 20 EntityNotFoundError, 21 EntityAlreadyExistsError, 22 BusinessRuleViolationError, 23 ConfigurationError, 24 DataIntegrityError, 25 ExternalServiceError, 26 InternalServiceError, 27 ) 28 from .error_dto import EventErrorDTO 29 30 log = logging.getLogger(__name__) 31 32 33 def create_error_response( 34 status_code: int, message: str, validation_details: dict = None 35 ) -> JSONResponse: 36 """Create standardized error response using EventErrorDTO format.""" 37 if validation_details: 38 error_dto = EventErrorDTO.validation_error(message, validation_details) 39 else: 40 error_dto = EventErrorDTO.create(message) 41 42 return JSONResponse(status_code=status_code, content=error_dto.model_dump()) 43 44 45 async def validation_error_handler( 46 request: Request, exc: ValidationError 47 ) -> JSONResponse: 48 """Handle domain validation errors - 422 Unprocessable Entity.""" 49 if exc.validation_details: 50 error_dto = EventErrorDTO.validation_error(exc.message, exc.validation_details) 51 else: 52 error_dto = EventErrorDTO.create("bad request" if not exc.message else exc.message) 53 54 return JSONResponse(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content=error_dto.model_dump()) 55 56 57 async def entity_not_found_handler( 58 request: Request, exc: EntityNotFoundError 59 ) -> JSONResponse: 60 """Handle entity not found errors - 404 Not Found.""" 61 # Format: "Could not find applicationDomain with id: some-invalid-id" 62 message = f"Could not find {exc.entity_type} with id: {exc.entity_id}" 63 error_dto = EventErrorDTO.create(message) 64 return JSONResponse(status_code=status.HTTP_404_NOT_FOUND, content=error_dto.model_dump()) 65 66 67 async def entity_already_exists_handler( 68 request: Request, exc: EntityAlreadyExistsError 69 ) -> JSONResponse: 70 """Handle entity already exists errors - 409 Conflict.""" 71 error_dto = EventErrorDTO.create(exc.message) 72 return JSONResponse(status_code=status.HTTP_409_CONFLICT, content=error_dto.model_dump()) 73 74 75 async def business_rule_violation_handler( 76 request: Request, exc: BusinessRuleViolationError 77 ) -> JSONResponse: 78 """Handle business rule violations - 422 Unprocessable Entity.""" 79 error_dto = EventErrorDTO.create(exc.message) 80 return JSONResponse(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content=error_dto.model_dump()) 81 82 83 async def configuration_error_handler( 84 request: Request, exc: ConfigurationError 85 ) -> JSONResponse: 86 """Handle configuration errors - 500 Internal Server Error.""" 87 error_dto = EventErrorDTO.create("An unexpected server error occurred.") 88 return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=error_dto.model_dump()) 89 90 91 async def data_integrity_error_handler( 92 request: Request, exc: DataIntegrityError 93 ) -> JSONResponse: 94 """Handle data integrity errors - 422 Unprocessable Entity.""" 95 # Format: "An entity of type applicationDomain was passed in an invalid format" 96 message = f"An entity of type {exc.entity_type} was passed in an invalid format" if hasattr(exc, 'entity_type') else "bad request" 97 error_dto = EventErrorDTO.create(message) 98 return JSONResponse(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content=error_dto.model_dump()) 99 100 101 async def external_service_error_handler( 102 request: Request, exc: ExternalServiceError 103 ) -> JSONResponse: 104 """Handle external service errors - 503 Service Unavailable.""" 105 message = exc.message if exc.message else "Service is unavailable." 106 error_dto = EventErrorDTO.create(message) 107 return JSONResponse(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, content=error_dto.model_dump()) 108 109 110 async def internal_service_error_handler( 111 request: Request, exc: InternalServiceError 112 ) -> JSONResponse: 113 """Handle unexpected internal errors - 500 Internal Server Error.""" 114 log.error( 115 "InternalServiceError: %s", 116 exc.message, 117 extra={"path": request.url.path, "method": request.method}, 118 exc_info=True 119 ) 120 error_dto = EventErrorDTO.create("An unexpected error occurred.") 121 return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=error_dto.model_dump()) 122 123 124 async def webui_backend_exception_handler( 125 request: Request, exc: WebUIBackendException 126 ) -> JSONResponse: 127 """Handle generic WebUI backend exceptions - 500 Internal Server Error.""" 128 log.error( 129 f"WebUIBackendException: {exc.message}", 130 extra={ 131 "path": request.url.path, 132 "method": request.method, 133 "details": exc.details if hasattr(exc, 'details') else None 134 }, 135 exc_info=True 136 ) 137 138 message = exc.message if exc.message else "An unexpected server error occurred." 139 error_dto = EventErrorDTO.create(message) 140 return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=error_dto.model_dump()) 141 142 143 async def http_exception_handler( 144 request: Request, exc: HTTPException 145 ) -> JSONResponse: 146 """Handle FastAPI HTTPExceptions with standardized format.""" 147 # Map common HTTP status codes to standard messages (only used as fallback) 148 message_map = { 149 401: "An authentication error occurred. Try logging out and in again.", 150 403: "You do not have permissions to perform this operation", 151 404: f"Resource not found with path {request.url.path}", 152 405: f"Request method '{request.method}' is not supported", 153 406: "Unacceptable Content-type.", 154 429: "Rate limit exceeded message here", 155 500: "An unexpected server error occurred.", 156 501: "Not Implemented", 157 503: "Service is unavailable.", 158 } 159 160 message = exc.detail if exc.detail else message_map.get(exc.status_code, "bad request") 161 error_dto = EventErrorDTO.create(message) 162 return JSONResponse(status_code=exc.status_code, content=error_dto.model_dump()) 163 164 165 async def request_validation_exception_handler( 166 request: Request, exc: RequestValidationError 167 ) -> JSONResponse: 168 """Handle FastAPI request validation errors - 422 Unprocessable Entity.""" 169 validation_details = {} 170 for error in exc.errors(): 171 field_path = ".".join(str(x) for x in error["loc"] if x != "body") 172 if field_path not in validation_details: 173 validation_details[field_path] = [] 174 validation_details[field_path].append(error["msg"]) 175 176 if validation_details: 177 message = "body must not be empty" if not validation_details else "Validation error" 178 error_dto = EventErrorDTO.validation_error(message, validation_details) 179 else: 180 error_dto = EventErrorDTO.create("bad request") 181 182 return JSONResponse(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content=error_dto.model_dump()) 183 184 185 async def pydantic_validation_exception_handler( 186 request: Request, exc: PydanticValidationError 187 ) -> JSONResponse: 188 """Handle Pydantic validation errors raised in service layer - 422 Unprocessable Entity.""" 189 validation_details = {} 190 for error in exc.errors(): 191 field_path = ".".join(str(loc) for loc in error["loc"]) 192 if field_path not in validation_details: 193 validation_details[field_path] = [] 194 validation_details[field_path].append(error["msg"]) 195 196 error_dto = EventErrorDTO.validation_error("Validation failed", validation_details) 197 return JSONResponse(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content=error_dto.model_dump()) 198 199 200 def register_exception_handlers(app): 201 """ 202 Register all exception handlers with a FastAPI app. 203 204 This function registers all the generic exception handlers, providing 205 consistent error responses across the entire application. 206 207 Args: 208 app: FastAPI application instance 209 210 Example: 211 from fastapi import FastAPI 212 from solace_agent_mesh.shared.exceptions.exception_handlers import register_exception_handlers 213 214 app = FastAPI() 215 register_exception_handlers(app) 216 """ 217 # Domain exception handlers 218 app.add_exception_handler(ValidationError, validation_error_handler) 219 app.add_exception_handler(EntityNotFoundError, entity_not_found_handler) 220 app.add_exception_handler(EntityAlreadyExistsError, entity_already_exists_handler) 221 app.add_exception_handler( 222 BusinessRuleViolationError, business_rule_violation_handler 223 ) 224 app.add_exception_handler(ConfigurationError, configuration_error_handler) 225 app.add_exception_handler(DataIntegrityError, data_integrity_error_handler) 226 app.add_exception_handler(ExternalServiceError, external_service_error_handler) 227 app.add_exception_handler(InternalServiceError, internal_service_error_handler) 228 app.add_exception_handler(WebUIBackendException, webui_backend_exception_handler) 229 230 # FastAPI built-in exception handlers 231 app.add_exception_handler(HTTPException, http_exception_handler) 232 app.add_exception_handler(StarletteHTTPException, http_exception_handler) 233 app.add_exception_handler(RequestValidationError, request_validation_exception_handler) 234 app.add_exception_handler(PydanticValidationError, pydantic_validation_exception_handler)