Files
orion/docs/architecture/request-flow.md
Samir Boulahtit b769f5a047 refactor: centralize frontend detection with FrontendDetector
Major architecture change to unify frontend detection:

## Problem Solved
- Eliminated code duplication across 3 middleware files
- Fixed incomplete path detection (now detects /api/v1/admin/*)
- Unified on FrontendType enum (deprecates RequestContext)
- Added request.state.frontend_type for all requests

## New Components
- app/core/frontend_detector.py: Centralized FrontendDetector class
- middleware/frontend_type.py: FrontendTypeMiddleware (replaces ContextMiddleware)
- docs/architecture/frontend-detection.md: Complete architecture documentation

## Changes
- main.py: Use FrontendTypeMiddleware instead of ContextMiddleware
- middleware/context.py: Deprecated (kept for backwards compatibility)
- middleware/platform_context.py: Use FrontendDetector.is_admin()
- middleware/vendor_context.py: Use FrontendDetector.is_admin()
- middleware/language.py: Use FrontendType instead of context_value
- app/exceptions/handler.py: Use FrontendType.STOREFRONT
- app/exceptions/error_renderer.py: Use FrontendType
- Customer routes: Cookie path changed from /shop to /storefront

## Documentation
- docs/architecture/frontend-detection.md: New comprehensive docs
- docs/architecture/middleware.md: Updated for new system
- docs/architecture/request-flow.md: Updated for FrontendType
- docs/backend/middleware-reference.md: Updated API reference

## Tests
- tests/unit/core/test_frontend_detector.py: 37 new tests
- tests/unit/middleware/test_frontend_type.py: 11 new tests
- tests/unit/middleware/test_context.py: Updated for compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 16:15:19 +01:00

13 KiB

Request Flow

Complete journey of a request through the Wizamart platform, from client to response.

Overview

This document traces how requests flow through the multi-tenant system, showing the path through middleware, routing, and response generation.

High-Level Flow

graph TB
    A[Client Request] --> B{Request Type?}

    B -->|API| C[REST API Flow]
    B -->|HTML Page| D[Page Rendering Flow]

    C --> E[Middleware Stack]
    D --> E

    E --> F[Vendor Detection]
    F --> G[Context Detection]
    G --> H[Theme Loading]
    H --> I[Router]

    I -->|API| J[API Handler]
    I -->|Page| K[Route Handler]

    J --> L[JSON Response]
    K --> M[Jinja2 Template]
    M --> N[HTML Response]

    L --> O[Client]
    N --> O

Detailed Request Flow

1. Client Sends Request

Example Requests:

# Shop page request (subdomain mode)
GET https://wizamart.platform.com/shop/products
Host: wizamart.platform.com

# API request
GET https://platform.com/api/v1/products?vendor_id=1
Authorization: Bearer eyJ0eXAi...
Host: platform.com

# Admin page request
GET https://platform.com/admin/vendors
Authorization: Bearer eyJ0eXAi...
Host: platform.com

2. LoggingMiddleware (Entry Point)

What happens:

  • Request enters the application
  • Timer starts
  • Request logged with method, path, client IP

Request State:

# Start time recorded
start_time = time.time()

# Log entry
logger.info(f"Request: GET /shop/products from 192.168.1.100")

Output: Nothing added to request.state yet

3. VendorContextMiddleware

What happens:

  • Analyzes host header and path
  • Determines routing mode (custom domain / subdomain / path-based)
  • Queries database for vendor
  • Extracts clean path

Example Processing (Subdomain Mode):

# Input
host = "wizamart.platform.com"
path = "/shop/products"

# Detection logic
if host != settings.platform_domain:
    # Subdomain detected
    vendor_code = host.split('.')[0]  # "wizamart"

    # Query database
    vendor = db.query(Vendor).filter(
        Vendor.code == vendor_code
    ).first()

    # Set request state
    request.state.vendor = vendor
    request.state.vendor_id = vendor.id
    request.state.clean_path = "/shop/products"  # Already clean

Request State After:

request.state.vendor = <Vendor: Wizamart>
request.state.vendor_id = 1
request.state.clean_path = "/shop/products"

4. Router Matching (FastAPI Native)

What happens:

  • FastAPI matches the request path against registered routes
  • For path-based development mode, routes are registered with two prefixes:
    • /shop/* for subdomain/custom domain
    • /vendors/{vendor_code}/shop/* for path-based development

Example (Path-Based Mode):

# In main.py - Double router mounting
app.include_router(shop_pages.router, prefix="/shop")
app.include_router(shop_pages.router, prefix="/vendors/{vendor_code}/shop")

# Request: /vendors/WIZAMART/shop/products
# Matches: Second router (/vendors/{vendor_code}/shop)
# Route: @router.get("/products")
# vendor_code available as path parameter = "WIZAMART"

Note: Previous implementations used PathRewriteMiddleware to rewrite paths. This has been replaced with FastAPI's native routing via double router mounting.

Request State After: No changes to state, but internal path updated

5. ContextDetectionMiddleware

What happens:

  • Analyzes the clean path
  • Determines request context type
  • Sets context in request state

Detection Logic:

host = request.headers.get("host", "")
path = request.state.clean_path  # "/shop/products"
has_vendor = hasattr(request.state, 'vendor') and request.state.vendor

# FrontendDetector handles all detection logic centrally
frontend_type = FrontendDetector.detect(host, path, has_vendor)
# Returns: FrontendType.STOREFRONT  # ← Our example

request.state.frontend_type = frontend_type

Request State After:

request.state.frontend_type = FrontendType.STOREFRONT

Note

: Detection logic is centralized in app/core/frontend_detector.py. See Frontend Detection Architecture for details.

6. ThemeContextMiddleware

What happens:

  • Checks if request has a vendor
  • Loads theme configuration from database
  • Injects theme into request state

Theme Loading:

if hasattr(request.state, 'vendor_id'):
    theme = db.query(VendorTheme).filter(
        VendorTheme.vendor_id == request.state.vendor_id
    ).first()

    request.state.theme = {
        "primary_color": theme.primary_color,
        "secondary_color": theme.secondary_color,
        "logo_url": theme.logo_url,
        "custom_css": theme.custom_css
    }

Request State After:

request.state.theme = {
    "primary_color": "#3B82F6",
    "secondary_color": "#10B981",
    "logo_url": "/static/vendors/wizamart/logo.png",
    "custom_css": "..."
}

7. FastAPI Router

What happens:

  • Request reaches FastAPI's router
  • Router matches path to registered route
  • Route dependencies are resolved
  • Handler function is called

Route Matching:

# Request path (after rewrite): "/shop/products"

# Matches this route
@app.get("/shop/products")
async def get_shop_products(request: Request):
    # Handler code
    pass

8. Route Handler Execution

Example Handler:

# Routes are defined in modules: app/modules/<module>/routes/pages/storefront.py

@router.get("/products")
async def shop_products_page(
    request: Request,
    db: Session = Depends(get_db)
):
    # Access vendor from request state
    vendor = request.state.vendor
    vendor_id = request.state.vendor_id

    # Query products for this vendor
    products = db.query(Product).filter(
        Product.vendor_id == vendor_id
    ).all()

    # Render template with context
    return templates.TemplateResponse(
        "shop/products.html",
        {
            "request": request,
            "vendor": vendor,
            "products": products,
            "theme": request.state.theme
        }
    )

9. Template Rendering (Jinja2)

Template (templates/shop/products.html):

<!DOCTYPE html>
<html>
<head>
    <title>{{ vendor.name }} - Products</title>
    <style>
        :root {
            --primary-color: {{ theme.primary_color }};
            --secondary-color: {{ theme.secondary_color }};
        }
    </style>
</head>
<body>
    <h1>{{ vendor.name }} Shop</h1>

    <div class="products">
        {% for product in products %}
        <div class="product-card">
            <h2>{{ product.name }}</h2>
            <p>{{ product.price }}</p>
        </div>
        {% endfor %}
    </div>
</body>
</html>

Rendered HTML:

<!DOCTYPE html>
<html>
<head>
    <title>Wizamart - Products</title>
    <style>
        :root {
            --primary-color: #3B82F6;
            --secondary-color: #10B981;
        }
    </style>
</head>
<body>
    <h1>Wizamart Shop</h1>
    <div class="products">
        <div class="product-card">
            <h2>Product 1</h2>
            <p>$29.99</p>
        </div>
        <!-- More products... -->
    </div>
</body>
</html>

10. Response Sent Back

LoggingMiddleware (Response Phase):

  • Calculates total request time
  • Logs response status and duration
  • Adds performance header

Logging:

duration = time.time() - start_time  # 0.143 seconds

logger.info(
    f"Response: 200 for GET /shop/products (0.143s)"
)

# Add header
response.headers["X-Process-Time"] = "0.143"

Final Response:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
X-Process-Time: 0.143
Content-Length: 2847

<!DOCTYPE html>
<html>
...
</html>

Flow Diagrams by Request Type

API Request Flow

sequenceDiagram
    participant Client
    participant Logging
    participant Vendor
    participant Context
    participant Router
    participant Handler
    participant DB

    Client->>Logging: GET /api/v1/products?vendor_id=1
    Logging->>Vendor: Pass request
    Note over Vendor: No vendor detection<br/>(API uses query param)
    Vendor->>Context: Pass request
    Context->>Context: Detect API context
    Note over Context: frontend_type = API (via FrontendDetector)
    Context->>Router: Route request
    Router->>Handler: Call API handler
    Handler->>DB: Query products
    DB-->>Handler: Product data
    Handler-->>Router: JSON response
    Router-->>Client: {products: [...]}

Admin Page Flow

sequenceDiagram
    participant Client
    participant Logging
    participant Vendor
    participant Context
    participant Theme
    participant Router
    participant Handler
    participant Template

    Client->>Logging: GET /admin/vendors
    Logging->>Vendor: Pass request
    Note over Vendor: No vendor<br/>(Admin area)
    Vendor->>Context: Pass request
    Context->>Context: Detect Admin context
    Note over Context: frontend_type = ADMIN
    Context->>Theme: Pass request
    Note over Theme: Skip theme<br/>(No vendor)
    Theme->>Router: Route request
    Router->>Handler: Call handler
    Handler->>Template: Render admin template
    Template-->>Client: Admin HTML page

Shop Page Flow (Full Multi-Tenant)

sequenceDiagram
    participant Client
    participant Logging
    participant Vendor
    participant Path
    participant Context
    participant Theme
    participant Router
    participant Handler
    participant DB
    participant Template

    Client->>Logging: GET /shop/products<br/>Host: wizamart.platform.com
    Logging->>Vendor: Pass request
    Vendor->>DB: Query vendor by subdomain
    DB-->>Vendor: Vendor object
    Note over Vendor: Set vendor, vendor_id, clean_path
    Vendor->>Path: Pass request
    Note over Path: Path already clean
    Path->>Context: Pass request
    Context->>Context: Detect Shop context
    Note over Context: frontend_type = STOREFRONT
    Context->>Theme: Pass request
    Theme->>DB: Query theme
    DB-->>Theme: Theme config
    Note over Theme: Set theme in request.state
    Theme->>Router: Route request
    Router->>Handler: Call handler
    Handler->>DB: Query products for vendor
    DB-->>Handler: Product list
    Handler->>Template: Render with theme
    Template-->>Client: Themed shop HTML

Request State Timeline

Showing how request.state is built up through the middleware stack:

Initial State: {}

After VendorContextMiddleware:
{
    vendor: <Vendor: Wizamart>,
    vendor_id: 1,
    clean_path: "/shop/products"
}

After FrontendTypeMiddleware:
{
    vendor: <Vendor: Wizamart>,
    vendor_id: 1,
    clean_path: "/shop/products",
    frontend_type: FrontendType.STOREFRONT
}

After ThemeContextMiddleware:
{
    vendor: <Vendor: Wizamart>,
    vendor_id: 1,
    clean_path: "/shop/products",
    frontend_type: FrontendType.STOREFRONT,
    theme: {
        primary_color: "#3B82F6",
        secondary_color: "#10B981",
        logo_url: "/static/vendors/wizamart/logo.png",
        custom_css: "..."
    }
}

Performance Metrics

Typical request timings:

Component Time Percentage
Middleware Stack 5ms 3%
- VendorContextMiddleware 2ms 1%
- FrontendTypeMiddleware <1ms <1%
- ThemeContextMiddleware 2ms 1%
Database Queries 15ms 10%
Business Logic 50ms 35%
Template Rendering 75ms 52%
Total 145ms 100%

Error Handling in Flow

Middleware Errors

If middleware encounters an error:

try:
    # Middleware logic
    vendor = detect_vendor(request)
except Exception as e:
    logger.error(f"Vendor detection failed: {e}")
    # Set default/None
    request.state.vendor = None
    # Continue to next middleware

Handler Errors

If route handler raises an exception:

try:
    response = await handler(request)
except HTTPException as e:
    # FastAPI handles HTTP exceptions
    return error_response(e.status_code, e.detail)
except Exception as e:
    # Custom exception handler
    logger.error(f"Handler error: {e}")
    return error_response(500, "Internal Server Error")

Debugging Request Flow

Enable Request Logging

import logging

logging.getLogger("middleware").setLevel(logging.DEBUG)
logging.getLogger("fastapi").setLevel(logging.DEBUG)

Add Debug Endpoint

@app.get("/debug/request-state")
async def debug_state(request: Request):
    return {
        "path": request.url.path,
        "host": request.headers.get("host"),
        "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),
        "frontend_type": request.state.frontend_type.value if hasattr(request.state, 'frontend_type') else None,
        "has_theme": bool(getattr(request.state, 'theme', None))
    }

Check Middleware Order

In main.py, middleware registration order is critical:

# REVERSE order (Last In, First Out)
app.add_middleware(LoggingMiddleware)         # Runs first
app.add_middleware(ThemeContextMiddleware)    # Runs sixth
app.add_middleware(LanguageMiddleware)        # Runs fifth
app.add_middleware(FrontendTypeMiddleware)    # Runs fourth
app.add_middleware(VendorContextMiddleware)   # Runs second