Major architecture change to unify frontend detection: ## Problem Solved - Eliminated code duplication across 3 middleware files - Fixed incomplete path detection (now detects /api/v1/admin/*) - Unified on FrontendType enum (deprecates RequestContext) - Added request.state.frontend_type for all requests ## New Components - app/core/frontend_detector.py: Centralized FrontendDetector class - middleware/frontend_type.py: FrontendTypeMiddleware (replaces ContextMiddleware) - docs/architecture/frontend-detection.md: Complete architecture documentation ## Changes - main.py: Use FrontendTypeMiddleware instead of ContextMiddleware - middleware/context.py: Deprecated (kept for backwards compatibility) - middleware/platform_context.py: Use FrontendDetector.is_admin() - middleware/vendor_context.py: Use FrontendDetector.is_admin() - middleware/language.py: Use FrontendType instead of context_value - app/exceptions/handler.py: Use FrontendType.STOREFRONT - app/exceptions/error_renderer.py: Use FrontendType - Customer routes: Cookie path changed from /shop to /storefront ## Documentation - docs/architecture/frontend-detection.md: New comprehensive docs - docs/architecture/middleware.md: Updated for new system - docs/architecture/request-flow.md: Updated for FrontendType - docs/backend/middleware-reference.md: Updated API reference ## Tests - tests/unit/core/test_frontend_detector.py: 37 new tests - tests/unit/middleware/test_frontend_type.py: 11 new tests - tests/unit/middleware/test_context.py: Updated for compatibility Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
18 KiB
Middleware Stack
The middleware stack is the backbone of the multi-tenant system, handling tenant detection, context injection, and theme loading for all requests.
Overview
The application uses a custom middleware stack that processes every request regardless of whether it's:
- REST API calls (
/api/*) - Admin interface pages (
/admin/*) - Vendor dashboard pages (
/vendor/*) - Shop pages (
/shop/*or custom domains)
This middleware layer is system-wide and enables the multi-tenant architecture to function seamlessly.
Middleware Components
1. Platform Context Middleware
Purpose: Detect which platform (OMS, Loyalty, Main) the request is for
What it does:
- Detects platform from:
- Custom domain (e.g.,
oms.lu,loyalty.lu) - Path prefix in development (e.g.,
/platforms/oms/,/platforms/loyalty/) - Default to
mainplatform for localhost without prefix
- Custom domain (e.g.,
- Rewrites path for platform-prefixed requests (strips
/platforms/{code}/) - Queries database to find platform by domain or code
- Injects platform object into
request.state.platform
URL Detection Logic:
Request arrives
│
▼
┌─────────────────────────────────────┐
│ Production domain? (oms.lu, etc.) │
└─────────────────────────────────────┘
│ YES → Use that platform
│
▼ NO (localhost)
┌─────────────────────────────────────┐
│ Path starts with /platforms/{code}? │
└─────────────────────────────────────┘
│ YES → Strip prefix, use platform
│ /platforms/oms/pricing → /pricing
│
▼ NO
┌─────────────────────────────────────┐
│ Use 'main' platform (DEFAULT) │
│ Path unchanged │
└─────────────────────────────────────┘
Example:
Request: https://localhost:9999/platforms/oms/pricing
↓
Middleware detects: platform_code = "oms"
↓
Rewrites path: /platforms/oms/pricing → /pricing
↓
Queries database: SELECT * FROM platforms WHERE code = 'oms'
↓
Injects: request.state.platform = <Platform object>
Why it's critical: Without this, the system wouldn't know which platform's content to serve
Configuration: Runs BEFORE VendorContextMiddleware (sets platform context first)
2. Logging Middleware
Purpose: Request/response logging and performance monitoring
What it does:
- Logs every incoming request with method, path, and client IP
- Measures request processing time
- Logs response status codes
- Adds
X-Process-Timeheader with processing duration - Logs errors with stack traces
Example Log Output:
INFO Request: GET /admin/dashboard from 192.168.1.100
INFO Response: 200 for GET /admin/dashboard (0.143s)
Configuration: Runs first to capture full request timing
2. Vendor Context Middleware
Purpose: Detect which vendor's shop the request is for (multi-tenant core)
What it does:
- Detects vendor from:
- Custom domain (e.g.,
customdomain.com) - Subdomain (e.g.,
vendor1.platform.com) - Path prefix (e.g.,
/vendor/vendor1/or/vendors/vendor1/)
- Custom domain (e.g.,
- Queries database to find vendor by domain or code
- Injects vendor object into
request.state.vendor - Extracts "clean path" (path without vendor prefix)
- Sets
request.state.clean_pathfor routing
Example:
Request: https://wizamart.platform.com/shop/products
↓
Middleware detects: vendor_code = "wizamart"
↓
Queries database: SELECT * FROM vendors WHERE code = 'wizamart'
↓
Injects: request.state.vendor = <Vendor object>
request.state.vendor_id = 1
request.state.clean_path = "/shop/products"
Why it's critical: Without this, the system wouldn't know which vendor's data to show
See: Multi-Tenant System for routing modes
Note on Path-Based Routing: Previous implementations used a PathRewriteMiddleware to rewrite paths at runtime. This has been replaced with double router mounting in main.py, where shop routes are registered twice with different prefixes (/shop and /vendors/{vendor_code}/shop). This approach is simpler and uses FastAPI's native routing capabilities.
3. Frontend Type Detection Middleware
Purpose: Determine which frontend the request targets
What it does:
- Uses centralized
FrontendDetectorclass for all detection logic - Determines which frontend is being accessed:
ADMIN-/admin/*,/api/v1/admin/*paths oradmin.*subdomainVENDOR-/vendor/*,/api/v1/vendor/*paths (management area)STOREFRONT- Customer shop pages (/storefront/*,/vendors/*, vendor subdomains)PLATFORM- Marketing pages (/,/pricing,/about)
- Injects
request.state.frontend_type(FrontendType enum)
Detection Priority (handled by FrontendDetector):
1. Admin subdomain (admin.oms.lu) → ADMIN
2. Path-based detection:
- /admin/* or /api/v1/admin/* → ADMIN
- /vendor/* or /api/v1/vendor/* → VENDOR
- /storefront/*, /shop/*, /vendors/* → STOREFRONT
- /api/v1/platform/* → PLATFORM
3. Vendor subdomain (wizamart.oms.lu) → STOREFRONT
4. Vendor context set by middleware → STOREFRONT
5. Default → PLATFORM
Why it's useful: Error handlers, templates, and language detection adapt based on frontend type
See: Frontend Detection Architecture for complete details
4. Theme Context Middleware
Purpose: Load vendor-specific theme settings
What it does:
- Checks if request has a vendor (from VendorContextMiddleware)
- Queries database for vendor's theme settings
- Injects theme configuration into
request.state.theme - Provides default theme if vendor has no custom theme
Theme Data Structure:
{
"primary_color": "#3B82F6",
"secondary_color": "#10B981",
"logo_url": "/static/vendors/wizamart/logo.png",
"favicon_url": "/static/vendors/wizamart/favicon.ico",
"custom_css": "/* vendor-specific styles */"
}
Why it's needed: Each vendor shop can have custom branding
Naming Conventions
Middleware File Organization
All middleware components follow a consistent naming pattern for maintainability and clarity.
File Naming: Simple Nouns Without Redundant Suffixes
Pattern: {purpose}.py (no "_middleware" suffix)
✅ Good:
middleware/logging.py
middleware/context.py
middleware/auth.py
❌ Avoid:
middleware/logging_middleware.py
middleware/context_middleware.py
middleware/auth_middleware.py
Rationale:
- Keeps names concise and consistent
- Follows Django, Flask, and FastAPI conventions
- Makes imports cleaner:
from middleware.logging import LoggingMiddleware - Reduces redundancy (the
middleware/directory already indicates the purpose)
Test File Naming: Mirror the Source File
Test files directly mirror the middleware filename with a test_ prefix:
middleware/logging.py → tests/unit/middleware/test_logging.py
middleware/context.py → tests/unit/middleware/test_context.py
middleware/auth.py → tests/unit/middleware/test_auth.py
middleware/vendor_context.py → tests/unit/middleware/test_vendor_context.py
One Component Per File
Each middleware file contains one primary class or a tightly related set of classes:
# middleware/logging.py
class LoggingMiddleware(BaseHTTPMiddleware):
"""Request/response logging middleware"""
# middleware/frontend_type.py
class FrontendTypeMiddleware: # ASGI wrapper for frontend detection
# Uses FrontendDetector from app/core/frontend_detector.py
# middleware/auth.py
class AuthManager: # Authentication logic
Note
: The old
middleware/context.pywithContextMiddlewareandRequestContextis deprecated. UseFrontendTypeMiddlewareandFrontendTypeenum instead. See Frontend Detection Architecture for migration guide.
One Test File Per Component
Follow the Single Responsibility Principle - each test file tests exactly one component:
✅ Good:
tests/unit/middleware/test_logging.py # Tests only LoggingMiddleware
tests/unit/middleware/test_context.py # Tests only ContextManager/Middleware
tests/unit/middleware/test_decorators.py # Tests only rate_limit decorator
❌ Avoid:
tests/unit/middleware/test_all_middleware.py # Tests multiple components
tests/unit/middleware/test_combined.py # Violates SRP
Benefits:
- Easy to locate tests for specific components
- Clear test organization and maintainability
- Follows unit testing best practices
- Simplifies test debugging and updates
Import Convention
When importing middleware components, use explicit imports:
# ✅ Preferred - Explicit and clear
from middleware.logging import LoggingMiddleware
from middleware.context import ContextManager, RequestContext
from middleware.auth import AuthManager
# ❌ Avoid - Less clear
from middleware import logging_middleware
from middleware import context_middleware
See: Complete Naming Conventions Guide for project-wide standards.
Middleware Execution Order
The Stack (First to Last)
graph TD
A[Client Request] --> B[1. LoggingMiddleware]
B --> C[2. PlatformContextMiddleware]
C --> D[3. VendorContextMiddleware]
D --> E[4. ContextDetectionMiddleware]
E --> F[5. ThemeContextMiddleware]
F --> G[6. FastAPI Router]
G --> H[Route Handler]
H --> I[Response]
I --> J[Client]
Why This Order Matters
Critical Dependencies:
-
LoggingMiddleware first
- Needs to wrap everything to measure total time
- Must log errors from all other middleware
-
PlatformContextMiddleware second
- Must run before VendorContextMiddleware (sets platform context)
- Rewrites path for
/platforms/{code}/prefixed requests - Sets
request.state.platformfor downstream middleware
-
VendorContextMiddleware third
- Uses rewritten path from PlatformContextMiddleware
- Must run before ContextDetectionMiddleware (provides vendor and clean_path)
- Must run before ThemeContextMiddleware (provides vendor_id)
-
ContextDetectionMiddleware fourth
- Uses clean_path from VendorContextMiddleware
- Provides context_type for ThemeContextMiddleware
-
ThemeContextMiddleware last
- Depends on vendor from VendorContextMiddleware
- Depends on context_type from ContextDetectionMiddleware
Breaking this order will break the application!
Note: Path-based routing (e.g., /vendors/{code}/shop/*) is handled by double router mounting in main.py, not by middleware. Platform path-based routing (e.g., /platforms/oms/) IS handled by PlatformContextMiddleware which rewrites the path.
Request State Variables
Middleware components inject these variables into request.state:
| Variable | Set By | Type | Used By | Description |
|---|---|---|---|---|
platform |
PlatformContextMiddleware | Platform | Routes, Content | Current platform object (main, oms, loyalty) |
platform_context |
PlatformContextMiddleware | dict | Routes | Platform detection details (method, paths) |
vendor |
VendorContextMiddleware | Vendor | Theme, Templates | Current vendor object |
vendor_id |
VendorContextMiddleware | int | Queries, Theme | Current vendor ID |
clean_path |
VendorContextMiddleware | str | Context | Path without vendor prefix (for context detection) |
context_type |
ContextDetectionMiddleware | RequestContext | Theme, Error handlers | Request context enum |
theme |
ThemeContextMiddleware | dict | Templates | Vendor theme config |
Using in Route Handlers
from fastapi import Request
@app.get("/shop/products")
async def get_products(request: Request):
# Access vendor
vendor = request.state.vendor
vendor_id = request.state.vendor_id
# Access context
context = request.state.context_type
# Access theme
theme = request.state.theme
# Use in queries
products = db.query(Product).filter(
Product.vendor_id == vendor_id
).all()
return {"vendor": vendor.name, "products": products}
Using in Templates
{# Access vendor #}
<h1>{{ request.state.vendor.name }}</h1>
{# Access theme #}
<style>
:root {
--primary-color: {{ request.state.theme.primary_color }};
--secondary-color: {{ request.state.theme.secondary_color }};
}
</style>
{# Access frontend type #}
{% if request.state.frontend_type.value == "admin" %}
<div class="admin-badge">Admin Mode</div>
{% endif %}
Request Flow Example
Example: Shop Product Page Request
URL: https://wizamart.myplatform.com/shop/products
Middleware Processing:
1. LoggingMiddleware
↓ Starts timer
↓ Logs: "Request: GET /shop/products from 192.168.1.100"
2. VendorContextMiddleware
↓ Detects subdomain: "wizamart"
↓ Queries DB: vendor = get_vendor_by_code("wizamart")
↓ Sets: request.state.vendor = <Vendor: Wizamart>
↓ Sets: request.state.vendor_id = 1
↓ Sets: request.state.clean_path = "/shop/products"
3. FrontendTypeMiddleware
↓ Uses FrontendDetector with path: "/shop/products"
↓ Has vendor context: Yes
↓ Detects storefront frontend
↓ Sets: request.state.frontend_type = FrontendType.STOREFRONT
4. ThemeContextMiddleware
↓ Loads theme for vendor_id = 1
↓ Sets: request.state.theme = {...theme config...}
5. FastAPI Router
↓ Matches route: @app.get("/shop/products")
↓ Calls handler function
6. Route Handler
↓ Accesses: request.state.vendor_id
↓ Queries: products WHERE vendor_id = 1
↓ Renders template with vendor data
8. Response
↓ Returns HTML with vendor theme
9. LoggingMiddleware (response phase)
↓ Logs: "Response: 200 for GET /shop/products (0.143s)"
↓ Adds header: X-Process-Time: 0.143
Error Handling in Middleware
Each middleware component handles errors gracefully:
VendorContextMiddleware
- If vendor not found: Sets
request.state.vendor = None - If database error: Logs error, allows request to continue
- Fallback: Request proceeds without vendor context
FrontendTypeMiddleware
- If clean_path missing: Uses original path
- If vendor missing: Defaults to PLATFORM frontend type
- Always sets a frontend_type (never None)
ThemeContextMiddleware
- If vendor missing: Skips theme loading
- If theme query fails: Uses default theme
- If no theme exists: Returns empty theme dict
Design Philosophy: Middleware should never crash the application. Degrade gracefully.
Performance Considerations
Database Queries
Per Request:
- 1 query in VendorContextMiddleware (vendor lookup) - cached by DB
- 1 query in ThemeContextMiddleware (theme lookup) - cached by DB
Total: ~2 DB queries per request
Optimization Opportunities:
- Implement Redis caching for vendor lookups
- Cache theme data in memory
- Use connection pooling (already enabled)
Memory Usage
Minimal per-request overhead:
- Small objects stored in
request.state - No global state maintained
- Garbage collected after response
Latency
Typical overhead: < 5ms per request
- Vendor lookup: ~2ms
- Theme lookup: ~2ms
- Context detection: <1ms
Configuration
Middleware is registered in main.py:
# Add in REVERSE order (LIFO execution)
app.add_middleware(LoggingMiddleware)
app.add_middleware(ThemeContextMiddleware)
app.add_middleware(LanguageMiddleware)
app.add_middleware(FrontendTypeMiddleware)
app.add_middleware(VendorContextMiddleware)
app.add_middleware(PlatformContextMiddleware)
Note: FastAPI's add_middleware executes in reverse order (Last In, First Out)
Testing Middleware
Unit Testing
Test each middleware component in isolation:
from middleware.vendor_context import VendorContextManager
def test_vendor_detection_subdomain():
# Mock request
request = create_mock_request(host="wizamart.platform.com")
# Test detection
manager = VendorContextManager()
vendor = manager.detect_vendor_from_subdomain(request)
assert vendor.code == "wizamart"
Integration Testing
Test the full middleware stack:
def test_shop_request_flow(client):
response = client.get(
"/shop/products",
headers={"Host": "wizamart.platform.com"}
)
assert response.status_code == 200
assert "Wizamart" in response.text
See: Testing Guide
Debugging Middleware
Enable Debug Logging
import logging
logging.getLogger("middleware").setLevel(logging.DEBUG)
Check Request State
In route handlers:
@app.get("/debug")
async def debug_state(request: Request):
return {
"vendor": request.state.vendor.name if hasattr(request.state, 'vendor') else None,
"vendor_id": getattr(request.state, 'vendor_id', None),
"clean_path": getattr(request.state, 'clean_path', None),
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None,
"theme": bool(getattr(request.state, 'theme', None))
}
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| Vendor not detected | Wrong host header | Check domain configuration |
| Context is FALLBACK | Path doesn't match patterns | Check route prefix |
| Theme not loading | Vendor ID missing | Check VendorContextMiddleware runs first |
| Sidebar broken | Variable name conflict | See frontend troubleshooting |
Related Documentation
- Multi-Tenant System - Detailed routing modes
- Request Flow - Complete request journey
- Authentication & RBAC - Security middleware
- Backend API Reference - Technical API docs
- Frontend Development - Using middleware state in frontend
Technical Reference
For detailed API documentation of middleware classes and methods, see:
This includes:
- Complete class documentation
- Method signatures
- Parameter details
- Return types
- Auto-generated from source code