Files
orion/docs/architecture/request-flow.md
Samir Boulahtit e9253fbd84 refactor: rename Wizamart to Orion across entire codebase
Replace all ~1,086 occurrences of Wizamart/wizamart/WIZAMART/WizaMart
with Orion/orion/ORION across 184 files. This includes database
identifiers, email addresses, domain references, R2 bucket names,
DNS prefixes, encryption salt, Celery app name, config defaults,
Docker configs, CI configs, documentation, seed data, and templates.

Renames homepage-wizamart.html template to homepage-orion.html.
Fixes duplicate file_pattern key in api.yaml architecture rule.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 16:46:56 +01:00

13 KiB

Request Flow

Complete journey of a request through the Orion 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[Store 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://orion.platform.com/shop/products
Host: orion.platform.com

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

# Admin page request
GET https://platform.com/admin/stores
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. StoreContextMiddleware

What happens:

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

Example Processing (Subdomain Mode):

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

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

    # Query database
    store = db.query(Store).filter(
        Store.code == store_code
    ).first()

    # Set request state
    request.state.store = store
    request.state.store_id = store.id
    request.state.clean_path = "/shop/products"  # Already clean

Request State After:

request.state.store = <Store: Orion>
request.state.store_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
    • /stores/{store_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="/stores/{store_code}/shop")

# Request: /stores/ORION/shop/products
# Matches: Second router (/stores/{store_code}/shop)
# Route: @router.get("/products")
# store_code available as path parameter = "ORION"

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_store = hasattr(request.state, 'store') and request.state.store

# FrontendDetector handles all detection logic centrally
frontend_type = FrontendDetector.detect(host, path, has_store)
# 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 store
  • Loads theme configuration from database
  • Injects theme into request state

Theme Loading:

if hasattr(request.state, 'store_id'):
    theme = db.query(StoreTheme).filter(
        StoreTheme.store_id == request.state.store_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/stores/orion/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 store from request state
    store = request.state.store
    store_id = request.state.store_id

    # Query products for this store
    products = db.query(Product).filter(
        Product.store_id == store_id
    ).all()

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

9. Template Rendering (Jinja2)

Template (templates/shop/products.html):

<!DOCTYPE html>
<html>
<head>
    <title>{{ store.name }} - Products</title>
    <style>
        :root {
            --primary-color: {{ theme.primary_color }};
            --secondary-color: {{ theme.secondary_color }};
        }
    </style>
</head>
<body>
    <h1>{{ store.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>Orion - Products</title>
    <style>
        :root {
            --primary-color: #3B82F6;
            --secondary-color: #10B981;
        }
    </style>
</head>
<body>
    <h1>Orion 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 Store
    participant Context
    participant Router
    participant Handler
    participant DB

    Client->>Logging: GET /api/v1/products?store_id=1
    Logging->>Store: Pass request
    Note over Store: No store detection<br/>(API uses query param)
    Store->>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 Store
    participant Context
    participant Theme
    participant Router
    participant Handler
    participant Template

    Client->>Logging: GET /admin/stores
    Logging->>Store: Pass request
    Note over Store: No store<br/>(Admin area)
    Store->>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 store)
    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 Store
    participant Path
    participant Context
    participant Theme
    participant Router
    participant Handler
    participant DB
    participant Template

    Client->>Logging: GET /shop/products<br/>Host: orion.platform.com
    Logging->>Store: Pass request
    Store->>DB: Query store by subdomain
    DB-->>Store: Store object
    Note over Store: Set store, store_id, clean_path
    Store->>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 store
    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 StoreContextMiddleware:
{
    store: <Store: Orion>,
    store_id: 1,
    clean_path: "/shop/products"
}

After FrontendTypeMiddleware:
{
    store: <Store: Orion>,
    store_id: 1,
    clean_path: "/shop/products",
    frontend_type: FrontendType.STOREFRONT
}

After ThemeContextMiddleware:
{
    store: <Store: Orion>,
    store_id: 1,
    clean_path: "/shop/products",
    frontend_type: FrontendType.STOREFRONT,
    theme: {
        primary_color: "#3B82F6",
        secondary_color: "#10B981",
        logo_url: "/static/stores/orion/logo.png",
        custom_css: "..."
    }
}

Performance Metrics

Typical request timings:

Component Time Percentage
Middleware Stack 5ms 3%
- StoreContextMiddleware 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
    store = detect_store(request)
except Exception as e:
    logger.error(f"Store detection failed: {e}")
    # Set default/None
    request.state.store = 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"),
        "store": request.state.store.name if hasattr(request.state, 'store') else None,
        "store_id": getattr(request.state, 'store_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(StoreContextMiddleware)   # Runs second