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

568 lines
13 KiB
Markdown

# 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
```mermaid
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**:
```http
# 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**:
```python
# 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):
```python
# 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**:
```python
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):
```python
# 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**:
```python
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**:
```python
request.state.frontend_type = FrontendType.STOREFRONT
```
> **Note**: Detection logic is centralized in `app/core/frontend_detector.py`.
> See [Frontend Detection Architecture](frontend-detection.md) for details.
### 6. ThemeContextMiddleware
**What happens**:
- Checks if request has a store
- Loads theme configuration from database
- Injects theme into request state
**Theme Loading**:
```python
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**:
```python
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**:
```python
# 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**:
```python
# 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`):
```jinja2
<!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**:
```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**:
```python
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
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
X-Process-Time: 0.143
Content-Length: 2847
```
## Flow Diagrams by Request Type
### API Request Flow
```mermaid
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
```mermaid
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)
```mermaid
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:
```python
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:
```python
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](middleware.md) - Detailed middleware documentation
- [Multi-Tenant System](multi-tenant.md) - Tenant routing modes
- [Authentication & RBAC](auth-rbac.md) - Security flow
- [Architecture Overview](overview.md) - System architecture
## Debugging Request Flow
### Enable Request Logging
```python
import logging
logging.getLogger("middleware").setLevel(logging.DEBUG)
logging.getLogger("fastapi").setLevel(logging.DEBUG)
```
### Add Debug Endpoint
```python
@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:
```python
# 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
```