13 KiB
Request Flow
Complete journey of a request through the Wizamart 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[Vendor 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://wizamart.platform.com/shop/products
Host: wizamart.platform.com
# API request
GET https://platform.com/api/v1/products?vendor_id=1
Authorization: Bearer eyJ0eXAi...
Host: platform.com
# Admin page request
GET https://platform.com/admin/vendors
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. VendorContextMiddleware
What happens:
- Analyzes host header and path
- Determines routing mode (custom domain / subdomain / path-based)
- Queries database for vendor
- Extracts clean path
Example Processing (Subdomain Mode):
# Input
host = "wizamart.platform.com"
path = "/shop/products"
# Detection logic
if host != settings.platform_domain:
# Subdomain detected
vendor_code = host.split('.')[0] # "wizamart"
# Query database
vendor = db.query(Vendor).filter(
Vendor.code == vendor_code
).first()
# Set request state
request.state.vendor = vendor
request.state.vendor_id = vendor.id
request.state.clean_path = "/shop/products" # Already clean
Request State After:
request.state.vendor = <Vendor: Wizamart>
request.state.vendor_id = 1
request.state.clean_path = "/shop/products"
4. PathRewriteMiddleware
What happens:
- Checks if
clean_pathis different from original path - If different, rewrites the request path for FastAPI routing
Example (Path-Based Mode):
# Input (path-based mode)
original_path = "/vendor/WIZAMART/shop/products"
clean_path = "/shop/products" # From VendorContextMiddleware
# Path rewrite
if clean_path != original_path:
request.scope['path'] = clean_path
request._url = request.url.replace(path=clean_path)
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:
path = request.state.clean_path # "/shop/products"
if path.startswith("/api/"):
context = RequestContext.API
elif path.startswith("/admin/"):
context = RequestContext.ADMIN
elif path.startswith("/vendor/"):
context = RequestContext.VENDOR_DASHBOARD
elif hasattr(request.state, 'vendor') and request.state.vendor:
context = RequestContext.SHOP # ← Our example
else:
context = RequestContext.FALLBACK
request.state.context_type = context
Request State After:
request.state.context_type = RequestContext.SHOP
6. ThemeContextMiddleware
What happens:
- Checks if request has a vendor
- Loads theme configuration from database
- Injects theme into request state
Theme Loading:
if hasattr(request.state, 'vendor_id'):
theme = db.query(VendorTheme).filter(
VendorTheme.vendor_id == request.state.vendor_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/vendors/wizamart/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:
from app.routes import shop_pages
@router.get("/shop/products")
async def shop_products_page(
request: Request,
db: Session = Depends(get_db)
):
# Access vendor from request state
vendor = request.state.vendor
vendor_id = request.state.vendor_id
# Query products for this vendor
products = db.query(Product).filter(
Product.vendor_id == vendor_id
).all()
# Render template with context
return templates.TemplateResponse(
"shop/products.html",
{
"request": request,
"vendor": vendor,
"products": products,
"theme": request.state.theme
}
)
9. Template Rendering (Jinja2)
Template (templates/shop/products.html):
<!DOCTYPE html>
<html>
<head>
<title>{{ vendor.name }} - Products</title>
<style>
:root {
--primary-color: {{ theme.primary_color }};
--secondary-color: {{ theme.secondary_color }};
}
</style>
</head>
<body>
<h1>{{ vendor.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>Wizamart - Products</title>
<style>
:root {
--primary-color: #3B82F6;
--secondary-color: #10B981;
}
</style>
</head>
<body>
<h1>Wizamart 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 Vendor
participant Context
participant Router
participant Handler
participant DB
Client->>Logging: GET /api/v1/products?vendor_id=1
Logging->>Vendor: Pass request
Note over Vendor: No vendor detection<br/>(API uses query param)
Vendor->>Context: Pass request
Context->>Context: Detect API context
Note over Context: context_type = API
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 Vendor
participant Context
participant Theme
participant Router
participant Handler
participant Template
Client->>Logging: GET /admin/vendors
Logging->>Vendor: Pass request
Note over Vendor: No vendor<br/>(Admin area)
Vendor->>Context: Pass request
Context->>Context: Detect Admin context
Note over Context: context_type = ADMIN
Context->>Theme: Pass request
Note over Theme: Skip theme<br/>(No vendor)
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 Vendor
participant Path
participant Context
participant Theme
participant Router
participant Handler
participant DB
participant Template
Client->>Logging: GET /shop/products<br/>Host: wizamart.platform.com
Logging->>Vendor: Pass request
Vendor->>DB: Query vendor by subdomain
DB-->>Vendor: Vendor object
Note over Vendor: Set vendor, vendor_id, clean_path
Vendor->>Path: Pass request
Note over Path: Path already clean
Path->>Context: Pass request
Context->>Context: Detect Shop context
Note over Context: context_type = SHOP
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 vendor
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 VendorContextMiddleware:
{
vendor: <Vendor: Wizamart>,
vendor_id: 1,
clean_path: "/shop/products"
}
After ContextDetectionMiddleware:
{
vendor: <Vendor: Wizamart>,
vendor_id: 1,
clean_path: "/shop/products",
context_type: RequestContext.SHOP
}
After ThemeContextMiddleware:
{
vendor: <Vendor: Wizamart>,
vendor_id: 1,
clean_path: "/shop/products",
context_type: RequestContext.SHOP,
theme: {
primary_color: "#3B82F6",
secondary_color: "#10B981",
logo_url: "/static/vendors/wizamart/logo.png",
custom_css: "..."
}
}
Performance Metrics
Typical request timings:
| Component | Time | Percentage |
|---|---|---|
| Middleware Stack | 5ms | 3% |
| - VendorContextMiddleware | 2ms | 1% |
| - ContextDetectionMiddleware | <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
vendor = detect_vendor(request)
except Exception as e:
logger.error(f"Vendor detection failed: {e}")
# Set default/None
request.state.vendor = 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"),
"vendor": request.state.vendor.name if hasattr(request.state, 'vendor') else None,
"vendor_id": getattr(request.state, 'vendor_id', None),
"clean_path": getattr(request.state, 'clean_path', None),
"context_type": request.state.context_type.value if hasattr(request.state, 'context_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 fifth
app.add_middleware(ContextDetectionMiddleware) # Runs fourth
app.add_middleware(VendorContextMiddleware) # Runs second