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>
293 lines
8.0 KiB
Markdown
293 lines
8.0 KiB
Markdown
# Error Handling
|
|
|
|
Comprehensive error handling system for the FastAPI multi-tenant e-commerce platform.
|
|
|
|
## Overview
|
|
|
|
The application uses a structured exception hierarchy with custom exception classes and centralized error handlers. All exceptions are logged, formatted consistently, and return appropriate HTTP status codes.
|
|
|
|
## Exception Hierarchy
|
|
|
|
### Base Exceptions
|
|
|
|
All custom exceptions inherit from base exception classes defined in `app.exceptions`:
|
|
|
|
```python
|
|
from app.exceptions import (
|
|
InvalidTokenException,
|
|
TokenExpiredException,
|
|
InvalidCredentialsException,
|
|
UserNotActiveException,
|
|
AdminRequiredException,
|
|
InsufficientPermissionsException,
|
|
RateLimitException
|
|
)
|
|
```
|
|
|
|
### Authentication Exceptions
|
|
|
|
| Exception | Status Code | Description |
|
|
|-----------|-------------|-------------|
|
|
| `InvalidTokenException` | 401 | JWT token is invalid, malformed, or missing required claims |
|
|
| `TokenExpiredException` | 401 | JWT token has expired |
|
|
| `InvalidCredentialsException` | 401 | Username/password authentication failed |
|
|
| `UserNotActiveException` | 403 | User account is inactive or disabled |
|
|
|
|
### Authorization Exceptions
|
|
|
|
| Exception | Status Code | Description |
|
|
|-----------|-------------|-------------|
|
|
| `AdminRequiredException` | 403 | Endpoint requires admin role |
|
|
| `InsufficientPermissionsException` | 403 | User lacks required permissions |
|
|
|
|
### Rate Limiting Exceptions
|
|
|
|
| Exception | Status Code | Description |
|
|
|-----------|-------------|-------------|
|
|
| `RateLimitException` | 429 | Too many requests, rate limit exceeded |
|
|
|
|
### Shopping Cart Exceptions
|
|
|
|
| Exception | Status Code | Description |
|
|
|-----------|-------------|-------------|
|
|
| `CartItemNotFoundException` | 404 | Cart item not found (product not in cart) |
|
|
| `InsufficientInventoryForCartException` | 400 | Product doesn't have enough inventory for cart operation |
|
|
| `InvalidCartQuantityException` | 422 | Cart quantity is invalid (e.g., less than min or greater than max) |
|
|
| `CartValidationException` | 422 | Cart data validation failed |
|
|
| `EmptyCartException` | 422 | Operation attempted on empty cart |
|
|
| `ProductNotAvailableForCartException` | 400 | Product is not available for adding to cart |
|
|
|
|
### Product Exceptions
|
|
|
|
| Exception | Status Code | Description |
|
|
|-----------|-------------|-------------|
|
|
| `ProductNotFoundException` | 404 | Product not found in store catalog |
|
|
| `ProductNotActiveException` | 400 | Product is inactive and cannot be purchased |
|
|
|
|
### Inventory Exceptions
|
|
|
|
| Exception | Status Code | Description |
|
|
|-----------|-------------|-------------|
|
|
| `InventoryNotFoundException` | 404 | Inventory record not found |
|
|
| `InsufficientInventoryException` | 400 | Not enough inventory for operation |
|
|
|
|
## Error Response Format
|
|
|
|
All custom exceptions (inheriting from `OrionException`) return a structured JSON format:
|
|
|
|
```json
|
|
{
|
|
"error_code": "PRODUCT_NOT_FOUND",
|
|
"message": "Product with ID '123' not found in store 1 catalog",
|
|
"status_code": 404,
|
|
"details": {
|
|
"resource_type": "Product",
|
|
"identifier": "123",
|
|
"product_id": 123,
|
|
"store_id": 1
|
|
}
|
|
}
|
|
```
|
|
|
|
**Standard Fields:**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `error_code` | string | Machine-readable error code (e.g., "CART_ITEM_NOT_FOUND") |
|
|
| `message` | string | Human-readable error message |
|
|
| `status_code` | integer | HTTP status code |
|
|
| `details` | object | Additional context-specific error details |
|
|
|
|
**Note:** Generic FastAPI/HTTP errors may still use the simpler format:
|
|
|
|
```json
|
|
{
|
|
"detail": "Error message",
|
|
"status_code": 401
|
|
}
|
|
```
|
|
|
|
### Rate Limit Error Response
|
|
|
|
Rate limit errors include additional information:
|
|
|
|
```json
|
|
{
|
|
"detail": "Rate limit exceeded",
|
|
"status_code": 429,
|
|
"retry_after": 3600,
|
|
"timestamp": "2024-11-16T13:00:00Z",
|
|
"path": "/api/v1/resource"
|
|
}
|
|
```
|
|
|
|
## Usage Examples
|
|
|
|
### Raising Exceptions in Code
|
|
|
|
```python
|
|
from app.exceptions import InvalidCredentialsException, AdminRequiredException
|
|
|
|
# Authentication failure
|
|
if not user:
|
|
raise InvalidCredentialsException("User not found")
|
|
|
|
# Authorization check
|
|
if user.role != "admin":
|
|
raise AdminRequiredException()
|
|
```
|
|
|
|
### Catching Exceptions in Routes
|
|
|
|
```python
|
|
from fastapi import HTTPException
|
|
from app.exceptions import InvalidTokenException
|
|
|
|
@app.post("/api/v1/auth/protected")
|
|
async def protected_endpoint(token: str):
|
|
try:
|
|
user_data = auth_manager.verify_token(token)
|
|
return {"user": user_data}
|
|
except InvalidTokenException as e:
|
|
# Exception will be caught by global handler
|
|
raise
|
|
except Exception as e:
|
|
# Unexpected errors
|
|
logger.error(f"Unexpected error: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
```
|
|
|
|
## Context-Aware Error Handling
|
|
|
|
The error handling system is context-aware and provides different error formats based on the request context:
|
|
|
|
### API Requests (`/api/*`)
|
|
Returns JSON error responses suitable for API clients.
|
|
|
|
### Admin/Store Dashboard (`/admin/*`, `/store/*`)
|
|
Returns JSON errors or redirects to error pages based on accept headers.
|
|
|
|
### Shop Requests (`/shop/*`)
|
|
Returns themed error pages matching the store's shop design.
|
|
|
|
## Logging
|
|
|
|
All errors are automatically logged with the following information:
|
|
- Error type and message
|
|
- Request path and method
|
|
- User information (if authenticated)
|
|
- Stack trace (for unexpected errors)
|
|
- Timestamp
|
|
|
|
Example log output:
|
|
```
|
|
2024-11-16 13:00:00 [ERROR] middleware.auth: Token verification error: Token missing user identifier
|
|
2024-11-16 13:00:00 [ERROR] app.main: Request failed: POST /api/v1/auth/login - 401
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### 1. Use Specific Exceptions
|
|
Always use the most specific exception class available:
|
|
|
|
```python
|
|
# Good
|
|
raise InvalidCredentialsException("Invalid email or password")
|
|
|
|
# Avoid
|
|
raise HTTPException(status_code=401, detail="Invalid credentials")
|
|
```
|
|
|
|
### 2. Provide Meaningful Messages
|
|
Include context in error messages:
|
|
|
|
```python
|
|
# Good
|
|
raise InvalidTokenException("Token missing user identifier")
|
|
|
|
# Avoid
|
|
raise InvalidTokenException("Invalid token")
|
|
```
|
|
|
|
### 3. Don't Expose Sensitive Information
|
|
Never include sensitive data in error messages:
|
|
|
|
```python
|
|
# Good
|
|
raise InvalidCredentialsException("Invalid email or password")
|
|
|
|
# Avoid - reveals which field is wrong
|
|
raise InvalidCredentialsException(f"User {email} not found")
|
|
```
|
|
|
|
### 4. Log Before Raising
|
|
Log errors before raising them for debugging:
|
|
|
|
```python
|
|
try:
|
|
result = risky_operation()
|
|
except OperationFailed as e:
|
|
logger.error(f"Operation failed: {e}", exc_info=True)
|
|
raise InternalServerException("Operation failed")
|
|
```
|
|
|
|
## Testing Error Handling
|
|
|
|
### Unit Tests
|
|
|
|
```python
|
|
import pytest
|
|
from app.exceptions import InvalidTokenException
|
|
|
|
def test_invalid_token():
|
|
auth_manager = AuthManager()
|
|
|
|
with pytest.raises(InvalidTokenException) as exc_info:
|
|
auth_manager.verify_token("invalid-token")
|
|
|
|
assert "Could not validate credentials" in str(exc_info.value.message)
|
|
```
|
|
|
|
### Integration Tests
|
|
|
|
```python
|
|
def test_authentication_error_response(client):
|
|
response = client.post(
|
|
"/api/v1/auth/login",
|
|
json={"username": "wrong", "password": "wrong"}
|
|
)
|
|
|
|
assert response.status_code == 401
|
|
assert "detail" in response.json()
|
|
```
|
|
|
|
## Global Exception Handlers
|
|
|
|
The application registers global exception handlers in `main.py`:
|
|
|
|
```python
|
|
from fastapi import FastAPI
|
|
from app.exceptions import InvalidTokenException, RateLimitException
|
|
|
|
app = FastAPI()
|
|
|
|
@app.exception_handler(InvalidTokenException)
|
|
async def invalid_token_handler(request, exc):
|
|
return JSONResponse(
|
|
status_code=401,
|
|
content={
|
|
"detail": exc.message,
|
|
"status_code": 401,
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"path": str(request.url.path)
|
|
}
|
|
)
|
|
```
|
|
|
|
## Related Documentation
|
|
|
|
- [Authentication](authentication.md) - Authentication-related exceptions
|
|
- [RBAC](rbac.md) - Authorization and permission exceptions
|
|
- [Rate Limiting](rate-limiting.md) - Rate limit error handling
|
|
- [Testing Guide](../testing/testing-guide.md) - Testing error scenarios
|