Files
claudetools/api/middleware/error_handler.py
Mike Swanson 73573800b0 feat: coord API — no-auth, DB softfail 503, agent tracking protocol
- coord routers: removed JWT auth requirement (internal-only endpoints)
- error_handler: SQLAlchemy OperationalError/DisconnectionError → 503
  with Retry-After: 30 header instead of 500
- /health: live DB probe (SELECT 1) instead of static response
- CLAUDE.md: "Live State Tracking" section with full agent protocol
  for all projects — session start, lock claim/release, component
  state updates, softfail + local queue catch-up
- COORDINATION_PROTOCOL.md: softfail/catch-up section + server-side
  503 behavior documented

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 08:45:33 -07:00

338 lines
9.2 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 DisconnectionError, OperationalError, 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 db_unavailable_exception_handler(request: Request, exc: Exception) -> JSONResponse:
return JSONResponse(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
content={
"error": "Database unavailable. Retry after 30 seconds.",
"path": str(request.url.path),
},
headers={"Retry-After": "30"},
)
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(OperationalError, db_unavailable_exception_handler)
app.add_exception_handler(DisconnectionError, db_unavailable_exception_handler)
app.add_exception_handler(Exception, generic_exception_handler)