Files
orion/docs/development/exception-handling.md
Samir Boulahtit e9253fbd84 refactor: rename Wizamart to Orion across entire codebase
Replace all ~1,086 occurrences of Wizamart/wizamart/WIZAMART/WizaMart
with Orion/orion/ORION across 184 files. This includes database
identifiers, email addresses, domain references, R2 bucket names,
DNS prefixes, encryption salt, Celery app name, config defaults,
Docker configs, CI configs, documentation, seed data, and templates.

Renames homepage-wizamart.html template to homepage-orion.html.
Fixes duplicate file_pattern key in api.yaml architecture rule.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 16:46:56 +01:00

410 lines
13 KiB
Markdown

# Exception Handling Guide
## Overview
The Orion API uses a unified custom exception system to provide consistent, meaningful error responses across all endpoints. This system was redesigned to eliminate competing exception handlers and provide a single source of truth for error handling.
## Architecture
### Exception Hierarchy
```
OrionException (Base)
├── ValidationException (422)
├── AuthenticationException (401)
├── AuthorizationException (403)
├── ResourceNotFoundException (404)
├── ConflictException (409)
├── BusinessLogicException (400)
├── RateLimitException (429)
└── ExternalServiceException (502)
```
### File Structure
```
app/exceptions/
├── __init__.py # All exception exports
├── base.py # Base exception classes
├── handler.py # Unified FastAPI exception handlers
├── auth.py # Authentication/authorization exceptions
├── admin.py # Admin operation exceptions
├── marketplace.py # Import/marketplace exceptions
├── product.py # MarketplaceProduct management exceptions
├── shop.py # Shop management exceptions
└── inventory.py # Inventory management exceptions
```
## Core Concepts
### Unified Exception Handling
The system uses **only FastAPI's built-in exception handlers** - no middleware or competing systems. All exceptions are processed through a single, consistent pipeline in `handler.py`.
### Exception Response Format
All custom exceptions return a consistent JSON structure:
```json
{
"error_code": "PRODUCT_NOT_FOUND",
"message": "MarketplaceProduct with ID 'ABC123' not found",
"status_code": 404,
"details": {
"resource_type": "MarketplaceProduct",
"identifier": "ABC123"
}
}
```
### HTTP Status Codes
The system uses appropriate HTTP status codes:
- **400**: Business logic violations (cannot modify own admin account)
- **401**: Authentication failures (invalid credentials, expired tokens)
- **403**: Authorization failures (admin privileges required)
- **404**: Resource not found (user, shop, product not found)
- **409**: Resource conflicts (duplicate shop codes, existing products)
- **422**: Validation errors (Pydantic validation failures)
- **429**: Rate limiting (too many requests)
- **500**: Internal server errors (unexpected exceptions)
## Implementation Details
### Exception Handler Setup
The main application registers all exception handlers in a single function:
```python
# main.py
from app.exceptions.handler import setup_exception_handlers
app = FastAPI(...)
setup_exception_handlers(app) # Single registration point
```
### Handler Types
The system handles four categories of exceptions:
```python
# app/exceptions/handler.py
@app.exception_handler(OrionException)
async def custom_exception_handler(request, exc):
"""Handle all custom business exceptions"""
return JSONResponse(
status_code=exc.status_code,
content=exc.to_dict()
)
@app.exception_handler(HTTPException)
async def http_exception_handler(request, exc):
"""Handle FastAPI HTTP exceptions"""
return JSONResponse(
status_code=exc.status_code,
content={
"error_code": f"HTTP_{exc.status_code}",
"message": exc.detail,
"status_code": exc.status_code,
}
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
"""Handle Pydantic validation errors"""
return JSONResponse(
status_code=422,
content={
"error_code": "VALIDATION_ERROR",
"message": "Request validation failed",
"status_code": 422,
"details": {"validation_errors": exc.errors()}
}
)
@app.exception_handler(Exception)
async def generic_exception_handler(request, exc):
"""Handle unexpected exceptions"""
return JSONResponse(
status_code=500,
content={
"error_code": "INTERNAL_SERVER_ERROR",
"message": "Internal server error",
"status_code": 500,
}
)
```
## Working with Exceptions
### Service Layer Pattern
Services raise custom exceptions with specific error codes:
```python
# app/services/admin_service.py
from app.exceptions.admin import UserNotFoundException, CannotModifySelfException
def toggle_user_status(self, db: Session, user_id: int, current_admin_id: int):
user = self._get_user_by_id_or_raise(db, user_id)
# Prevent self-modification
if user.id == current_admin_id:
raise CannotModifySelfException(user_id, "deactivate account")
# Prevent admin-to-admin modification
if user.role == "admin" and user.id != current_admin_id:
raise UserStatusChangeException(
user_id=user_id,
current_status="admin",
attempted_action="toggle status",
reason="Cannot modify another admin user"
)
```
### Authentication Integration
Authentication dependencies use custom exceptions instead of HTTPException:
```python
# middleware/auth.py
from app.exceptions.auth import AdminRequiredException, TokenExpiredException
def require_admin(self, current_user: User):
if current_user.role != "admin":
raise AdminRequiredException() # Returns error_code: "ADMIN_REQUIRED"
return current_user
def verify_token(self, token: str):
try:
payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
# ... token verification logic
except jwt.ExpiredSignatureError:
raise TokenExpiredException() # Returns error_code: "TOKEN_EXPIRED"
```
### Endpoint Implementation
Endpoints rely on service layer exceptions and don't handle HTTPException:
```python
# app/api/v1/admin.py
@router.put("/admin/users/{user_id}/status")
def toggle_user_status(
user_id: int,
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api), # May raise AdminRequiredException
):
# Service raises UserNotFoundException, CannotModifySelfException, etc.
user, message = admin_service.toggle_user_status(db, user_id, current_admin.id)
return {"message": message}
```
## Creating New Exceptions
### Step 1: Define Domain-Specific Exceptions
Create exceptions in the appropriate module:
```python
# app/exceptions/payment.py
from .base import BusinessLogicException, ResourceNotFoundException
class PaymentNotFoundException(ResourceNotFoundException):
"""Raised when payment record is not found."""
def __init__(self, payment_id: str):
super().__init__(
resource_type="Payment",
identifier=payment_id,
message=f"Payment with ID '{payment_id}' not found",
error_code="PAYMENT_NOT_FOUND",
)
class InsufficientFundsException(BusinessLogicException):
"""Raised when account has insufficient funds."""
def __init__(self, required: float, available: float, account_id: str):
super().__init__(
message=f"Insufficient funds. Required: {required}, Available: {available}",
error_code="INSUFFICIENT_FUNDS",
details={
"required_amount": required,
"available_amount": available,
"account_id": account_id,
},
)
```
### Step 2: Export in __init__.py
```python
# app/exceptions/__init__.py
from .payment import PaymentNotFoundException, InsufficientFundsException
__all__ = [
# ... existing exports
"PaymentNotFoundException",
"InsufficientFundsException",
]
```
### Step 3: Use in Services
```python
# app/services/payment_service.py
from app.exceptions import PaymentNotFoundException, InsufficientFundsException
def process_payment(self, db: Session, payment_id: str, amount: float):
payment = self._get_payment_by_id_or_raise(db, payment_id)
if payment.account_balance < amount:
raise InsufficientFundsException(amount, payment.account_balance, payment.account_id)
# Process payment...
return payment
```
## Testing Exception Handling
### Unit Tests
Test exception properties and inheritance:
```python
# tests/unit/exceptions/test_admin_exceptions.py
import pytest
from app.exceptions.admin import UserNotFoundException, CannotModifySelfException
def test_user_not_found_exception():
exc = UserNotFoundException("123")
assert exc.error_code == "USER_NOT_FOUND"
assert exc.status_code == 404
assert "123" in exc.message
assert exc.details["identifier"] == "123"
def test_cannot_modify_self_exception():
exc = CannotModifySelfException(456, "deactivate account")
assert exc.error_code == "CANNOT_MODIFY_SELF"
assert exc.status_code == 400
assert "deactivate account" in exc.message
```
### Integration Tests
Test complete error handling flow:
```python
# tests/integration/api/v1/test_admin_endpoints.py
def test_toggle_user_status_cannot_modify_self(client, admin_headers, test_admin):
"""Test that admin cannot modify their own account"""
response = client.put(
f"/api/v1/admin/users/{test_admin.id}/status",
headers=admin_headers
)
assert response.status_code == 400
data = response.json()
assert data["error_code"] == "CANNOT_MODIFY_SELF"
assert "Cannot perform 'deactivate account' on your own account" in data["message"]
def test_get_all_users_non_admin(client, auth_headers):
"""Test non-admin trying to access admin endpoint"""
response = client.get("/api/v1/admin/users", headers=auth_headers)
assert response.status_code == 403
data = response.json()
assert data["error_code"] == "ADMIN_REQUIRED" # Custom exception, not HTTP_403
assert "Admin privileges required" in data["message"]
```
## Migration from Legacy System
### What Was Changed
1. **Removed competing handlers**: Eliminated `ExceptionHandlerMiddleware` and duplicate exception handling
2. **Unified response format**: All exceptions now return consistent JSON structure
3. **Enhanced authentication**: Replaced `HTTPException` in auth layer with custom exceptions
4. **Improved logging**: Added structured logging with request context
### Breaking Changes
- Authentication failures now return `ADMIN_REQUIRED` instead of `HTTP_403`
- Rate limiting returns structured error with `RATE_LIMIT_EXCEEDED` code
- All custom exceptions use 400 for business logic errors (not 422)
### Migration Steps
1. **Update exception imports**: Replace `HTTPException` imports with custom exceptions
2. **Fix test expectations**: Update tests to expect specific error codes (e.g., `ADMIN_REQUIRED`)
3. **Remove legacy handlers**: Delete any remaining middleware or duplicate handlers
4. **Update frontend**: Map new error codes to user-friendly messages
## Error Code Reference
### Authentication (401)
- `INVALID_CREDENTIALS`: Invalid username or password
- `TOKEN_EXPIRED`: JWT token has expired
- `INVALID_TOKEN`: Malformed or invalid token
- `USER_NOT_ACTIVE`: User account is deactivated
### Authorization (403)
- `ADMIN_REQUIRED`: Admin privileges required for operation
- `INSUFFICIENT_PERMISSIONS`: User lacks required permissions
- `UNAUTHORIZED_SHOP_ACCESS`: Cannot access shop (not owner)
### Resource Not Found (404)
- `USER_NOT_FOUND`: User with specified ID not found
- `SHOP_NOT_FOUND`: Shop with specified code/ID not found
- `PRODUCT_NOT_FOUND`: MarketplaceProduct with specified ID not found
### Business Logic (400)
- `CANNOT_MODIFY_SELF`: Admin cannot modify own account
- `USER_STATUS_CHANGE_FAILED`: Cannot change user status
- `SHOP_VERIFICATION_FAILED`: Shop verification operation failed
- `ADMIN_OPERATION_FAILED`: Generic admin operation failure
### Validation (422)
- `VALIDATION_ERROR`: Pydantic request validation failed
- `INVALID_PRODUCT_DATA`: MarketplaceProduct data validation failed
- `INVALID_SHOP_DATA`: Shop data validation failed
### Rate Limiting (429)
- `RATE_LIMIT_EXCEEDED`: API rate limit exceeded
### Service Errors (500/502)
- `INTERNAL_SERVER_ERROR`: Unexpected server error
- `EXTERNAL_SERVICE_ERROR`: Third-party service failure
## Best Practices
### Exception Design
- Use specific error codes for programmatic handling
- Include relevant context in exception details
- Inherit from appropriate base exception class
- Provide clear, actionable error messages
### Service Layer
- Raise custom exceptions, never HTTPException
- Include identifiers and context in exception details
- Use private helper methods with `_or_raise` suffix
- Log errors before raising exceptions
### Testing
- Test both exception raising and response format
- Verify error codes, status codes, and message content
- Test edge cases and error conditions
- Use custom exceptions in test assertions
### Frontend Integration
- Map error codes to user-friendly messages
- Handle field-specific validation errors
- Implement retry logic for transient errors
- Log errors for debugging and monitoring
This unified exception system provides consistent error handling across your entire application while maintaining clean separation of concerns and excellent developer experience.