Some checks failed
- 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>
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/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
|
|
```
|