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>
36 KiB
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
- Overview
- Architecture
- System Components
- Context Detection
- Error Response Types
- Template System
- Implementation Status
- Developer Guidelines
- Adding New Error Pages
- Testing Guide
- Troubleshooting
- 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
- Separation of Concerns: HTML templates are separate from exception handler logic
- Fail-Safe: Multiple fallback levels ensure errors always render
- Developer-Friendly: Easy to add new error pages or contexts
- User-Centric: Professional error pages with clear messaging and actions
- 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:
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:
- API - Path starts with
/api/(highest priority) - ADMIN - Path starts with
/adminor host starts withadmin. - STORE_DASHBOARD - Path starts with
/store/ - SHOP -
request.state.storeexists (set by store_context_middleware) - FALLBACK - None of the above match
Usage:
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:
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:
{context}/errors/{status_code}.html(e.g.,admin/errors/404.html){context}/errors/generic.html(e.g.,admin/errors/generic.html)shared/{status_code}-fallback.html(e.g.,shared/404-fallback.html)shared/generic-fallback.html(absolute fallback)
Template Variables Provided:
{
"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 exceptionsHTTPException- FastAPI HTTP exceptionsRequestValidationError- Pydantic validation errorsException- Generic Python exceptions404- Not Found errors
Response Logic:
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:
# 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:
{
"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>
<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:
<!-- 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
<!-- 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:
<!-- 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:
- Create
/app/templates/store/errors/directory - Copy admin templates as starting point
- Customize messaging for store context:
- Change "Admin Portal" to "Store Portal"
- Update dashboard links to
/store/dashboard - Adjust support links to store support
- Update action button destinations
- 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:
- Create
/app/templates/shop/errors/directory - Create customer-facing error templates:
- Use customer-friendly language
- Integrate store theme variables
- Add store logo/branding
- Update ErrorPageRenderer to pass theme data
- Implement theme integration:
if context_type == RequestContext.SHOP: template_data["theme"] = request.state.theme template_data["store"] = request.state.store - Test with multiple store themes
- 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:
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:
# 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:
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
# 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
# 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:
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)
<!-- 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)
<!-- 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:
# 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
# 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:
# 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:
# 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:
# 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:
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:
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:
# 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):
# 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):
# 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:
# 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:
# 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:
curl http://localhost:8000/api/v1/admin/stores/999
# Returns HTML instead of JSON
Diagnosis:
Check if _is_api_request() is working correctly:
# Add logging to handler.py
logger.debug(f"Path: {request.url.path}, is_api: {_is_api_request(request)}")
Solution: Ensure path check is correct:
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:
# Add logging
logger.debug(f"Accept: {request.headers.get('accept', '')}")
logger.debug(f"is_html: {_is_html_page_request(request)}")
Common Causes:
- Browser not sending
Accept: text/html - Request is POST/PUT/DELETE (not GET)
- Path is
/api/* - Already on login page
Solution:
Verify _is_html_page_request() logic:
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:
- Check template file exists:
ls -la app/templates/admin/errors/404.html
- Check templates directory configuration in main.py:
# 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:
# 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:
# 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:
# 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_debugparameter 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:
# 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:
- Check if theme is in request state:
logger.debug(f"Theme in state: {hasattr(request.state, 'theme')}")
logger.debug(f"Theme data: {getattr(request.state, 'theme', None)}")
- Check if theme is passed to template:
# 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:
<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:
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:
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:
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 typestatus_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:
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:
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:
# 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:
- Store context must set
request.state.storefirst - Context detection needs store info to identify SHOP context
- Theme context needs store info to load theme
- Logging should be last to capture all middleware activity
Template Directory
Configure in main.py:
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:
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:
- Check Logs: Look for
[CONTEXT]and[ERROR]log messages - Review This Documentation: Most issues are covered in Troubleshooting
- Check Implementation Status: Verify which phase is complete
- Test Locally: Reproduce the issue with curl/browser
- 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