# 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 `main` platform for localhost without prefix - 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 = ``` **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-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. Frontend Type Detection Middleware **Purpose**: Determine which frontend the request targets **What it does**: - Uses centralized `FrontendDetector` class for all detection logic - Determines which frontend is being accessed: - `ADMIN` - `/admin/*`, `/api/v1/admin/*` paths or `admin.*` subdomain - `VENDOR` - `/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`): ```python 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](frontend-detection.md) 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**: ```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 ## 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: ```python # 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.py` with `ContextMiddleware` and `RequestContext` is deprecated. > Use `FrontendTypeMiddleware` and `FrontendType` enum instead. > See [Frontend Detection Architecture](frontend-detection.md) 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: ```python # ✅ 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](../development/naming-conventions.md) for project-wide standards. ## Middleware Execution Order ### The Stack (First to Last) ```mermaid 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**: 1. **LoggingMiddleware first** - Needs to wrap everything to measure total time - Must log errors from all other middleware 2. **PlatformContextMiddleware second** - Must run before VendorContextMiddleware (sets platform context) - Rewrites path for `/platforms/{code}/` prefixed requests - Sets `request.state.platform` for downstream middleware 3. **VendorContextMiddleware third** - Uses rewritten path from PlatformContextMiddleware - Must run before ContextDetectionMiddleware (provides vendor and clean_path) - Must run before ThemeContextMiddleware (provides vendor_id) 4. **ContextDetectionMiddleware fourth** - Uses clean_path from VendorContextMiddleware - Provides context_type for ThemeContextMiddleware 5. **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 ```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 frontend type #} {% if request.state.frontend_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. 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`: ```python # 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: ```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