""" Error handling middleware for ClaudeTools API. This module provides custom exception classes and global exception handlers for consistent error responses across the FastAPI application. """ from typing import Any, Dict, Optional from fastapi import FastAPI, Request, status from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from sqlalchemy.exc import SQLAlchemyError class ClaudeToolsException(Exception): """Base exception class for ClaudeTools application.""" def __init__( self, message: str, status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR, details: Optional[Dict[str, Any]] = None, ): """ Initialize the exception. Args: message: Human-readable error message status_code: HTTP status code for the error details: Optional dictionary with additional error details """ self.message = message self.status_code = status_code self.details = details or {} super().__init__(self.message) class AuthenticationError(ClaudeToolsException): """ Exception raised for authentication failures. This includes invalid credentials, expired tokens, or missing authentication. """ def __init__( self, message: str = "Authentication failed", details: Optional[Dict[str, Any]] = None ): """ Initialize authentication error. Args: message: Error message details: Optional additional details """ super().__init__( message=message, status_code=status.HTTP_401_UNAUTHORIZED, details=details, ) class AuthorizationError(ClaudeToolsException): """ Exception raised for authorization failures. This occurs when an authenticated user lacks permission for an action. """ def __init__( self, message: str = "Insufficient permissions", details: Optional[Dict[str, Any]] = None ): """ Initialize authorization error. Args: message: Error message details: Optional additional details """ super().__init__( message=message, status_code=status.HTTP_403_FORBIDDEN, details=details, ) class NotFoundError(ClaudeToolsException): """ Exception raised when a requested resource is not found. This should be used for missing users, organizations, tools, etc. """ def __init__( self, message: str = "Resource not found", resource_type: Optional[str] = None, resource_id: Optional[str] = None, ): """ Initialize not found error. Args: message: Error message resource_type: Optional type of resource (e.g., "User", "Tool") resource_id: Optional ID of the missing resource """ details = {} if resource_type: details["resource_type"] = resource_type if resource_id: details["resource_id"] = resource_id super().__init__( message=message, status_code=status.HTTP_404_NOT_FOUND, details=details, ) class ValidationError(ClaudeToolsException): """ Exception raised for business logic validation failures. This is separate from FastAPI's RequestValidationError and should be used for application-level validation (e.g., duplicate usernames, invalid state transitions). """ def __init__( self, message: str = "Validation failed", field: Optional[str] = None, details: Optional[Dict[str, Any]] = None, ): """ Initialize validation error. Args: message: Error message field: Optional field name that failed validation details: Optional additional details """ error_details = details or {} if field: error_details["field"] = field super().__init__( message=message, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, details=error_details, ) class ConflictError(ClaudeToolsException): """ Exception raised when a request conflicts with existing data. This includes duplicate entries, concurrent modifications, etc. """ def __init__( self, message: str = "Resource conflict", details: Optional[Dict[str, Any]] = None ): """ Initialize conflict error. Args: message: Error message details: Optional additional details """ super().__init__( message=message, status_code=status.HTTP_409_CONFLICT, details=details, ) class DatabaseError(ClaudeToolsException): """ Exception raised for database operation failures. This wraps SQLAlchemy errors with a consistent interface. """ def __init__( self, message: str = "Database operation failed", details: Optional[Dict[str, Any]] = None ): """ Initialize database error. Args: message: Error message details: Optional additional details """ super().__init__( message=message, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, details=details, ) async def claudetools_exception_handler( request: Request, exc: ClaudeToolsException ) -> JSONResponse: """ Handler for custom ClaudeTools exceptions. Args: request: The FastAPI request object exc: The ClaudeTools exception Returns: JSONResponse: Formatted error response """ return JSONResponse( status_code=exc.status_code, content={ "error": exc.message, "details": exc.details, "path": str(request.url.path), }, ) async def validation_exception_handler( request: Request, exc: RequestValidationError ) -> JSONResponse: """ Handler for FastAPI request validation errors. Args: request: The FastAPI request object exc: The validation error Returns: JSONResponse: Formatted error response """ errors = [] for error in exc.errors(): errors.append( { "field": ".".join(str(loc) for loc in error["loc"]), "message": error["msg"], "type": error["type"], } ) return JSONResponse( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content={ "error": "Request validation failed", "details": {"validation_errors": errors}, "path": str(request.url.path), }, ) async def sqlalchemy_exception_handler( request: Request, exc: SQLAlchemyError ) -> JSONResponse: """ Handler for SQLAlchemy database errors. Args: request: The FastAPI request object exc: The SQLAlchemy exception Returns: JSONResponse: Formatted error response """ return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={ "error": "Database operation failed", "details": {"type": type(exc).__name__}, "path": str(request.url.path), }, ) async def generic_exception_handler(request: Request, exc: Exception) -> JSONResponse: """ Handler for unhandled exceptions. Args: request: The FastAPI request object exc: The exception Returns: JSONResponse: Formatted error response """ return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={ "error": "Internal server error", "details": {"type": type(exc).__name__}, "path": str(request.url.path), }, ) def register_exception_handlers(app: FastAPI) -> None: """ Register all exception handlers with the FastAPI application. This should be called during application startup to ensure all exceptions are handled consistently. Args: app: The FastAPI application instance Example: ```python from fastapi import FastAPI from api.middleware.error_handler import register_exception_handlers app = FastAPI() register_exception_handlers(app) ``` """ app.add_exception_handler(ClaudeToolsException, claudetools_exception_handler) app.add_exception_handler(RequestValidationError, validation_exception_handler) app.add_exception_handler(SQLAlchemyError, sqlalchemy_exception_handler) app.add_exception_handler(Exception, generic_exception_handler)