Files
orion/docs/architecture/middleware.md

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-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 for routing modes

3. Path Rewrite Middleware

Purpose: Rewrite request paths for proper FastAPI routing

What it does:

  • Uses the clean_path extracted 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/* 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:

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:

  1. LoggingMiddleware first

    • Needs to wrap everything to measure total time
    • Must log errors from all other middleware
  2. VendorContextMiddleware second

    • Must run before PathRewriteMiddleware (provides clean_path)
    • Must run before ContextDetectionMiddleware (provides vendor)
    • Must run before ThemeContextMiddleware (provides vendor_id)
  3. PathRewriteMiddleware third

    • Depends on clean_path from VendorContextMiddleware
    • Must run before ContextDetectionMiddleware (rewrites path)
  4. ContextDetectionMiddleware fourth

    • Uses clean_path from VendorContextMiddleware
    • Uses rewritten path from PathRewriteMiddleware
    • 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!

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

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