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>
568 lines
13 KiB
Markdown
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
|
|
```
|