Clean up accumulated backward-compat shims, deprecated wrappers, unused aliases, and legacy code across the codebase. Since the platform is not live yet, this establishes a clean baseline. Changes: - Delete deprecated middleware/context.py (RequestContext, get_request_context) - Remove unused factory get_store_email_settings_service() - Remove deprecated pagination_full macro, /admin/platform-homepage route - Remove ConversationResponse, InvoiceSettings* unprefixed aliases - Simplify celery_config.py (remove empty LEGACY_TASK_MODULES) - Standardize billing exceptions: *Error aliases → *Exception names - Consolidate duplicate TierNotFoundError/FeatureNotFoundError classes - Remove deprecated is_admin_request() from Store/PlatformContextManager - Remove is_platform_default field, MediaUploadResponse legacy flat fields - Remove MediaItemResponse.url alias, update JS to use file_url - Update all affected tests and documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1417 lines
36 KiB
Markdown
1417 lines
36 KiB
Markdown
# Error Handling System - Developer Documentation
|
|
|
|
**Version:** 1.0.0
|
|
**Last Updated:** 2025
|
|
**Status:** Phase 1 Complete (Admin), Phase 2-3 Pending (Store, Shop)
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Overview](#overview)
|
|
2. [Architecture](#architecture)
|
|
3. [System Components](#system-components)
|
|
4. [Context Detection](#context-detection)
|
|
5. [Error Response Types](#error-response-types)
|
|
6. [Template System](#template-system)
|
|
7. [Implementation Status](#implementation-status)
|
|
8. [Developer Guidelines](#developer-guidelines)
|
|
9. [Adding New Error Pages](#adding-new-error-pages)
|
|
10. [Testing Guide](#testing-guide)
|
|
11. [Troubleshooting](#troubleshooting)
|
|
12. [API Reference](#api-reference)
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
### Purpose
|
|
|
|
The error handling system provides context-aware error responses throughout the application. It automatically determines whether to return JSON (for API calls) or HTML error pages (for browser requests), and renders appropriate error pages based on the request context (Admin, Store Dashboard, or Shop).
|
|
|
|
### Key Features
|
|
|
|
- **Context-Aware**: Different error pages for Admin, Store, and Shop areas
|
|
- **Automatic Detection**: Distinguishes between API and HTML page requests
|
|
- **Consistent JSON API**: API endpoints always return standardized JSON errors
|
|
- **Fallback Mechanism**: Gracefully handles missing templates
|
|
- **Debug Mode**: Shows technical details to admin users only
|
|
- **Theme Integration**: Shop error pages support store theming (Phase 3)
|
|
- **Security**: 401 errors automatically redirect to appropriate login pages
|
|
|
|
### Design Principles
|
|
|
|
1. **Separation of Concerns**: HTML templates are separate from exception handler logic
|
|
2. **Fail-Safe**: Multiple fallback levels ensure errors always render
|
|
3. **Developer-Friendly**: Easy to add new error pages or contexts
|
|
4. **User-Centric**: Professional error pages with clear messaging and actions
|
|
5. **Secure**: Technical details hidden from non-admin users
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
### Request Flow
|
|
|
|
```
|
|
HTTP Request
|
|
↓
|
|
Store Context Middleware (detects store from domain/subdomain/path)
|
|
↓
|
|
Context Detection Middleware (detects API/Admin/Store/Shop)
|
|
↓
|
|
Route Handler (processes request, may throw exception)
|
|
↓
|
|
Exception Handler (catches and processes exception)
|
|
↓
|
|
├─→ API Request? → Return JSON Response
|
|
├─→ HTML Request + 401? → Redirect to Login
|
|
└─→ HTML Request? → Render Context-Aware Error Page
|
|
```
|
|
|
|
### Components
|
|
|
|
```
|
|
middleware/
|
|
├── store_context.py # Detects store from URL (existing)
|
|
└── context_middleware.py # Detects request context type (NEW)
|
|
|
|
app/exceptions/
|
|
├── handler.py # Exception handlers (refactored)
|
|
├── error_renderer.py # Error page rendering logic (NEW)
|
|
└── base.py # Base exception classes (existing)
|
|
|
|
app/templates/
|
|
├── admin/errors/ # Admin error pages (Phase 1 - COMPLETE)
|
|
├── store/errors/ # Store error pages (Phase 2 - PENDING)
|
|
├── shop/errors/ # Shop error pages (Phase 3 - PENDING)
|
|
└── shared/ # Shared fallback error pages (COMPLETE)
|
|
```
|
|
|
|
---
|
|
|
|
## System Components
|
|
|
|
### 1. Context Detection Middleware
|
|
|
|
**File:** `middleware/context_middleware.py`
|
|
|
|
**Purpose:** Detects the request context type and injects it into `request.state.context_type`.
|
|
|
|
**Context Types:**
|
|
|
|
```python
|
|
class RequestContext(str, Enum):
|
|
API = "api" # API endpoints (/api/*)
|
|
ADMIN = "admin" # Admin portal (/admin/* or admin.*)
|
|
STORE_DASHBOARD = "store" # Store management (/store/*)
|
|
SHOP = "shop" # Customer storefront (store subdomains)
|
|
FALLBACK = "fallback" # Unknown/generic context
|
|
```
|
|
|
|
**Detection Logic:**
|
|
|
|
1. **API** - Path starts with `/api/` (highest priority)
|
|
2. **ADMIN** - Path starts with `/admin` or host starts with `admin.`
|
|
3. **STORE_DASHBOARD** - Path starts with `/store/`
|
|
4. **SHOP** - `request.state.store` exists (set by store_context_middleware)
|
|
5. **FALLBACK** - None of the above match
|
|
|
|
**Usage:**
|
|
|
|
```python
|
|
from middleware.frontend_type import get_frontend_type
|
|
from app.modules.enums import FrontendType
|
|
|
|
def my_handler(request: Request):
|
|
frontend_type = get_frontend_type(request)
|
|
|
|
if frontend_type == FrontendType.ADMIN:
|
|
# Handle admin-specific logic
|
|
pass
|
|
```
|
|
|
|
### 2. Error Page Renderer
|
|
|
|
**File:** `app/exceptions/error_renderer.py`
|
|
|
|
**Purpose:** Renders context-aware HTML error pages using Jinja2 templates.
|
|
|
|
**Key Methods:**
|
|
|
|
```python
|
|
ErrorPageRenderer.render_error_page(
|
|
request: Request,
|
|
status_code: int,
|
|
error_code: str,
|
|
message: str,
|
|
details: Optional[Dict[str, Any]] = None,
|
|
show_debug: bool = False,
|
|
) -> HTMLResponse
|
|
```
|
|
|
|
**Template Selection Priority:**
|
|
|
|
1. `{context}/errors/{status_code}.html` (e.g., `admin/errors/404.html`)
|
|
2. `{context}/errors/generic.html` (e.g., `admin/errors/generic.html`)
|
|
3. `shared/{status_code}-fallback.html` (e.g., `shared/404-fallback.html`)
|
|
4. `shared/generic-fallback.html` (absolute fallback)
|
|
|
|
**Template Variables Provided:**
|
|
|
|
```python
|
|
{
|
|
"status_code": int, # HTTP status code
|
|
"status_name": str, # Friendly name ("Not Found")
|
|
"error_code": str, # Application error code
|
|
"message": str, # User-friendly message
|
|
"details": dict, # Additional error details
|
|
"show_debug": bool, # Whether to show debug info
|
|
"context_type": str, # Request context type
|
|
"path": str, # Request path
|
|
"store": dict, # Store info (shop context only)
|
|
"theme": dict, # Theme data (shop context only)
|
|
}
|
|
```
|
|
|
|
### 3. Exception Handler
|
|
|
|
**File:** `app/exceptions/handler.py`
|
|
|
|
**Purpose:** Central exception handling for all application exceptions.
|
|
|
|
**Handlers:**
|
|
|
|
- `WizamartException` - Custom application exceptions
|
|
- `HTTPException` - FastAPI HTTP exceptions
|
|
- `RequestValidationError` - Pydantic validation errors
|
|
- `Exception` - Generic Python exceptions
|
|
- `404` - Not Found errors
|
|
|
|
**Response Logic:**
|
|
|
|
```python
|
|
if request.url.path.startswith("/api/"):
|
|
return JSONResponse(...) # Always JSON for API
|
|
elif _is_html_page_request(request):
|
|
if status_code == 401:
|
|
return RedirectResponse(...) # Redirect to login
|
|
else:
|
|
return ErrorPageRenderer.render_error_page(...) # HTML error page
|
|
else:
|
|
return JSONResponse(...) # Default to JSON
|
|
```
|
|
|
|
---
|
|
|
|
## Context Detection
|
|
|
|
### How Context is Determined
|
|
|
|
The system uses a priority-based approach to detect context:
|
|
|
|
```python
|
|
# Priority 1: API Context
|
|
if path.startswith("/api/"):
|
|
return RequestContext.API
|
|
|
|
# Priority 2: Admin Context
|
|
if path.startswith("/admin") or host.startswith("admin."):
|
|
return RequestContext.ADMIN
|
|
|
|
# Priority 3: Store Dashboard Context
|
|
if path.startswith("/store/"):
|
|
return RequestContext.STORE_DASHBOARD
|
|
|
|
# Priority 4: Shop Context
|
|
if hasattr(request.state, 'store') and request.state.store:
|
|
return RequestContext.SHOP
|
|
|
|
# Priority 5: Fallback
|
|
return RequestContext.FALLBACK
|
|
```
|
|
|
|
### Context Examples
|
|
|
|
| URL | Host | Context |
|
|
|-----|------|---------|
|
|
| `/api/v1/admin/stores` | any | API |
|
|
| `/admin/dashboard` | any | ADMIN |
|
|
| `/store/products` | any | STORE_DASHBOARD |
|
|
| `/products` | `store1.platform.com` | SHOP |
|
|
| `/products` | `customdomain.com` | SHOP (if store detected) |
|
|
| `/about` | `platform.com` | FALLBACK |
|
|
|
|
### Special Cases
|
|
|
|
**Admin Access via Subdomain:**
|
|
```
|
|
admin.platform.com/dashboard → ADMIN context
|
|
```
|
|
|
|
**Store Dashboard Access:**
|
|
```
|
|
/store/store1/dashboard → STORE_DASHBOARD context
|
|
store1.platform.com/store/dashboard → STORE_DASHBOARD context
|
|
```
|
|
|
|
**Shop Access:**
|
|
```
|
|
store1.platform.com/ → SHOP context
|
|
customdomain.com/ → SHOP context (if store verified)
|
|
```
|
|
|
|
---
|
|
|
|
## Error Response Types
|
|
|
|
### 1. JSON Responses (API Context)
|
|
|
|
**When:** Request path starts with `/api/`
|
|
|
|
**Format:**
|
|
```json
|
|
{
|
|
"error_code": "STORE_NOT_FOUND",
|
|
"message": "Store with ID '999' not found",
|
|
"status_code": 404,
|
|
"details": {
|
|
"store_id": "999"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Always JSON, Regardless of Accept Header:**
|
|
API endpoints MUST always return JSON, even if the client sends `Accept: text/html`.
|
|
|
|
### 2. HTML Error Pages (HTML Page Requests)
|
|
|
|
**When:**
|
|
- NOT an API request
|
|
- GET request
|
|
- Accept header includes `text/html`
|
|
- NOT already on login page
|
|
|
|
**Renders:** Context-appropriate HTML error page
|
|
|
|
**Example:** Admin 404
|
|
```html
|
|
<html>
|
|
<head><title>404 - Page Not Found | Admin Portal</title></head>
|
|
<body>
|
|
<div class="error-container">
|
|
<h1>404</h1>
|
|
<p>The admin page you're looking for doesn't exist.</p>
|
|
<a href="/admin/dashboard">Go to Dashboard</a>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
### 3. Login Redirects (401 Unauthorized)
|
|
|
|
**When:**
|
|
- HTML page request
|
|
- 401 status code
|
|
|
|
**Behavior:** Redirect to appropriate login page based on context
|
|
|
|
| Context | Redirect To |
|
|
|---------|-------------|
|
|
| ADMIN | `/admin/login` |
|
|
| STORE_DASHBOARD | `/store/login` |
|
|
| SHOP | `/shop/login` |
|
|
| FALLBACK | `/admin/login` |
|
|
|
|
---
|
|
|
|
## Template System
|
|
|
|
### Template Structure
|
|
|
|
All error templates follow this organization:
|
|
|
|
```
|
|
app/templates/
|
|
├── admin/
|
|
│ └── errors/
|
|
│ ├── base.html # Base template (extends nothing)
|
|
│ ├── 400.html # Bad Request
|
|
│ ├── 401.html # Unauthorized
|
|
│ ├── 403.html # Forbidden
|
|
│ ├── 404.html # Not Found
|
|
│ ├── 422.html # Validation Error
|
|
│ ├── 429.html # Rate Limit
|
|
│ ├── 500.html # Internal Server Error
|
|
│ ├── 502.html # Bad Gateway
|
|
│ └── generic.html # Catch-all
|
|
│
|
|
├── store/
|
|
│ └── errors/
|
|
│ └── (same structure as admin)
|
|
│
|
|
├── shop/
|
|
│ └── errors/
|
|
│ └── (same structure as admin)
|
|
│
|
|
└── shared/
|
|
├── 404-fallback.html
|
|
├── 500-fallback.html
|
|
├── generic-fallback.html
|
|
└── cdn-fallback.html
|
|
```
|
|
|
|
### Base Template Pattern
|
|
|
|
Each context has its own `base.html` template:
|
|
|
|
```html
|
|
<!-- app/templates/admin/errors/base.html -->
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<title>{% block title %}{{ status_code }} - {{ status_name }}{% endblock %}</title>
|
|
<style>
|
|
/* Admin-specific styling */
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="error-container">
|
|
{% block content %}
|
|
<!-- Default error content -->
|
|
{% endblock %}
|
|
</div>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
### Specific Error Template Pattern
|
|
|
|
```html
|
|
<!-- app/templates/admin/errors/404.html -->
|
|
{% extends "admin/errors/base.html" %}
|
|
|
|
{% block icon %}🔍{% endblock %}
|
|
{% block title %}404 - Page Not Found{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="error-icon">🔍</div>
|
|
<div class="status-code">404</div>
|
|
<div class="status-name">Page Not Found</div>
|
|
<div class="error-message">
|
|
The admin page you're looking for doesn't exist or has been moved.
|
|
</div>
|
|
<div class="error-code">Error Code: {{ error_code }}</div>
|
|
|
|
<div class="action-buttons">
|
|
<a href="/admin/dashboard" class="btn btn-primary">Go to Dashboard</a>
|
|
<a href="javascript:history.back()" class="btn btn-secondary">Go Back</a>
|
|
</div>
|
|
|
|
{% if show_debug %}
|
|
<div class="debug-info">
|
|
<!-- Debug information for admins -->
|
|
</div>
|
|
{% endif %}
|
|
{% endblock %}
|
|
```
|
|
|
|
### Context-Specific Considerations
|
|
|
|
#### Admin Error Pages
|
|
|
|
**Purpose:** Error pages for platform administrators
|
|
|
|
**Characteristics:**
|
|
- Professional platform branding
|
|
- Links to admin dashboard
|
|
- Full debug information when appropriate
|
|
- Support contact link
|
|
|
|
**Action Buttons:**
|
|
- Primary: "Go to Dashboard" → `/admin/dashboard`
|
|
- Secondary: "Go Back" → `javascript:history.back()`
|
|
|
|
#### Store Error Pages
|
|
|
|
**Purpose:** Error pages for store dashboard users
|
|
|
|
**Characteristics:**
|
|
- Professional store management branding
|
|
- Links to store dashboard
|
|
- Debug information for store admins
|
|
- Store support contact link
|
|
|
|
**Action Buttons:**
|
|
- Primary: "Go to Dashboard" → `/store/dashboard`
|
|
- Secondary: "Go Back" → `javascript:history.back()`
|
|
|
|
#### Shop Error Pages
|
|
|
|
**Purpose:** Error pages for customers on store storefronts
|
|
|
|
**Characteristics:**
|
|
- **Uses store theme** (colors, logo, fonts)
|
|
- Customer-friendly language
|
|
- No technical jargon
|
|
- Links to shop homepage
|
|
- Customer support contact
|
|
|
|
**Action Buttons:**
|
|
- Primary: "Continue Shopping" → Shop homepage
|
|
- Secondary: "Contact Support" → Store support page
|
|
|
|
**Theme Integration:**
|
|
```html
|
|
<!-- Shop error pages use store theme variables -->
|
|
<style>
|
|
:root {
|
|
--color-primary: {{ theme.colors.primary }};
|
|
--color-background: {{ theme.colors.background }};
|
|
--font-body: {{ theme.fonts.body }};
|
|
}
|
|
</style>
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Status
|
|
|
|
### Phase 1: Admin Error Handling ✅ COMPLETE
|
|
|
|
**Status:** Fully implemented and ready for use
|
|
|
|
**Components:**
|
|
- ✅ Context detection middleware
|
|
- ✅ Error page renderer
|
|
- ✅ Refactored exception handler
|
|
- ✅ Admin error templates (11 files)
|
|
- ✅ Fallback templates (3 files)
|
|
|
|
**Error Codes Implemented:**
|
|
- ✅ 400 (Bad Request)
|
|
- ✅ 401 (Unauthorized)
|
|
- ✅ 403 (Forbidden)
|
|
- ✅ 404 (Not Found)
|
|
- ✅ 422 (Validation Error)
|
|
- ✅ 429 (Rate Limit)
|
|
- ✅ 500 (Internal Server Error)
|
|
- ✅ 502 (Bad Gateway)
|
|
- ✅ Generic (Catch-all)
|
|
|
|
### Phase 2: Store Error Handling ⏳ PENDING
|
|
|
|
**Status:** Not yet implemented
|
|
|
|
**Required Tasks:**
|
|
1. Create `/app/templates/store/errors/` directory
|
|
2. Copy admin templates as starting point
|
|
3. Customize messaging for store context:
|
|
- Change "Admin Portal" to "Store Portal"
|
|
- Update dashboard links to `/store/dashboard`
|
|
- Adjust support links to store support
|
|
4. Update action button destinations
|
|
5. Test all error codes in store context
|
|
|
|
**Estimated Effort:** 1-2 hours
|
|
|
|
**Priority:** Medium (stores currently see fallback pages)
|
|
|
|
### Phase 3: Shop Error Handling ⏳ PENDING
|
|
|
|
**Status:** Not yet implemented
|
|
|
|
**Required Tasks:**
|
|
1. Create `/app/templates/shop/errors/` directory
|
|
2. Create customer-facing error templates:
|
|
- Use customer-friendly language
|
|
- Integrate store theme variables
|
|
- Add store logo/branding
|
|
3. Update ErrorPageRenderer to pass theme data
|
|
4. Implement theme integration:
|
|
```python
|
|
if context_type == RequestContext.SHOP:
|
|
template_data["theme"] = request.state.theme
|
|
template_data["store"] = request.state.store
|
|
```
|
|
5. Test with multiple store themes
|
|
6. Test on custom domains
|
|
|
|
**Estimated Effort:** 2-3 hours
|
|
|
|
**Priority:** High (customers currently see non-branded fallback pages)
|
|
|
|
**Dependencies:** Requires store theme system (already exists)
|
|
|
|
---
|
|
|
|
## Developer Guidelines
|
|
|
|
### When to Throw Exceptions
|
|
|
|
**Use Custom Exceptions:**
|
|
```python
|
|
from app.exceptions import StoreNotFoundException
|
|
|
|
# Good - Specific exception
|
|
raise StoreNotFoundException(store_id="123")
|
|
|
|
# Avoid - Generic exception with less context
|
|
raise HTTPException(status_code=404, detail="Store not found")
|
|
```
|
|
|
|
**Exception Selection Guide:**
|
|
|
|
| Scenario | Exception | Status Code |
|
|
|----------|-----------|-------------|
|
|
| Resource not found | `{Resource}NotFoundException` | 404 |
|
|
| Validation failed | `ValidationException` | 422 |
|
|
| Unauthorized | `AuthenticationException` | 401 |
|
|
| Forbidden | `AuthorizationException` | 403 |
|
|
| Business rule violated | `BusinessLogicException` | 400 |
|
|
| External service failed | `ExternalServiceException` | 502 |
|
|
| Rate limit exceeded | `RateLimitException` | 429 |
|
|
|
|
### Creating Custom Exceptions
|
|
|
|
Follow this pattern:
|
|
|
|
```python
|
|
# app/exceptions/my_domain.py
|
|
from .base import ResourceNotFoundException
|
|
|
|
class MyResourceNotFoundException(ResourceNotFoundException):
|
|
"""Raised when MyResource is not found."""
|
|
|
|
def __init__(self, resource_id: str):
|
|
super().__init__(
|
|
resource_type="MyResource",
|
|
identifier=resource_id,
|
|
message=f"MyResource with ID '{resource_id}' not found",
|
|
error_code="MY_RESOURCE_NOT_FOUND",
|
|
)
|
|
```
|
|
|
|
Then register in `app/exceptions/__init__.py`:
|
|
|
|
```python
|
|
from .my_domain import MyResourceNotFoundException
|
|
|
|
__all__ = [
|
|
# ... existing exports
|
|
"MyResourceNotFoundException",
|
|
]
|
|
```
|
|
|
|
### Error Messages Best Practices
|
|
|
|
**User-Facing Messages:**
|
|
- Clear and concise
|
|
- Avoid technical jargon
|
|
- Suggest next actions
|
|
- Never expose sensitive information
|
|
|
|
```python
|
|
# Good
|
|
message="The product you're looking for is currently unavailable."
|
|
|
|
# Bad
|
|
message="SELECT * FROM products WHERE id=123 returned 0 rows"
|
|
```
|
|
|
|
**Error Codes:**
|
|
- Use UPPER_SNAKE_CASE
|
|
- Be descriptive but concise
|
|
- Follow existing patterns
|
|
|
|
```python
|
|
# Good
|
|
error_code="PRODUCT_OUT_OF_STOCK"
|
|
error_code="PAYMENT_PROCESSING_FAILED"
|
|
|
|
# Bad
|
|
error_code="error1"
|
|
error_code="ProductOutOfStockException"
|
|
```
|
|
|
|
### Details Dictionary
|
|
|
|
Use the `details` dictionary for additional context:
|
|
|
|
```python
|
|
raise StoreNotFoundException(
|
|
store_id="123"
|
|
)
|
|
# Results in:
|
|
# details = {"store_id": "123", "resource_type": "Store"}
|
|
|
|
# For validation errors:
|
|
raise ValidationException(
|
|
message="Invalid email format",
|
|
field="email",
|
|
details={
|
|
"provided_value": user_input,
|
|
"expected_format": "user@example.com"
|
|
}
|
|
)
|
|
```
|
|
|
|
**What to Include in Details:**
|
|
- ✅ Resource identifiers
|
|
- ✅ Validation error specifics
|
|
- ✅ Operation context
|
|
- ✅ Non-sensitive debugging info
|
|
- ❌ Passwords or secrets
|
|
- ❌ Internal system paths
|
|
- ❌ Database queries
|
|
- ❌ Stack traces (use show_debug instead)
|
|
|
|
---
|
|
|
|
## Adding New Error Pages
|
|
|
|
### Step 1: Identify Context
|
|
|
|
Determine which context needs the new error page:
|
|
- Admin Portal → `admin/errors/`
|
|
- Store Dashboard → `store/errors/`
|
|
- Customer Shop → `shop/errors/`
|
|
|
|
### Step 2: Choose Template Type
|
|
|
|
**Specific Error Code Template:**
|
|
- For common HTTP status codes (400, 403, 404, 500, etc.)
|
|
- File name: `{status_code}.html`
|
|
|
|
**Generic Catch-All Template:**
|
|
- For less common or custom error codes
|
|
- File name: `generic.html`
|
|
- Uses variables to display appropriate content
|
|
|
|
### Step 3: Create Template
|
|
|
|
**Option A: Extend Base Template** (Recommended)
|
|
|
|
```html
|
|
<!-- app/templates/admin/errors/503.html -->
|
|
{% extends "admin/errors/base.html" %}
|
|
|
|
{% block icon %}🔧{% endblock %}
|
|
{% block title %}503 - Service Unavailable{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="error-icon">🔧</div>
|
|
<div class="status-code">503</div>
|
|
<div class="status-name">Service Unavailable</div>
|
|
<div class="error-message">
|
|
The service is temporarily unavailable. We're working to restore it.
|
|
</div>
|
|
<div class="error-code">Error Code: {{ error_code }}</div>
|
|
|
|
<div class="action-buttons">
|
|
<a href="javascript:location.reload()" class="btn btn-primary">Retry</a>
|
|
<a href="/admin/dashboard" class="btn btn-secondary">Dashboard</a>
|
|
</div>
|
|
|
|
{% if show_debug %}
|
|
<div class="debug-info">
|
|
<h3>🔧 Debug Information (Admin Only)</h3>
|
|
<div class="debug-item">
|
|
<span class="debug-label">Path:</span>
|
|
<span class="debug-value">{{ path }}</span>
|
|
</div>
|
|
{% if details %}
|
|
<div class="debug-item">
|
|
<span class="debug-label">Details:</span>
|
|
<pre>{{ details | tojson(indent=2) }}</pre>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
{% endblock %}
|
|
```
|
|
|
|
**Option B: Standalone Template** (For custom designs)
|
|
|
|
```html
|
|
<!-- app/templates/shop/errors/custom.html -->
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<title>{{ status_code }} - {{ status_name }}</title>
|
|
<style>
|
|
/* Custom styling that uses store theme */
|
|
:root {
|
|
--primary: {{ theme.colors.primary }};
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Custom error page design -->
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
### Step 4: Add to ErrorPageRenderer (if needed)
|
|
|
|
If adding a new status code with a friendly name:
|
|
|
|
```python
|
|
# app/exceptions/error_renderer.py
|
|
|
|
STATUS_CODE_NAMES = {
|
|
# ... existing codes
|
|
503: "Service Unavailable", # Add new code
|
|
}
|
|
|
|
STATUS_CODE_MESSAGES = {
|
|
# ... existing messages
|
|
503: "The service is temporarily unavailable. Please try again later.",
|
|
}
|
|
```
|
|
|
|
### Step 5: Test
|
|
|
|
```bash
|
|
# Test the new error page
|
|
curl -H "Accept: text/html" http://localhost:8000/admin/test-503
|
|
|
|
# Or trigger programmatically:
|
|
from app.exceptions import ServiceUnavailableException
|
|
raise ServiceUnavailableException("Maintenance in progress")
|
|
```
|
|
|
|
---
|
|
|
|
## Testing Guide
|
|
|
|
### Unit Tests
|
|
|
|
**Test Context Detection:**
|
|
|
|
```python
|
|
# tests/test_frontend_detector.py
|
|
from app.core.frontend_detector import FrontendDetector
|
|
from app.modules.enums import FrontendType
|
|
|
|
def test_admin_detection():
|
|
assert FrontendDetector.is_admin("localhost", "/admin/dashboard") is True
|
|
|
|
def test_storefront_detection():
|
|
frontend_type = FrontendDetector.detect(
|
|
host="localhost", path="/storefront/products", has_store_context=True
|
|
)
|
|
assert frontend_type == FrontendType.STOREFRONT
|
|
```
|
|
|
|
**Test Error Renderer:**
|
|
|
|
```python
|
|
# tests/test_error_renderer.py
|
|
from app.exceptions.error_renderer import ErrorPageRenderer
|
|
|
|
def test_template_selection_admin():
|
|
template = ErrorPageRenderer._find_template(
|
|
context_type=RequestContext.ADMIN,
|
|
status_code=404
|
|
)
|
|
assert template == "admin/errors/404.html"
|
|
|
|
def test_template_fallback():
|
|
template = ErrorPageRenderer._find_template(
|
|
context_type=RequestContext.ADMIN,
|
|
status_code=999 # Non-existent error code
|
|
)
|
|
assert template == "admin/errors/generic.html"
|
|
```
|
|
|
|
**Test Exception Handlers:**
|
|
|
|
```python
|
|
# tests/test_exception_handlers.py
|
|
from fastapi.testclient import TestClient
|
|
|
|
def test_api_returns_json(client: TestClient):
|
|
response = client.get("/api/v1/nonexistent")
|
|
assert response.status_code == 404
|
|
assert response.headers["content-type"] == "application/json"
|
|
data = response.json()
|
|
assert "error_code" in data
|
|
assert "message" in data
|
|
|
|
def test_html_page_returns_html(client: TestClient):
|
|
response = client.get(
|
|
"/admin/nonexistent",
|
|
headers={"Accept": "text/html"}
|
|
)
|
|
assert response.status_code == 404
|
|
assert "text/html" in response.headers["content-type"]
|
|
assert "<html" in response.text
|
|
```
|
|
|
|
### Integration Tests
|
|
|
|
**Test Admin Error Pages:**
|
|
|
|
```python
|
|
def test_admin_404_error_page(client: TestClient):
|
|
response = client.get(
|
|
"/admin/nonexistent",
|
|
headers={"Accept": "text/html"}
|
|
)
|
|
assert response.status_code == 404
|
|
assert "Page Not Found" in response.text
|
|
assert "/admin/dashboard" in response.text # Dashboard link
|
|
assert "Admin Portal" in response.text
|
|
|
|
def test_admin_403_error_page(client: TestClient):
|
|
# Trigger 403 by accessing restricted resource
|
|
response = client.get(
|
|
"/admin/super-admin-only",
|
|
headers={"Accept": "text/html", "Authorization": "Bearer regular_user_token"}
|
|
)
|
|
assert response.status_code == 403
|
|
assert "Access Denied" in response.text
|
|
```
|
|
|
|
**Test 401 Redirect:**
|
|
|
|
```python
|
|
def test_401_redirects_to_admin_login(client: TestClient):
|
|
response = client.get(
|
|
"/admin/dashboard",
|
|
headers={"Accept": "text/html"},
|
|
follow_redirects=False
|
|
)
|
|
assert response.status_code == 302
|
|
assert response.headers["location"] == "/admin/login"
|
|
|
|
def test_401_api_returns_json(client: TestClient):
|
|
response = client.get(
|
|
"/api/v1/admin/dashboard",
|
|
follow_redirects=False
|
|
)
|
|
assert response.status_code == 401
|
|
assert response.headers["content-type"] == "application/json"
|
|
```
|
|
|
|
### Manual Testing Checklist
|
|
|
|
**Admin Context:**
|
|
|
|
```bash
|
|
# Test 404
|
|
curl -H "Accept: text/html" http://localhost:8000/admin/nonexistent
|
|
# Expected: Admin 404 page with "Go to Dashboard" button
|
|
|
|
# Test API 404
|
|
curl http://localhost:8000/api/v1/admin/nonexistent
|
|
# Expected: JSON error response
|
|
|
|
# Test 401 redirect
|
|
curl -H "Accept: text/html" http://localhost:8000/admin/dashboard
|
|
# Expected: 302 redirect to /admin/login
|
|
|
|
# Test validation error
|
|
curl -X POST -H "Accept: text/html" http://localhost:8000/admin/stores \
|
|
-d '{"invalid": "data"}'
|
|
# Expected: Admin 422 page with validation errors
|
|
```
|
|
|
|
**Store Context (Phase 2):**
|
|
|
|
```bash
|
|
# Test 404
|
|
curl -H "Accept: text/html" http://localhost:8000/store/nonexistent
|
|
# Expected: Store 404 page
|
|
|
|
# Test 401 redirect
|
|
curl -H "Accept: text/html" http://localhost:8000/store/dashboard
|
|
# Expected: 302 redirect to /store/login
|
|
```
|
|
|
|
**Shop Context (Phase 3):**
|
|
|
|
```bash
|
|
# Test 404 on store subdomain
|
|
curl -H "Accept: text/html" http://store1.localhost:8000/nonexistent
|
|
# Expected: Shop 404 page with store theme
|
|
|
|
# Test 500 error
|
|
# Trigger server error on shop
|
|
# Expected: Shop 500 page with store branding
|
|
```
|
|
|
|
### Performance Testing
|
|
|
|
**Load Test Error Handling:**
|
|
|
|
```python
|
|
# Test that error pages don't significantly impact performance
|
|
import time
|
|
|
|
def test_error_page_performance(client: TestClient):
|
|
# Test 100 consecutive 404 requests
|
|
start = time.time()
|
|
for _ in range(100):
|
|
response = client.get(
|
|
"/admin/nonexistent",
|
|
headers={"Accept": "text/html"}
|
|
)
|
|
assert response.status_code == 404
|
|
duration = time.time() - start
|
|
|
|
# Should complete in reasonable time (< 5 seconds)
|
|
assert duration < 5.0
|
|
print(f"100 error pages rendered in {duration:.2f}s")
|
|
```
|
|
|
|
### Template Validation
|
|
|
|
**Check Template Syntax:**
|
|
|
|
```bash
|
|
# Validate all Jinja2 templates
|
|
python -c "
|
|
from jinja2 import Environment, FileSystemLoader
|
|
import os
|
|
|
|
env = Environment(loader=FileSystemLoader('app/templates'))
|
|
errors = []
|
|
|
|
for root, dirs, files in os.walk('app/templates'):
|
|
for file in files:
|
|
if file.endswith('.html'):
|
|
path = os.path.join(root, file)
|
|
rel_path = os.path.relpath(path, 'app/templates')
|
|
try:
|
|
env.get_template(rel_path)
|
|
print(f'✓ {rel_path}')
|
|
except Exception as e:
|
|
errors.append((rel_path, str(e)))
|
|
print(f'✗ {rel_path}: {e}')
|
|
|
|
if errors:
|
|
print(f'\n{len(errors)} template(s) have errors')
|
|
exit(1)
|
|
else:
|
|
print(f'\nAll templates validated successfully')
|
|
"
|
|
```
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Issue: API Returning HTML Instead of JSON
|
|
|
|
**Symptoms:**
|
|
```bash
|
|
curl http://localhost:8000/api/v1/admin/stores/999
|
|
# Returns HTML instead of JSON
|
|
```
|
|
|
|
**Diagnosis:**
|
|
Check if `_is_api_request()` is working correctly:
|
|
|
|
```python
|
|
# Add logging to handler.py
|
|
logger.debug(f"Path: {request.url.path}, is_api: {_is_api_request(request)}")
|
|
```
|
|
|
|
**Solution:**
|
|
Ensure path check is correct:
|
|
```python
|
|
def _is_api_request(request: Request) -> bool:
|
|
return request.url.path.startswith("/api/") # Must have leading slash
|
|
```
|
|
|
|
### Issue: HTML Pages Returning JSON
|
|
|
|
**Symptoms:**
|
|
Browser shows JSON instead of error page when visiting `/admin/nonexistent`
|
|
|
|
**Diagnosis:**
|
|
Check Accept header:
|
|
```python
|
|
# Add logging
|
|
logger.debug(f"Accept: {request.headers.get('accept', '')}")
|
|
logger.debug(f"is_html: {_is_html_page_request(request)}")
|
|
```
|
|
|
|
**Common Causes:**
|
|
1. Browser not sending `Accept: text/html`
|
|
2. Request is POST/PUT/DELETE (not GET)
|
|
3. Path is `/api/*`
|
|
4. Already on login page
|
|
|
|
**Solution:**
|
|
Verify `_is_html_page_request()` logic:
|
|
```python
|
|
def _is_html_page_request(request: Request) -> bool:
|
|
if _is_api_request(request):
|
|
return False
|
|
if request.url.path.endswith("/login"):
|
|
return False
|
|
if request.method != "GET":
|
|
return False
|
|
accept_header = request.headers.get("accept", "")
|
|
if "text/html" not in accept_header:
|
|
return False
|
|
return True
|
|
```
|
|
|
|
### Issue: Template Not Found Error
|
|
|
|
**Symptoms:**
|
|
```
|
|
jinja2.exceptions.TemplateNotFound: admin/errors/404.html
|
|
```
|
|
|
|
**Diagnosis:**
|
|
1. Check template file exists:
|
|
```bash
|
|
ls -la app/templates/admin/errors/404.html
|
|
```
|
|
|
|
2. Check templates directory configuration in main.py:
|
|
```python
|
|
# Should be:
|
|
TEMPLATES_DIR = BASE_DIR / "app" / "templates"
|
|
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
|
```
|
|
|
|
**Solution:**
|
|
- Ensure template files are in correct location
|
|
- Verify templates directory path is correct
|
|
- Check file permissions
|
|
|
|
### Issue: Wrong Context Detected
|
|
|
|
**Symptoms:**
|
|
Admin page shows shop error page, or vice versa
|
|
|
|
**Diagnosis:**
|
|
Add context logging:
|
|
```python
|
|
# In context_middleware.py
|
|
logger.info(
|
|
f"Context detection: path={request.url.path}, "
|
|
f"host={request.headers.get('host')}, "
|
|
f"context={context_type}"
|
|
)
|
|
```
|
|
|
|
**Solution:**
|
|
Check middleware order in `main.py`:
|
|
```python
|
|
# Correct order:
|
|
app.middleware("http")(store_context_middleware) # FIRST
|
|
app.middleware("http")(context_middleware) # SECOND
|
|
```
|
|
|
|
### Issue: Debug Info Not Showing for Admin
|
|
|
|
**Symptoms:**
|
|
Debug information not visible in admin error pages
|
|
|
|
**Diagnosis:**
|
|
Check `_is_admin_user()` implementation:
|
|
```python
|
|
# In error_renderer.py
|
|
@staticmethod
|
|
def _is_admin_user(request: Request) -> bool:
|
|
context_type = get_request_context(request)
|
|
logger.debug(f"Checking admin user: context={context_type}")
|
|
return context_type == RequestContext.ADMIN
|
|
```
|
|
|
|
**Solution:**
|
|
- Verify context is correctly detected as ADMIN
|
|
- Check `show_debug` parameter is True in render call
|
|
- Ensure template has `{% if show_debug %}` block
|
|
|
|
### Issue: 401 Not Redirecting
|
|
|
|
**Symptoms:**
|
|
401 errors show JSON instead of redirecting to login
|
|
|
|
**Diagnosis:**
|
|
Check if request is detected as HTML page request:
|
|
```python
|
|
# Add logging before redirect check
|
|
logger.debug(
|
|
f"401 handling: is_html={_is_html_page_request(request)}, "
|
|
f"path={request.url.path}, method={request.method}, "
|
|
f"accept={request.headers.get('accept')}"
|
|
)
|
|
```
|
|
|
|
**Solution:**
|
|
Ensure all conditions for HTML page request are met:
|
|
- NOT an API endpoint
|
|
- GET request
|
|
- Accept header includes `text/html`
|
|
- NOT already on login page
|
|
|
|
### Issue: Store Theme Not Applied to Shop Errors
|
|
|
|
**Symptoms:**
|
|
Shop error pages don't use store colors/branding (Phase 3 issue)
|
|
|
|
**Diagnosis:**
|
|
1. Check if theme is in request state:
|
|
```python
|
|
logger.debug(f"Theme in state: {hasattr(request.state, 'theme')}")
|
|
logger.debug(f"Theme data: {getattr(request.state, 'theme', None)}")
|
|
```
|
|
|
|
2. Check if theme is passed to template:
|
|
```python
|
|
# In error_renderer.py _get_context_data()
|
|
if context_type == RequestContext.SHOP:
|
|
theme = getattr(request.state, "theme", None)
|
|
logger.debug(f"Passing theme to template: {theme}")
|
|
```
|
|
|
|
**Solution:**
|
|
- Ensure theme_context_middleware ran before error
|
|
- Verify theme data structure is correct
|
|
- Check template uses theme variables correctly:
|
|
```html
|
|
<style>
|
|
:root {
|
|
--primary: {{ theme.colors.primary if theme else '#6366f1' }};
|
|
}
|
|
</style>
|
|
```
|
|
|
|
---
|
|
|
|
## API Reference
|
|
|
|
### Frontend Type Detection
|
|
|
|
#### `get_frontend_type(request: Request) -> FrontendType`
|
|
|
|
Gets the frontend type for the current request.
|
|
|
|
**Parameters:**
|
|
- `request` (Request): FastAPI request object
|
|
|
|
**Returns:**
|
|
- FrontendType enum value
|
|
|
|
**Example:**
|
|
```python
|
|
from middleware.frontend_type import get_frontend_type
|
|
from app.modules.enums import FrontendType
|
|
|
|
def my_handler(request: Request):
|
|
frontend_type = get_frontend_type(request)
|
|
return {"frontend_type": frontend_type.value}
|
|
```
|
|
|
|
### Error Rendering
|
|
|
|
#### `ErrorPageRenderer.render_error_page(...) -> HTMLResponse`
|
|
|
|
Renders context-aware HTML error page.
|
|
|
|
**Parameters:**
|
|
```python
|
|
request: Request # FastAPI request object
|
|
status_code: int # HTTP status code
|
|
error_code: str # Application error code
|
|
message: str # User-friendly error message
|
|
details: Optional[Dict[str, Any]] = None # Additional error details
|
|
show_debug: bool = False # Whether to show debug info
|
|
```
|
|
|
|
**Returns:**
|
|
- HTMLResponse with rendered error page
|
|
|
|
**Example:**
|
|
```python
|
|
from app.exceptions.error_renderer import ErrorPageRenderer
|
|
|
|
return ErrorPageRenderer.render_error_page(
|
|
request=request,
|
|
status_code=404,
|
|
error_code="STORE_NOT_FOUND",
|
|
message="The store you're looking for doesn't exist.",
|
|
details={"store_id": "123"},
|
|
show_debug=True,
|
|
)
|
|
```
|
|
|
|
#### `ErrorPageRenderer.get_templates_dir() -> Path`
|
|
|
|
Gets the templates directory path.
|
|
|
|
**Returns:**
|
|
- Path object pointing to templates directory
|
|
|
|
#### `ErrorPageRenderer._find_template(context_type, status_code) -> str`
|
|
|
|
Finds appropriate error template based on context and status code.
|
|
|
|
**Parameters:**
|
|
- `context_type` (RequestContext): Request context type
|
|
- `status_code` (int): HTTP status code
|
|
|
|
**Returns:**
|
|
- String path to template (e.g., "admin/errors/404.html")
|
|
|
|
### Exception Utilities
|
|
|
|
#### `raise_not_found(resource_type: str, identifier: str) -> None`
|
|
|
|
Convenience function to raise ResourceNotFoundException.
|
|
|
|
**Example:**
|
|
```python
|
|
from app.exceptions.handler import raise_not_found
|
|
|
|
raise_not_found("Store", "123")
|
|
# Raises: ResourceNotFoundException with appropriate message
|
|
```
|
|
|
|
#### `raise_validation_error(message: str, field: str = None, details: dict = None) -> None`
|
|
|
|
Convenience function to raise ValidationException.
|
|
|
|
**Example:**
|
|
```python
|
|
from app.exceptions.handler import raise_validation_error
|
|
|
|
raise_validation_error(
|
|
message="Invalid email format",
|
|
field="email",
|
|
details={"provided": user_input}
|
|
)
|
|
```
|
|
|
|
#### `raise_auth_error(message: str = "Authentication failed") -> None`
|
|
|
|
Convenience function to raise AuthenticationException.
|
|
|
|
#### `raise_permission_error(message: str = "Access denied") -> None`
|
|
|
|
Convenience function to raise AuthorizationException.
|
|
|
|
---
|
|
|
|
## Configuration
|
|
|
|
### Middleware Order
|
|
|
|
**Critical:** Middleware must be registered in this exact order in `main.py`:
|
|
|
|
```python
|
|
# 1. CORS (if needed)
|
|
app.add_middleware(CORSMiddleware, ...)
|
|
|
|
# 2. Store Context Detection (MUST BE FIRST)
|
|
app.middleware("http")(store_context_middleware)
|
|
|
|
# 3. Context Detection (MUST BE AFTER STORE)
|
|
app.middleware("http")(context_middleware)
|
|
|
|
# 4. Theme Context (MUST BE AFTER CONTEXT)
|
|
app.middleware("http")(theme_context_middleware)
|
|
|
|
# 5. Logging (optional, should be last)
|
|
app.add_middleware(LoggingMiddleware)
|
|
```
|
|
|
|
**Why This Order Matters:**
|
|
1. Store context must set `request.state.store` first
|
|
2. Context detection needs store info to identify SHOP context
|
|
3. Theme context needs store info to load theme
|
|
4. Logging should be last to capture all middleware activity
|
|
|
|
### Template Directory
|
|
|
|
Configure in `main.py`:
|
|
|
|
```python
|
|
from pathlib import Path
|
|
|
|
BASE_DIR = Path(__file__).resolve().parent
|
|
TEMPLATES_DIR = BASE_DIR / "app" / "templates"
|
|
|
|
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
|
```
|
|
|
|
### Error Code Registry
|
|
|
|
Update as needed in `app/exceptions/error_renderer.py`:
|
|
|
|
```python
|
|
STATUS_CODE_NAMES = {
|
|
400: "Bad Request",
|
|
401: "Unauthorized",
|
|
403: "Forbidden",
|
|
404: "Not Found",
|
|
422: "Validation Error",
|
|
429: "Too Many Requests",
|
|
500: "Internal Server Error",
|
|
502: "Bad Gateway",
|
|
503: "Service Unavailable",
|
|
# Add new codes here
|
|
}
|
|
|
|
STATUS_CODE_MESSAGES = {
|
|
400: "The request could not be processed due to invalid data.",
|
|
# ... etc
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Best Practices Summary
|
|
|
|
### DO:
|
|
✅ Use specific exception classes for different error scenarios
|
|
✅ Provide clear, user-friendly error messages
|
|
✅ Include relevant details in the `details` dictionary
|
|
✅ Use consistent error codes across the application
|
|
✅ Test both API and HTML responses
|
|
✅ Keep error templates simple and accessible
|
|
✅ Use debug mode responsibly (admin only)
|
|
✅ Follow the template inheritance pattern
|
|
✅ Document any new exception types
|
|
✅ Test error pages manually in browsers
|
|
|
|
### DON'T:
|
|
❌ Expose sensitive information in error messages
|
|
❌ Use generic exceptions for domain-specific errors
|
|
❌ Return HTML for API endpoints
|
|
❌ Skip the Accept header check
|
|
❌ Hardcode HTML in exception handlers
|
|
❌ Forget to add fallback templates
|
|
❌ Show technical details to customers
|
|
❌ Use complex JavaScript in error pages
|
|
❌ Forget to test 401 redirects
|
|
❌ Mix API and page response logic
|
|
|
|
---
|
|
|
|
## Support and Questions
|
|
|
|
For questions or issues with the error handling system:
|
|
|
|
1. **Check Logs:** Look for `[CONTEXT]` and `[ERROR]` log messages
|
|
2. **Review This Documentation:** Most issues are covered in Troubleshooting
|
|
3. **Check Implementation Status:** Verify which phase is complete
|
|
4. **Test Locally:** Reproduce the issue with curl/browser
|
|
5. **Contact:** [Your team contact information]
|
|
|
|
---
|
|
|
|
**Document Version:** 1.0.0
|
|
**Last Updated:** 2025
|
|
**Maintained By:** [Your team name]
|
|
**Next Review:** After Phase 2 & 3 completion
|