# 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 - Checks platform domain guards: if host matches `platform.domain`, Methods 1 & 2 are skipped; if host is a subdomain of `platform.domain`, Method 1 is skipped - Determines routing mode (custom domain / subdomain / path-based) - Queries database for store - Extracts clean path **Example Processing** (Subdomain Mode — Two-Step Lookup): ```python # Input host = "wizatech-rewards.rewardflow.lu" path = "/storefront/products" # Detection logic — two-step subdomain lookup subdomain = host.split('.')[0] # "wizatech-rewards" # Step 1: Check per-platform custom subdomain (StorePlatform.custom_subdomain) store_platform = db.query(StorePlatform).filter( StorePlatform.custom_subdomain == subdomain ).first() if store_platform: store = store_platform.store else: # Step 2: Fall back to standard Store.subdomain store = db.query(Store).filter( Store.subdomain == subdomain ).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 = 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 - Both storefront and store dashboard routes are registered with two prefixes each: - Storefront: `/storefront/*` and `/storefront/{store_code}/*` - Store dashboard: `/store/*` and `/store/{store_code}/*` **Example** (Path-Based Mode): ```python # In main.py - Double router mounting for storefront app.include_router(storefront_pages.router, prefix="/storefront") app.include_router(storefront_pages.router, prefix="/storefront/{store_code}") # In main.py - Double router mounting for store dashboard app.include_router(store_pages.router, prefix="/store") app.include_router(store_pages.router, prefix="/store/{store_code}") # Storefront request: /storefront/ACME/products # Matches: Second storefront router (/storefront/{store_code}) # store_code = "ACME" # Store dashboard request: /store/ACME/dashboard # Matches: Second store router (/store/{store_code}) # store_code = "ACME" ``` Route handlers use the `get_resolved_store_code` dependency to transparently resolve the store code from either the path parameter (dev mode) or middleware state (production subdomain/custom domain). **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//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 {{ store.name }} - Products

{{ store.name }} Shop

{% for product in products %}

{{ product.name }}

{{ product.price }}

{% endfor %}
``` **Rendered HTML**: ```html Orion - Products

Orion Shop

Product 1

$29.99

``` ### 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
(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
(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
(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
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_id: 1, clean_path: "/storefront/products" } After FrontendTypeMiddleware: { store: , store_id: 1, clean_path: "/storefront/products", frontend_type: FrontendType.STOREFRONT } After ThemeContextMiddleware: { store: , 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 ```