mockforge_http/consistency/
middleware.rs

1//! Consistency middleware for HTTP
2//!
3//! This middleware ensures HTTP requests/responses use the unified state
4//! from the consistency engine (persona, scenario, reality level, etc.)
5
6use crate::consistency::HttpAdapter;
7use axum::{body::Body, extract::Request, http::Response, middleware::Next};
8use mockforge_core::consistency::ConsistencyEngine;
9use mockforge_core::request_logger::RealityTraceMetadata;
10use std::sync::Arc;
11use tracing::debug;
12
13/// Consistency middleware state
14#[derive(Clone)]
15pub struct ConsistencyMiddlewareState {
16    /// Consistency engine
17    pub engine: Arc<ConsistencyEngine>,
18    /// HTTP adapter
19    pub adapter: Arc<HttpAdapter>,
20    /// X-Ray state for request context storage (optional)
21    pub xray_state: Option<std::sync::Arc<crate::handlers::xray::XRayState>>,
22}
23
24/// Consistency middleware
25///
26/// This middleware:
27/// 1. Extracts workspace ID from request (header, query param, or default)
28/// 2. Gets unified state from consistency engine
29/// 3. Inserts state into request extensions for handlers to use
30/// 4. Ensures responses reflect the unified state
31pub async fn consistency_middleware(req: Request, next: Next) -> Response<Body> {
32    // Extract workspace ID from request
33    // Priority: X-MockForge-Workspace header > query param > default
34    let workspace_id = req
35        .headers()
36        .get("X-MockForge-Workspace")
37        .and_then(|h| h.to_str().ok())
38        .map(|s| s.to_string())
39        .or_else(|| {
40            req.uri().query().and_then(|q| {
41                q.split('&').find_map(|pair| {
42                    let mut parts = pair.splitn(2, '=');
43                    if parts.next() == Some("workspace") {
44                        parts.next().and_then(|v| {
45                            urlencoding::decode(v).ok().map(|decoded| decoded.to_string())
46                        })
47                    } else {
48                        None
49                    }
50                })
51            })
52        })
53        .unwrap_or_else(|| "default".to_string());
54
55    // Get state from extensions (set by router)
56    let state = req.extensions().get::<ConsistencyMiddlewareState>();
57
58    if let Some(state) = state {
59        // Get unified state for workspace
60        if let Some(unified_state) = state.engine.get_state(&workspace_id).await {
61            // Extract values for headers before moving unified_state
62            let persona_id = unified_state.active_persona.as_ref().map(|p| p.id.clone());
63            let scenario_id = unified_state.active_scenario.clone();
64            let reality_level = unified_state.reality_level.value();
65            let reality_ratio = unified_state.reality_continuum_ratio;
66            // Note: ChaosScenario is now serde_json::Value, so we extract the name field
67            let chaos_rules: Vec<String> = unified_state
68                .active_chaos_rules
69                .iter()
70                .filter_map(|r| r.get("name").and_then(|v| v.as_str()).map(|s| s.to_string()))
71                .collect();
72            let request_id = uuid::Uuid::new_v4().to_string();
73
74            // Build reality trace metadata from unified state
75            // Use the path from the request URI for path-specific blend ratio calculation
76            let path = req.uri().path();
77
78            // Record reality continuum usage if blend ratio > 0
79            if reality_ratio > 0.0 {
80                mockforge_core::pillar_tracking::record_reality_usage(
81                    Some(workspace_id.clone()),
82                    None,
83                    "blended_reality_ratio",
84                    serde_json::json!({
85                        "ratio": reality_ratio,
86                        "path": path
87                    }),
88                )
89                .await;
90            }
91
92            // Record chaos usage if chaos rules are active
93            if !chaos_rules.is_empty() {
94                mockforge_core::pillar_tracking::record_reality_usage(
95                    Some(workspace_id.clone()),
96                    None,
97                    "chaos_enabled",
98                    serde_json::json!({
99                        "rules": chaos_rules,
100                        "count": chaos_rules.len()
101                    }),
102                )
103                .await;
104            }
105            let reality_metadata =
106                RealityTraceMetadata::from_unified_state(&unified_state, reality_ratio, path);
107
108            // Store request context snapshot if X-Ray state is available
109            if let Some(xray_state) = &state.xray_state {
110                // Clone unified_state before moving it
111                let unified_state_clone = unified_state.clone();
112                let request_id_clone = request_id.clone();
113                let workspace_id_clone = workspace_id.clone();
114
115                // Store snapshot asynchronously (don't block request processing)
116                let xray_state_clone = xray_state.clone();
117                tokio::spawn(async move {
118                    crate::handlers::xray::store_request_context(
119                        &xray_state_clone,
120                        request_id_clone,
121                        workspace_id_clone,
122                        &unified_state_clone,
123                    )
124                    .await;
125                });
126            }
127
128            // Insert unified state and reality metadata into request extensions for handlers
129            let mut req = req;
130            req.extensions_mut().insert(unified_state);
131            req.extensions_mut().insert(reality_metadata);
132
133            // Continue with request processing
134            let mut response = next.run(req).await;
135
136            // Add X-Ray headers to response for browser extension
137            response
138                .headers_mut()
139                .insert("X-MockForge-Workspace", workspace_id.parse().unwrap());
140            response
141                .headers_mut()
142                .insert("X-MockForge-Request-ID", request_id.parse().unwrap());
143            if let Some(ref persona_id) = persona_id {
144                response
145                    .headers_mut()
146                    .insert("X-MockForge-Persona", persona_id.parse().unwrap());
147            }
148            if let Some(ref scenario_id) = scenario_id {
149                response
150                    .headers_mut()
151                    .insert("X-MockForge-Scenario", scenario_id.parse().unwrap());
152            }
153            response
154                .headers_mut()
155                .insert("X-MockForge-Reality-Level", reality_level.to_string().parse().unwrap());
156            response
157                .headers_mut()
158                .insert("X-MockForge-Reality-Ratio", reality_ratio.to_string().parse().unwrap());
159            if !chaos_rules.is_empty() {
160                response
161                    .headers_mut()
162                    .insert("X-MockForge-Chaos-Rules", chaos_rules.join(",").parse().unwrap());
163            }
164
165            return response;
166        } else {
167            debug!("No unified state found for workspace {}", workspace_id);
168        }
169    }
170
171    // Continue without unified state if not available
172    next.run(req).await
173}