Files
orion/docs/architecture/request-flow.md
Samir Boulahtit d648c921b7
Some checks failed
CI / ruff (push) Successful in 10s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled
docs: add consolidated dev URL reference and migrate /shop to /storefront
- Add Development URL Quick Reference section to url-routing overview
  with all login URLs, entry points, and full examples
- Replace /shop/ path segments with /storefront/ across 50 docs files
- Update file references: shop_pages.py → storefront_pages.py,
  templates/shop/ → templates/storefront/, api/v1/shop/ → api/v1/storefront/
- Preserve domain references (orion.shop) and /store/ staff dashboard paths
- Archive docs left unchanged (historical)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 13:23:44 +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/storefront/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 /storefront/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 = "/storefront/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 = "/storefront/products" # Already clean
```
**Request State After**:
```python
request.state.store = <Store: Orion>
request.state.store_id = 1
request.state.clean_path = "/storefront/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:
- `/storefront/*` for subdomain/custom domain
- `/stores/{store_code}/storefront/*` for path-based development
**Example** (Path-Based Mode):
```python
# In main.py - Double router mounting
app.include_router(storefront_pages.router, prefix="/storefront")
app.include_router(storefront_pages.router, prefix="/stores/{store_code}/storefront")
# Request: /stores/ORION/storefront/products
# Matches: Second router (/stores/{store_code}/storefront)
# 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 # "/storefront/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): "/storefront/products"
# Matches this route
@app.get("/storefront/products")
async def get_storefront_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/storefront/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 /storefront/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
```
### Storefront 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 /storefront/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 Storefront 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 storefront 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: "/storefront/products"
}
After FrontendTypeMiddleware:
{
store: <Store: Orion>,
store_id: 1,
clean_path: "/storefront/products",
frontend_type: FrontendType.STOREFRONT
}
After ThemeContextMiddleware:
{
store: <Store: Orion>,
store_id: 1,
clean_path: "/storefront/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
```