Some checks failed
- Add Development URL Quick Reference section to url-routing overview with all login URLs, entry points, and full examples - Replace /shop/ path segments with /storefront/ across 50 docs files - Update file references: shop_pages.py → storefront_pages.py, templates/shop/ → templates/storefront/, api/v1/shop/ → api/v1/storefront/ - Preserve domain references (orion.shop) and /store/ staff dashboard paths - Archive docs left unchanged (historical) 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.
|
|
|
|
### Storefront Requests (`/storefront/*`)
|
|
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
|