Files
claudetools/api/middleware/error_handler.py
Mike Swanson 390b10b32c Complete Phase 6: MSP Work Tracking with Context Recall System
Implements production-ready MSP platform with cross-machine persistent memory for Claude.

API Implementation:
- 130 REST API endpoints across 21 entities
- JWT authentication on all endpoints
- AES-256-GCM encryption for credentials
- Automatic audit logging
- Complete OpenAPI documentation

Database:
- 43 tables in MariaDB (172.16.3.20:3306)
- 42 SQLAlchemy models with modern 2.0 syntax
- Full Alembic migration system
- 99.1% CRUD test pass rate

Context Recall System (Phase 6):
- Cross-machine persistent memory via database
- Automatic context injection via Claude Code hooks
- Automatic context saving after task completion
- 90-95% token reduction with compression utilities
- Relevance scoring with time decay
- Tag-based semantic search
- One-command setup script

Security Features:
- JWT tokens with Argon2 password hashing
- AES-256-GCM encryption for all sensitive data
- Comprehensive audit trail for credentials
- HMAC tamper detection
- Secure configuration management

Test Results:
- Phase 3: 38/38 CRUD tests passing (100%)
- Phase 4: 34/35 core API tests passing (97.1%)
- Phase 5: 62/62 extended API tests passing (100%)
- Phase 6: 10/10 compression tests passing (100%)
- Overall: 144/145 tests passing (99.3%)

Documentation:
- Comprehensive architecture guides
- Setup automation scripts
- API documentation at /api/docs
- Complete test reports
- Troubleshooting guides

Project Status: 95% Complete (Production-Ready)
Phase 7 (optional work context APIs) remains for future enhancement.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 06:00:26 -07:00

325 lines
8.6 KiB
Python

"""
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)