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>
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")
Related Documentation
- Middleware Stack - Detailed middleware documentation
- Multi-Tenant System - Tenant routing modes
- Authentication & RBAC - Security flow
- Architecture Overview - System architecture
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