421 lines
12 KiB
Markdown
421 lines
12 KiB
Markdown
# 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-Time` header 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/`)
|
|
- 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_path` for 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](multi-tenant.md) 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. 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/*` paths
|
|
- `ADMIN` - `/admin/*` paths or `admin.*` subdomain
|
|
- `VENDOR_DASHBOARD` - `/vendor/*` paths (management area)
|
|
- `SHOP` - Storefront pages (has vendor + not admin/vendor/API)
|
|
- `FALLBACK` - Unknown context
|
|
- Injects `request.state.context_type`
|
|
|
|
**Detection Rules**:
|
|
```python
|
|
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
|
|
|
|
### 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**:
|
|
```python
|
|
{
|
|
"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)
|
|
|
|
```mermaid
|
|
graph TD
|
|
A[Client Request] --> B[1. LoggingMiddleware]
|
|
B --> C[2. VendorContextMiddleware]
|
|
C --> D[3. ContextDetectionMiddleware]
|
|
D --> E[4. ThemeContextMiddleware]
|
|
E --> F[5. FastAPI Router]
|
|
F --> G[Route Handler]
|
|
G --> H[Response]
|
|
H --> I[Client]
|
|
```
|
|
|
|
### Why This Order Matters
|
|
|
|
**Critical Dependencies**:
|
|
|
|
1. **LoggingMiddleware first**
|
|
- Needs to wrap everything to measure total time
|
|
- Must log errors from all other middleware
|
|
|
|
2. **VendorContextMiddleware second**
|
|
- Must run before ContextDetectionMiddleware (provides vendor and clean_path)
|
|
- Must run before ThemeContextMiddleware (provides vendor_id)
|
|
|
|
3. **ContextDetectionMiddleware third**
|
|
- Uses clean_path from VendorContextMiddleware
|
|
- Provides context_type for ThemeContextMiddleware
|
|
|
|
4. **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.
|
|
|
|
## 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 | 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
|
|
|
|
```python
|
|
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
|
|
|
|
```jinja2
|
|
{# 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. ContextDetectionMiddleware
|
|
↓ Analyzes path: "/shop/products"
|
|
↓ Has vendor: Yes
|
|
↓ Not admin/api/vendor dashboard
|
|
↓ Sets: request.state.context_type = RequestContext.SHOP
|
|
|
|
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
|
|
|
|
### 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`:
|
|
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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](../testing/testing-guide.md)
|
|
|
|
## Debugging Middleware
|
|
|
|
### Enable Debug Logging
|
|
|
|
```python
|
|
import logging
|
|
|
|
logging.getLogger("middleware").setLevel(logging.DEBUG)
|
|
```
|
|
|
|
### Check Request State
|
|
|
|
In route handlers:
|
|
|
|
```python
|
|
@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](multi-tenant.md) - Detailed routing modes
|
|
- [Request Flow](request-flow.md) - Complete request journey
|
|
- [Authentication & RBAC](auth-rbac.md) - Security middleware
|
|
- [Backend API Reference](../backend/middleware-reference.md) - Technical API docs
|
|
- [Frontend Development](../frontend/overview.md) - Using middleware state in frontend
|
|
|
|
## Technical Reference
|
|
|
|
For detailed API documentation of middleware classes and methods, see:
|
|
- [Backend Middleware Reference](../backend/middleware-reference.md)
|
|
|
|
This includes:
|
|
- Complete class documentation
|
|
- Method signatures
|
|
- Parameter details
|
|
- Return types
|
|
- Auto-generated from source code
|