/ src / solace_agent_mesh / shared / exceptions / exception_handlers.py
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)