# 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 = 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 #}

{{ request.state.vendor.name }}

{# Access theme #} {# Access context #} {% if request.state.context_type.value == "admin" %}
Admin Mode
{% 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 = ↓ 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