Purpose: Demonstrate how to migrate API endpoints from manual try-catch blocks to the new @with_error_handling decorator
Status: Phase 2 - Implementation Guide Created: 2025-10-28 Part of: ERROR_HANDLING_REFACTORING_PLAN.md Phase 2
from src.utils.error_boundaries import with_error_handling, ErrorCategory@router.get("/chat/health")
async def chat_health_check(request: Request):
"""Health check for chat service"""
try:
chat_history_manager = getattr(request.app.state, "chat_history_manager", None)
llm_service = getattr(request.app.state, "llm_service", None)
health_status = {
"status": "healthy",
"timestamp": datetime.utcnow().isoformat(),
"components": {
"chat_history_manager": (
"healthy" if chat_history_manager else "unavailable"
),
"llm_service": "healthy" if llm_service else "unavailable",
},
}
overall_healthy = all(
status == "healthy" for status in health_status["components"].values()
)
if not overall_healthy:
health_status["status"] = "degraded"
return JSONResponse(
status_code=200 if overall_healthy else 503, content=health_status
)
except Exception as e:
logger.error(f"Health check failed: {e}")
return JSONResponse(
status_code=503,
content={
"status": "unhealthy",
"error": str(e),
"timestamp": datetime.utcnow().isoformat(),
},
)Issues with this pattern:
- 16 lines of error handling code (lines 1300-1309)
- Manual status code management
- Inconsistent error response format
- No trace ID for debugging
- No error code for cataloging
- Error handling logic mixed with business logic
@router.get("/chat/health")
@with_error_handling(
category=ErrorCategory.SERVICE_UNAVAILABLE,
operation="chat_health_check",
error_code_prefix="CHAT"
)
async def chat_health_check(request: Request):
"""Health check for chat service"""
chat_history_manager = getattr(request.app.state, "chat_history_manager", None)
llm_service = getattr(request.app.state, "llm_service", None)
health_status = {
"status": "healthy",
"timestamp": datetime.utcnow().isoformat(),
"components": {
"chat_history_manager": (
"healthy" if chat_history_manager else "unavailable"
),
"llm_service": "healthy" if llm_service else "unavailable",
},
}
overall_healthy = all(
status == "healthy" for status in health_status["components"].values()
)
if not overall_healthy:
health_status["status"] = "degraded"
return JSONResponse(
status_code=200 if overall_healthy else 503, content=health_status
)Benefits:
- ✅ Eliminated 16 lines of error handling code
- ✅ Automatic HTTP status code (503) from ErrorCategory.SERVICE_UNAVAILABLE
- ✅ Automatic trace ID generation for debugging
- ✅ Automatic error code (e.g., CHAT_0042)
- ✅ Standardized error response format
- ✅ Automatic error logging with context
- ✅ Business logic clearly separated from error handling
Error Response Example:
{
"error": {
"category": "service_unavailable",
"message": "Chat history manager not initialized",
"code": "CHAT_1234",
"timestamp": 1730107234.567,
"details": {
"operation": "chat_health_check"
},
"trace_id": "chat_health_check_1730107234567"
}
}@router.post("/chat")
@router.post("/chat/message") # Alternative endpoint
async def send_message(
message: ChatMessage,
request: Request,
config=Depends(get_config),
knowledge_base=Depends(get_knowledge_base),
):
"""Send a chat message and get AI response"""
request_id = generate_request_id()
try:
log_request_context(request, "send_message", request_id)
# Validate message content
if not message.content or not message.content.strip():
raise ValidationError("Message content cannot be empty")
# Get dependencies from request state
chat_history_manager = get_chat_history_manager(request)
llm_service = get_llm_service(request)
memory_interface = get_memory_interface(request)
# Process the chat message
response_data = await process_chat_message(
message,
chat_history_manager,
llm_service,
memory_interface,
knowledge_base,
config,
request_id,
)
return JSONResponse(
status_code=200,
content={
"success": True,
"data": response_data,
"message": "Message processed successfully",
"request_id": request_id,
},
)
except Exception as e:
logger.error(f"[{request_id}] send_message error: {e}")
return create_error_response(
error_code="INTERNAL_ERROR",
message=str(e),
request_id=request_id,
status_code=500,
)Issues:
- Generic Exception catching (loses error context)
- Manual request_id generation
- Calls
create_error_response()helper (another layer of abstraction) - Hardcoded status code (500)
- No error categorization
@router.post("/chat")
@router.post("/chat/message") # Alternative endpoint
@with_error_handling(
category=ErrorCategory.SERVER_ERROR,
operation="send_message",
error_code_prefix="CHAT"
)
async def send_message(
message: ChatMessage,
request: Request,
config=Depends(get_config),
knowledge_base=Depends(get_knowledge_base),
):
"""Send a chat message and get AI response"""
# Validate message content
if not message.content or not message.content.strip():
raise ValueError("Message content cannot be empty") # Will be caught by decorator
# Get dependencies from request state
chat_history_manager = get_chat_history_manager(request)
llm_service = get_llm_service(request)
memory_interface = get_memory_interface(request)
# Process the chat message
response_data = await process_chat_message(
message,
chat_history_manager,
llm_service,
memory_interface,
knowledge_base,
config,
None, # No manual request_id - trace_id auto-generated
)
return JSONResponse(
status_code=200,
content={
"success": True,
"data": response_data,
"message": "Message processed successfully",
},
)Benefits:
- ✅ Removed 9 lines of error handling code
- ✅ Removed manual request_id generation (decorator provides trace_id)
- ✅ Removed
create_error_response()call - ✅ Automatic error categorization
- ✅ Simpler, cleaner code
- ✅ Consistent error format across all endpoints
@router.get("/chat/sessions/{session_id}")
@with_error_handling(
category=ErrorCategory.NOT_FOUND,
operation="get_session",
error_code_prefix="SESSION"
)
async def get_session(session_id: str, request: Request):
"""Get chat session details"""
chat_history_manager = get_chat_history_manager(request)
session = await chat_history_manager.get_session(session_id)
if not session:
raise ValueError(f"Session {session_id} not found") # → 404 error
return sessionError Response (404):
{
"error": {
"category": "not_found",
"message": "Session abc123 not found",
"code": "SESSION_0567",
"timestamp": 1730107234.567,
"trace_id": "get_session_1730107234567"
}
}@router.post("/chat/validate")
@with_error_handling(
category=ErrorCategory.VALIDATION,
operation="validate_message",
error_code_prefix="VAL"
)
async def validate_message(data: dict):
"""Validate message data"""
if "content" not in data:
raise ValueError("Missing required field: content") # → 400 error
if len(data["content"]) > 10000:
raise ValueError("Content too long (max 10000 chars)") # → 400 error
return {"valid": True}Error Response (400):
{
"error": {
"category": "validation",
"message": "Missing required field: content",
"code": "VAL_0123",
"timestamp": 1730107234.567
}
}@router.post("/chat/message")
@with_error_handling(
category=ErrorCategory.RATE_LIMIT,
operation="send_message",
error_code_prefix="RATE"
)
async def send_message(message: ChatMessage, request: Request):
"""Send message with rate limiting"""
if await is_rate_limited(request):
raise ValueError("Too many requests") # → 429 error
return await process_message(message)Error Response (429):
{
"error": {
"category": "rate_limit",
"message": "Too many requests",
"code": "RATE_8901",
"timestamp": 1730107234.567,
"retry_after": 60
}
}| ErrorCategory | HTTP Status | Use Case |
|---|---|---|
| VALIDATION | 400 | Invalid input, missing fields |
| AUTHENTICATION | 401 | Invalid credentials, expired token |
| AUTHORIZATION | 403 | Insufficient permissions |
| NOT_FOUND | 404 | Resource doesn't exist |
| CONFLICT | 409 | Resource already exists |
| RATE_LIMIT | 429 | Too many requests |
| SERVER_ERROR | 500 | Internal server error |
| EXTERNAL_SERVICE | 502 | Upstream service error |
| SERVICE_UNAVAILABLE | 503 | Service temporarily down |
For each endpoint:
-
Add Import (top of file):
from src.utils.error_boundaries import with_error_handling, ErrorCategory
-
Add Decorator (before endpoint function):
@with_error_handling( category=ErrorCategory.SERVER_ERROR, # Choose appropriate category operation="function_name", error_code_prefix="MODULE" )
-
Remove Try-Catch Block:
- Delete
try:line - Un-indent business logic
- Delete
except Exception as e:block - Delete manual error response creation
- Delete
-
Remove Manual Error Handling:
- Delete
request_id = generate_request_id()(if only used for errors) - Delete
logger.error()calls (decorator logs automatically) - Delete
create_error_response()calls - Delete
return JSONResponse(status_code=500, ...)for errors
- Delete
-
Test:
- Verify success case works
- Verify error case returns correct status code
- Verify error response has trace_id
- Check logs for proper error logging
- Before: ~60 lines (business logic + error handling)
- After: ~40 lines (business logic only)
- Savings: 20 lines (33% reduction)
- Error handling: 3 lines decorator vs 15+ lines manual
- chat.py: 32 try blocks → ~640 lines savings
- knowledge.py: 76 try blocks → ~1,520 lines savings
- Total backend: 1,070 try blocks → ~21,400 lines savings
# Test endpoint
response = client.post("/chat/message", json={"content": ""})
# Verify response
assert response.status_code == 400
assert "error" in response.json()
assert response.json()["error"]["category"] == "validation"
assert "trace_id" in response.json()["error"]
assert "code" in response.json()["error"]# Errors automatically logged with context:
# ERROR:src.utils.error_boundaries:Error in send_message: Message content cannot be empty
# (trace_id: send_message_1730107234567, code: CHAT_0042)-
Phase 2a (Sprint 2): Migrate high-traffic endpoints
autobot-backend/api/chat.py(32 try blocks)autobot-backend/api/knowledge.py(76 try blocks)
-
Phase 2b (Sprint 3): Migrate remaining API endpoints
- Session, workflow, file browser, terminal APIs
-
Phase 3 (Sprint 4): Migrate core service layer
- src/knowledge_base.py
- src/chat_workflow_manager.py
- src/llm_interface.py
Q: What if I need custom error handling? A: Keep the try-catch for specific logic, use decorator for generic errors:
@with_error_handling(category=ErrorCategory.SERVER_ERROR)
async def complex_operation():
try:
specific_risky_operation()
except SpecificError as e:
# Handle specific case
return custom_response()
# Other errors caught by decorator
general_operation()Q: Can I use multiple decorators? A: Yes, but only one @with_error_handling per function
Q: What about streaming endpoints? A: Decorator works with StreamingResponse, but error handling is limited once streaming starts
Document Status: Ready for Phase 2 Implementation Last Updated: 2025-10-28 Related: docs/developer/ERROR_HANDLING_REFACTORING_PLAN.md