Error Handling¶
Global exception handlers and standardized error response formats
Key Files:
backend/api/exception_handlers.py:1-771- Global exception handlersbackend/api/middleware/error_handler.py:1-356- Error classes and ErrorResponse schemabackend/api/middleware/exception_handler.py:1-100- Error message sanitizationbackend/api/schemas/problem_details.py- RFC 7807 Problem Details schemabackend/core/exceptions.py- Base exception hierarchy
Overview¶
The error handling system provides centralized exception handling that converts all exceptions to standardized error responses. It ensures consistent error formats across all API endpoints, proper logging with request context, and sanitization of error messages to prevent information leakage.
The system implements RFC 7807 "Problem Details for HTTP APIs" for standard HTTP exceptions, providing machine-readable error responses. Application-specific exceptions use a custom format with error codes, messages, and optional details.
Architecture¶
graph TD
subgraph "Exception Sources"
ROUTE[Route Handler] --> EXC
MW[Middleware] --> EXC
DEP[Dependencies] --> EXC
end
subgraph "Exception Processing"
EXC{Exception<br/>Raised} --> TYPE
TYPE{Exception<br/>Type?}
TYPE -->|CircuitBreakerOpenError| CBH[circuit_breaker_exception_handler<br/>backend/api/exception_handlers.py:461]
TYPE -->|RateLimitError| RLH[rate_limit_exception_handler<br/>backend/api/exception_handlers.py:506]
TYPE -->|ExternalServiceError| ESH[external_service_exception_handler<br/>backend/api/exception_handlers.py:550]
TYPE -->|SecurityIntelligenceError| SIH[security_intelligence_exception_handler<br/>backend/api/exception_handlers.py:195]
TYPE -->|RequestValidationError| VEH[validation_exception_handler<br/>backend/api/exception_handlers.py:305]
TYPE -->|PydanticValidationError| PVH[pydantic_validation_handler<br/>backend/api/exception_handlers.py:377]
TYPE -->|HTTPException| HEH[problem_details_exception_handler<br/>backend/api/exception_handlers.py:121]
TYPE -->|SQLAlchemyError| SQLH[sqlalchemy_exception_handler<br/>backend/api/exception_handlers.py:591]
TYPE -->|RedisError| REH[redis_exception_handler<br/>backend/api/exception_handlers.py:649]
TYPE -->|Exception| GEH[generic_exception_handler<br/>backend/api/exception_handlers.py:417]
end
subgraph "Response Generation"
CBH --> RESP[JSONResponse]
RLH --> RESP
ESH --> RESP
SIH --> RESP
VEH --> RESP
PVH --> RESP
HEH --> RESP
SQLH --> RESP
REH --> RESP
GEH --> RESP
end
RESP --> CLIENT[Client]
style TYPE fill:#fbb,stroke:#333
style RESP fill:#bfb,stroke:#333 Exception Handler Registration¶
Exception handlers are registered in order of specificity in backend/api/exception_handlers.py:695-770:
# From backend/api/exception_handlers.py:695-770
def register_exception_handlers(app: FastAPI) -> None:
"""Register all exception handlers with the FastAPI application."""
# Register handlers in order of specificity (most specific first)
# Circuit breaker errors (specific external service error)
app.add_exception_handler(CircuitBreakerOpenError, circuit_breaker_exception_handler)
# Rate limit errors
app.add_exception_handler(RateLimitError, rate_limit_exception_handler)
# External service errors (includes AI services, database, cache)
app.add_exception_handler(ExternalServiceError, external_service_exception_handler)
# Application-specific errors (includes validation, not found, etc.)
app.add_exception_handler(SecurityIntelligenceError, security_intelligence_exception_handler)
# FastAPI request validation errors
app.add_exception_handler(RequestValidationError, validation_exception_handler)
# Pydantic validation errors (response serialization)
app.add_exception_handler(PydanticValidationError, pydantic_validation_handler)
# Standard HTTP exceptions - RFC 7807 Problem Details format
app.add_exception_handler(StarletteHTTPException, problem_details_exception_handler)
# SQLAlchemy database errors
app.add_exception_handler(SQLAlchemyError, sqlalchemy_exception_handler)
# Redis cache errors
app.add_exception_handler(RedisError, redis_exception_handler)
# Catch-all for any unhandled exceptions
app.add_exception_handler(Exception, generic_exception_handler)
Error Response Formats¶
Standard Error Response¶
The standard error response format is used for application-specific exceptions (backend/api/middleware/error_handler.py:105-163):
{
"error": {
"code": "NOT_FOUND",
"message": "Camera 'front_door' not found",
"details": {
"camera_id": "front_door"
},
"request_id": "abc12345",
"timestamp": "2024-01-15T10:30:00.123456Z"
}
}
RFC 7807 Problem Details¶
HTTP exceptions use RFC 7807 "Problem Details" format (backend/api/exception_handlers.py:121-193):
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"detail": "Camera 'front_door' does not exist",
"instance": "/api/cameras/front_door"
}
This format is returned with media type application/problem+json.
Validation Error Response¶
Request validation errors include field-level details (backend/api/exception_handlers.py:305-374):
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"errors": [
{
"field": "body.risk_score",
"message": "Input should be less than or equal to 100",
"value": "150"
},
{
"field": "body.camera_id",
"message": "Field required",
"value": null
}
],
"request_id": "abc12345",
"timestamp": "2024-01-15T10:30:00.123456Z"
}
}
Exception Hierarchy¶
Application Exceptions¶
The SecurityIntelligenceError base class (backend/core/exceptions.py) provides:
| Exception | Status Code | Error Code | Use Case |
|---|---|---|---|
NotFoundError | 404 | NOT_FOUND | Resource not found |
ValidationError | 422 | VALIDATION_ERROR | Input validation failed |
ConflictError | 409 | CONFLICT | Resource conflict |
AuthenticationError | 401 | AUTHENTICATION_REQUIRED | Auth required |
AuthorizationError | 403 | ACCESS_DENIED | Permission denied |
RateLimitError | 429 | RATE_LIMIT_EXCEEDED | Rate limit hit |
ExternalServiceError | 503 | EXTERNAL_SERVICE_ERROR | External service failure |
CircuitBreakerOpenError | 503 | CIRCUIT_BREAKER_OPEN | Circuit breaker tripped |
AppException Classes¶
The AppException class (backend/api/middleware/error_handler.py:170-239) provides a simpler interface:
# From backend/api/middleware/error_handler.py:170-239
class AppException(Exception):
"""Base exception for application-specific API errors."""
status_code: int = 500
error: str = "internal_error"
def __init__(
self,
message: str | None = None,
*,
status_code: int | None = None,
error: str | None = None,
details: dict[str, Any] | None = None,
) -> None:
self.message = message or self._get_default_message()
if status_code is not None:
self.status_code = status_code
if error is not None:
self.error = error
self.details = details
super().__init__(self.message)
Error Code Mapping¶
HTTP status codes are mapped to error codes in backend/api/exception_handlers.py:260-274:
# From backend/api/exception_handlers.py:260-274
status_to_code = {
400: "BAD_REQUEST",
401: "AUTHENTICATION_REQUIRED",
403: "ACCESS_DENIED",
404: "NOT_FOUND",
405: "METHOD_NOT_ALLOWED",
409: "CONFLICT",
422: "VALIDATION_ERROR",
429: "RATE_LIMIT_EXCEEDED",
500: "INTERNAL_ERROR",
502: "BAD_GATEWAY",
503: "SERVICE_UNAVAILABLE",
504: "GATEWAY_TIMEOUT",
}
Error Message Sanitization¶
Error messages are sanitized to prevent information leakage (backend/api/middleware/exception_handler.py:61-100):
# From backend/api/middleware/exception_handler.py:61-100
def create_safe_error_message(
error: Exception,
*,
context: str = "",
include_exception_type: bool = False,
) -> str:
"""Create a safe error message for API responses.
This function wraps sanitize_error_for_response with additional options
for API response formatting.
"""
safe_message = sanitize_error_for_response(error, context=context)
if include_exception_type:
exception_type = type(error).__name__
if context:
return safe_message
return f"{exception_type}: {safe_message}"
return safe_message
The sanitization (backend/core/sanitization.py) removes:
- File paths (keeps filename only)
- URL credentials
- IP addresses
- API keys and tokens
- Password values
- Bearer tokens
Special Exception Handlers¶
Database Errors¶
SQLAlchemy errors are handled in backend/api/exception_handlers.py:591-646:
| Exception Type | Status | Error Code | Message |
|---|---|---|---|
IntegrityError | 409 | DATABASE_CONFLICT | Database constraint violation |
Other SQLAlchemyError | 503 | DATABASE_UNAVAILABLE | Database service temporarily unavailable |
Redis Errors¶
Redis errors return 503 with CACHE_UNAVAILABLE (backend/api/exception_handlers.py:649-693).
Rate Limit Errors¶
Rate limit errors include retry information (backend/api/exception_handlers.py:506-548):
# Response includes Retry-After header
if exc.details and "retry_after" in exc.details:
response.headers["Retry-After"] = str(exc.details["retry_after"])
Circuit Breaker Errors¶
Circuit breaker errors include service name and recovery timeout (backend/api/exception_handlers.py:461-503):
{
"error": {
"code": "CIRCUIT_BREAKER_OPEN",
"message": "Service temporarily unavailable",
"details": {
"service": "yolo26",
"recovery_timeout_seconds": 30
}
}
}
Logging¶
All exception handlers log errors with appropriate levels and context:
| Status Code | Log Level | Context |
|---|---|---|
| 5xx | ERROR | Full traceback, request ID, path, method |
| 429 | WARNING | Rate limit details |
| 4xx | INFO | Request ID, path, method |
Example logging from backend/api/exception_handlers.py:157-172:
# From backend/api/exception_handlers.py:157-172
log_context = {
"status_code": exc.status_code,
"path": str(request.url.path),
"method": request.method,
}
request_id = get_request_id(request)
if request_id:
log_context["request_id"] = request_id
if exc.status_code >= 500:
logger.error(f"HTTP error: {detail}", extra=log_context)
elif exc.status_code == 429:
logger.warning(f"Rate limit: {detail}", extra=log_context)
elif exc.status_code >= 400:
logger.info(f"Client error: {detail}", extra=log_context)
Usage Examples¶
Raising Application Errors¶
from backend.api.middleware.error_handler import NotFoundError, ValidationError
@router.get("/cameras/{camera_id}")
async def get_camera(camera_id: str) -> Camera:
camera = await get_camera_by_id(camera_id)
if not camera:
raise NotFoundError(
message=f"Camera '{camera_id}' not found",
details={"camera_id": camera_id},
)
return camera
@router.post("/events")
async def create_event(event: EventCreate) -> Event:
if event.risk_score > 100:
raise ValidationError(
message="Risk score must be between 0 and 100",
details={"field": "risk_score", "value": event.risk_score},
)
return await create_event_in_db(event)
Custom Exception with Details¶
from backend.api.middleware.error_handler import ServiceUnavailableError
@router.post("/analyze")
async def analyze_image(image_path: str):
try:
result = await ai_service.analyze(image_path)
except AIServiceError as e:
raise ServiceUnavailableError(
message="Object detection service temporarily unavailable",
details={"service": "yolo26", "retry_after": 30},
)
return result
Testing¶
Test coverage is in backend/tests/unit/api/test_exception_handlers.py.
Running Tests¶
# Run exception handler tests
uv run pytest backend/tests/unit/api/test_exception_handlers.py -v
# Run with coverage
uv run pytest backend/tests/unit/api/ --cov=backend.api.exception_handlers
Related Documents¶
- Request Logging - Request ID propagation for error tracing
- Rate Limiting - Rate limit error responses
- Resilience Patterns - Circuit breaker errors
Last updated: 2025-01-24 - Created for NEM-3461