228 lines
6.7 KiB
Plaintext
228 lines
6.7 KiB
Plaintext
# Error Handling Guide
|
|
|
|
This guide outlines Honcho's error handling strategy, providing consistent patterns for handling exceptions throughout the codebase.
|
|
|
|
## Core Principles
|
|
|
|
1. **User-friendly error messages**: Errors should be informative for API consumers but avoid exposing internal details.
|
|
2. **Consistent response format**: All API errors return JSON with a `detail` field.
|
|
3. **Appropriate status codes**: Use standard HTTP status codes consistently.
|
|
4. **Structured logging**: Log errors with context for troubleshooting.
|
|
5. **Centralized error management**: Define custom exceptions and handle them globally.
|
|
|
|
## Custom Exception Types
|
|
|
|
Honcho defines custom exception types in `src/exceptions.py`:
|
|
|
|
```python
|
|
class HonchoException(Exception):
|
|
status_code = 500 # Default status code
|
|
detail = "An unexpected error occurred" # Default message
|
|
|
|
class ResourceNotFoundException(HonchoException):
|
|
status_code = 404
|
|
detail = "Resource not found"
|
|
|
|
class ValidationException(HonchoException):
|
|
status_code = 422
|
|
detail = "Validation error"
|
|
|
|
class ConflictException(HonchoException):
|
|
status_code = 409
|
|
detail = "Resource conflict"
|
|
|
|
class AuthenticationException(HonchoException):
|
|
status_code = 401
|
|
detail = "Authentication failed"
|
|
|
|
class AuthorizationException(HonchoException):
|
|
status_code = 403
|
|
detail = "Not authorized to access this resource"
|
|
```
|
|
|
|
## Error Handling by Layer
|
|
|
|
### CRUD Layer
|
|
|
|
Use specific custom exceptions rather than generic `ValueError` or returning `None`:
|
|
|
|
```python
|
|
# Good
|
|
async def get_user(db, app_id, user_id):
|
|
user = await db.execute(stmt).scalar_one_or_none()
|
|
if user is None:
|
|
raise ResourceNotFoundException(f"User with ID {user_id} not found")
|
|
return user
|
|
|
|
# Avoid
|
|
async def get_user(db, app_id, user_id):
|
|
user = await db.execute(stmt).scalar_one_or_none()
|
|
return user # Returning None isn't explicit
|
|
```
|
|
|
|
For database integrity errors, catch and convert them to custom exceptions:
|
|
|
|
```python
|
|
try:
|
|
db.add(user)
|
|
await db.commit()
|
|
return user
|
|
except IntegrityError as e:
|
|
await db.rollback()
|
|
logger.error(f"IntegrityError creating user: {str(e)}")
|
|
raise ConflictException("User already exists") from e
|
|
```
|
|
|
|
### Router Layer
|
|
|
|
FastAPI routes should be simple, letting global exception handlers do most of the work:
|
|
|
|
```python
|
|
@router.get("/{user_id}")
|
|
async def get_user(app_id: str, user_id: str, db: AsyncSession = db):
|
|
# ResourceNotFoundException will be caught by global handler
|
|
return await crud.get_user(db, app_id, user_id)
|
|
```
|
|
|
|
For specific handling, use try/except only when necessary:
|
|
|
|
```python
|
|
@router.post("")
|
|
async def create_user(app_id: str, user: schemas.UserCreate, db: AsyncSession = db):
|
|
try:
|
|
return await crud.create_user(db, app_id, user)
|
|
except ConflictException:
|
|
# Example of capturing a specific exception for custom handling
|
|
logger.warning(f"Conflict creating user with name: {user.name}")
|
|
raise # Re-raise to be handled by global handler
|
|
```
|
|
|
|
### Background Tasks and Queue Processing
|
|
|
|
Use structured logging and context in background tasks:
|
|
|
|
```python
|
|
try:
|
|
await process_item(db, payload)
|
|
logger.info(f"Successfully processed message {message_id}")
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Error processing message {message_id}: {str(e)}",
|
|
exc_info=True
|
|
)
|
|
if os.getenv("SENTRY_ENABLED", "False").lower() == "true":
|
|
sentry_sdk.capture_exception(e)
|
|
```
|
|
|
|
## Logging Best Practices
|
|
|
|
1. **Use the standard logging module**:
|
|
```python
|
|
logger = logging.getLogger(__name__)
|
|
```
|
|
|
|
2. **Include context in logs**:
|
|
```python
|
|
logger.error(
|
|
f"Failed to process message for app {app_id}, user {user_id}",
|
|
exc_info=True
|
|
)
|
|
```
|
|
|
|
3. **Log levels**:
|
|
- `DEBUG`: Detailed information, useful for debugging
|
|
- `INFO`: Confirmation of normal operation
|
|
- `WARNING`: Unexpected situation that doesn't affect operation
|
|
- `ERROR`: Problem preventing normal operation
|
|
- `CRITICAL`: Application-wide failure
|
|
|
|
4. **Avoid print statements** - use proper logging instead.
|
|
|
|
## Error Reporting with Sentry
|
|
|
|
For production monitoring, we use Sentry:
|
|
|
|
```python
|
|
if os.getenv("SENTRY_ENABLED", "False").lower() == "true":
|
|
sentry_sdk.capture_exception(e)
|
|
```
|
|
|
|
## Examples
|
|
|
|
### CRUD Function
|
|
|
|
```python
|
|
async def update_document(db, collection_id, document_id, document):
|
|
"""
|
|
Update a document.
|
|
|
|
Args:
|
|
db: Database session
|
|
collection_id: ID of the collection
|
|
document_id: ID of the document
|
|
document: Document update schema
|
|
|
|
Returns:
|
|
The updated document
|
|
|
|
Raises:
|
|
ResourceNotFoundException: If the document or collection does not exist
|
|
ValidationException: If the document data is invalid
|
|
"""
|
|
try:
|
|
# Get document (raises ResourceNotFoundException if not found)
|
|
honcho_document = await get_document(db, collection_id, document_id)
|
|
|
|
# Update document data
|
|
if document.content is not None:
|
|
honcho_document.content = document.content
|
|
if document.metadata is not None:
|
|
honcho_document.h_metadata = document.metadata
|
|
|
|
await db.commit()
|
|
logger.info(f"Document {document_id} updated successfully")
|
|
return honcho_document
|
|
except IntegrityError as e:
|
|
await db.rollback()
|
|
logger.error(f"IntegrityError updating document {document_id}: {str(e)}")
|
|
raise ValidationException("Document update failed - constraint violation") from e
|
|
```
|
|
|
|
### Router Endpoint
|
|
|
|
```python
|
|
@router.get("/{document_id}")
|
|
async def get_document(
|
|
app_id: str,
|
|
user_id: str,
|
|
collection_id: str,
|
|
document_id: str,
|
|
db: AsyncSession = db
|
|
):
|
|
"""Get a document by ID"""
|
|
# ResourceNotFoundException will be caught by global handler
|
|
return await crud.get_document(db, collection_id, document_id)
|
|
```
|
|
|
|
### Background Processing
|
|
|
|
```python
|
|
async def process_item(db, payload):
|
|
"""Process a queue item"""
|
|
try:
|
|
# Validate required fields
|
|
required_fields = ["content", "app_id", "user_id", "session_id", "message_id"]
|
|
for field in required_fields:
|
|
if field not in payload:
|
|
logger.error(f"Missing required field in payload: {field}")
|
|
raise ValidationException(f"Missing field: {field}")
|
|
|
|
# Process the item
|
|
await do_processing(db, payload)
|
|
logger.info(f"Processed message {payload['message_id']}")
|
|
except Exception as e:
|
|
logger.error(f"Error processing message: {str(e)}", exc_info=True)
|
|
if os.getenv("SENTRY_ENABLED", "False").lower() == "true":
|
|
sentry_sdk.capture_exception(e)
|
|
raise
|
|
``` |