13 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. 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
3. Path Rewrite Middleware
Purpose: Rewrite request paths for proper FastAPI routing
What it does:
- Uses the
clean_pathextracted by VendorContextMiddleware - Rewrites
request.scope['path']to remove vendor prefix - Allows FastAPI routes to match correctly
Example (Path-Based Development Mode):
Original path: /vendors/WIZAMART/shop/products
Clean path: /shop/products (set by VendorContextMiddleware)
↓
Path Rewrite Middleware changes request path to: /shop/products
↓
FastAPI router can now match: @app.get("/shop/products")
Why it's needed: FastAPI routes don't include vendor prefix, so we strip it
4. Context Detection Middleware
Purpose: Determine the type/context of the request
What it does:
- Analyzes the request path (using clean_path)
- Determines which interface is being accessed:
API-/api/*pathsADMIN-/admin/*paths oradmin.*subdomainVENDOR_DASHBOARD-/vendor/*paths (management area)SHOP- Storefront pages (has vendor + not admin/vendor/API)FALLBACK- Unknown context
- Injects
request.state.context_type
Detection Rules:
if path.startswith("/api/"):
context = API
elif path.startswith("/admin/") or host.startswith("admin."):
context = ADMIN
elif path.startswith("/vendor/"):
context = VENDOR_DASHBOARD
elif request.state.vendor exists:
context = SHOP
else:
context = FALLBACK
Why it's useful: Error handlers and templates adapt based on context
5. 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
Middleware Execution Order
The Stack (First to Last)
graph TD
A[Client Request] --> B[1. LoggingMiddleware]
B --> C[2. VendorContextMiddleware]
C --> D[3. PathRewriteMiddleware]
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
-
VendorContextMiddleware second
- Must run before PathRewriteMiddleware (provides clean_path)
- Must run before ContextDetectionMiddleware (provides vendor)
- Must run before ThemeContextMiddleware (provides vendor_id)
-
PathRewriteMiddleware third
- Depends on clean_path from VendorContextMiddleware
- Must run before ContextDetectionMiddleware (rewrites path)
-
ContextDetectionMiddleware fourth
- Uses clean_path from VendorContextMiddleware
- Uses rewritten path from PathRewriteMiddleware
- 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!
Request State Variables
Middleware components inject these variables into request.state:
| Variable | Set By | Type | Used By | Description |
|---|---|---|---|---|
vendor |
VendorContextMiddleware | Vendor | Theme, Templates | Current vendor object |
vendor_id |
VendorContextMiddleware | int | Queries, Theme | Current vendor ID |
clean_path |
VendorContextMiddleware | str | PathRewrite, Context | Path without vendor prefix |
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 context #}
{% if request.state.context_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. PathRewriteMiddleware
↓ Path already clean (no rewrite needed for subdomain mode)
↓ request.scope['path'] = "/shop/products"
4. ContextDetectionMiddleware
↓ Analyzes path: "/shop/products"
↓ Has vendor: Yes
↓ Not admin/api/vendor dashboard
↓ Sets: request.state.context_type = RequestContext.SHOP
5. ThemeContextMiddleware
↓ Loads theme for vendor_id = 1
↓ Sets: request.state.theme = {...theme config...}
6. FastAPI Router
↓ Matches route: @app.get("/shop/products")
↓ Calls handler function
7. 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
ContextDetectionMiddleware
- If clean_path missing: Uses original path
- If vendor missing: Defaults to FALLBACK context
- Always sets a context_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(ContextDetectionMiddleware)
app.add_middleware(VendorContextMiddleware)
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