Skip to main content

ferro_rs/
server.rs

1use crate::cache::Cache;
2use crate::config::{Config, ServerConfig};
3use crate::container::App;
4use crate::http::{HttpResponse, Request};
5use crate::middleware::{Middleware, MiddlewareChain, MiddlewareRegistry};
6use crate::routing::Router;
7use crate::websocket::handle_ws_upgrade;
8use bytes::Bytes;
9use http_body_util::Full;
10use hyper::server::conn::http1;
11use hyper::service::service_fn;
12use hyper_util::rt::TokioIo;
13use std::convert::Infallible;
14use std::net::SocketAddr;
15use std::sync::Arc;
16use tokio::net::TcpListener;
17
18pub struct Server {
19    router: Arc<Router>,
20    middleware: MiddlewareRegistry,
21    host: String,
22    port: u16,
23}
24
25impl Server {
26    pub fn new(router: impl Into<Router>) -> Self {
27        Self {
28            router: Arc::new(router.into()),
29            middleware: MiddlewareRegistry::new(),
30            host: "127.0.0.1".to_string(),
31            port: 8080,
32        }
33    }
34
35    pub fn from_config(router: impl Into<Router>) -> Self {
36        // Initialize the App container
37        App::init();
38
39        // Boot all auto-registered services from #[service(ConcreteType)]
40        App::boot_services();
41
42        let config = Config::get::<ServerConfig>().unwrap_or_else(ServerConfig::from_env);
43        Self {
44            router: Arc::new(router.into()),
45            // Pull global middleware registered via global_middleware! in bootstrap.rs
46            middleware: MiddlewareRegistry::from_global(),
47            host: config.host,
48            port: config.port,
49        }
50    }
51
52    /// Add global middleware (runs on every request)
53    ///
54    /// For route-specific middleware, use `.middleware(M)` on the route itself.
55    ///
56    /// # Example
57    ///
58    /// ```rust,ignore
59    /// Server::from_config(router)
60    ///     .middleware(LoggingMiddleware)  // Global
61    ///     .middleware(CorsMiddleware)     // Global
62    ///     .run()
63    ///     .await;
64    /// ```
65    pub fn middleware<M: Middleware + 'static>(mut self, middleware: M) -> Self {
66        self.middleware = self.middleware.append(middleware);
67        self
68    }
69
70    pub fn host(mut self, host: &str) -> Self {
71        self.host = host.to_string();
72        self
73    }
74
75    pub fn port(mut self, port: u16) -> Self {
76        self.port = port;
77        self
78    }
79
80    fn get_addr(&self) -> SocketAddr {
81        SocketAddr::new(self.host.parse().unwrap(), self.port)
82    }
83
84    pub async fn run(self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
85        // Bootstrap cache (Redis with in-memory fallback)
86        Cache::bootstrap().await;
87
88        let addr: SocketAddr = self.get_addr();
89        let listener = TcpListener::bind(addr).await?;
90
91        println!("Ferro server running on http://{addr}");
92
93        let router = self.router;
94        let middleware = Arc::new(self.middleware);
95
96        loop {
97            let (stream, _) = listener.accept().await?;
98            let io = TokioIo::new(stream);
99            let router = router.clone();
100            let middleware = middleware.clone();
101
102            tokio::spawn(async move {
103                let service = service_fn(move |req: hyper::Request<hyper::body::Incoming>| {
104                    let router = router.clone();
105                    let middleware = middleware.clone();
106                    async move { Ok::<_, Infallible>(handle_request(router, middleware, req).await) }
107                });
108
109                if let Err(err) = http1::Builder::new()
110                    .serve_connection(io, service)
111                    .with_upgrades()
112                    .await
113                {
114                    eprintln!("Error serving connection: {err:?}");
115                }
116            });
117        }
118    }
119}
120
121async fn handle_request(
122    router: Arc<Router>,
123    middleware_registry: Arc<MiddlewareRegistry>,
124    req: hyper::Request<hyper::body::Incoming>,
125) -> hyper::Response<Full<Bytes>> {
126    let method = req.method().clone();
127    let path = req.uri().path().to_string();
128    let query = req.uri().query().unwrap_or("");
129
130    // WebSocket upgrade at /_ferro/ws (must run before middleware/routing)
131    if path == "/_ferro/ws" && hyper_tungstenite::is_upgrade_request(&req) {
132        return handle_ws_upgrade(req);
133    }
134
135    // Built-in framework endpoints at /_ferro/*
136    // Uses framework prefix to avoid conflicts with user-defined routes
137    if path.starts_with("/_ferro/") && method == hyper::Method::GET {
138        return match path.as_str() {
139            "/_ferro/health" => health_response(query).await,
140            "/_ferro/routes" => crate::debug::handle_routes(),
141            "/_ferro/middleware" => crate::debug::handle_middleware(),
142            "/_ferro/services" => crate::debug::handle_services(),
143            "/_ferro/metrics" => crate::debug::handle_metrics(),
144            "/_ferro/queue/jobs" => crate::debug::handle_queue_jobs().await,
145            "/_ferro/queue/stats" => crate::debug::handle_queue_stats().await,
146            _ => HttpResponse::text("404 Not Found").status(404).into_hyper(),
147        };
148    }
149
150    // Note: Inertia context is now read directly from Request headers
151    // via req.is_inertia(), req.inertia_version(), etc.
152    // No thread-local storage needed - this is async-safe.
153
154    let response = match router.match_route(&method, &path) {
155        Some((handler, params, route_pattern)) => {
156            let request = Request::new(req)
157                .with_params(params)
158                .with_route_pattern(route_pattern.clone());
159
160            // Build middleware chain
161            let mut chain = MiddlewareChain::new();
162
163            // 1. Add global middleware
164            chain.extend(middleware_registry.global_middleware().iter().cloned());
165
166            // 2. Add route-level middleware (already boxed)
167            let route_middleware = router.get_route_middleware(&route_pattern);
168            chain.extend(route_middleware);
169
170            // 3. Execute chain with handler
171            let response = chain.execute(request, handler).await;
172
173            // Unwrap the Result - both Ok and Err contain HttpResponse
174            let http_response = response.unwrap_or_else(|e| e);
175            http_response.into_hyper()
176        }
177        None => {
178            // Try static file serving before fallback (only GET/HEAD)
179            if method == hyper::Method::GET || method == hyper::Method::HEAD {
180                if let Some(response) = crate::static_files::try_serve_static_file(&path).await {
181                    return response;
182                }
183            }
184
185            // Check for fallback handler
186            if let Some((fallback_handler, fallback_middleware)) = router.get_fallback() {
187                let request = Request::new(req).with_params(std::collections::HashMap::new());
188
189                // Build middleware chain for fallback
190                let mut chain = MiddlewareChain::new();
191
192                // 1. Add global middleware
193                chain.extend(middleware_registry.global_middleware().iter().cloned());
194
195                // 2. Add fallback-specific middleware
196                chain.extend(fallback_middleware);
197
198                // 3. Execute chain with fallback handler
199                let response = chain.execute(request, fallback_handler).await;
200
201                // Unwrap the Result - both Ok and Err contain HttpResponse
202                let http_response = response.unwrap_or_else(|e| e);
203                http_response.into_hyper()
204            } else {
205                // No fallback defined, return default 404
206                HttpResponse::text("404 Not Found").status(404).into_hyper()
207            }
208        }
209    };
210
211    response
212}
213
214/// Built-in health check endpoint at /_ferro/health
215/// Returns {"status": "ok", "timestamp": "..."} by default
216/// Add ?db=true to also check database connectivity (/_ferro/health?db=true)
217async fn health_response(query: &str) -> hyper::Response<Full<Bytes>> {
218    use chrono::Utc;
219    use serde_json::json;
220
221    let timestamp = Utc::now().to_rfc3339();
222    let check_db = query.contains("db=true");
223
224    let mut response = json!({
225        "status": "ok",
226        "timestamp": timestamp
227    });
228
229    if check_db {
230        // Try to check database connection
231        match check_database_health().await {
232            Ok(_) => {
233                response["database"] = json!("connected");
234            }
235            Err(e) => {
236                response["database"] = json!("error");
237                response["database_error"] = json!(e);
238            }
239        }
240    }
241
242    let body =
243        serde_json::to_string(&response).unwrap_or_else(|_| r#"{"status":"ok"}"#.to_string());
244
245    hyper::Response::builder()
246        .status(200)
247        .header("Content-Type", "application/json")
248        .body(Full::new(Bytes::from(body)))
249        .unwrap()
250}
251
252/// Check database health by attempting a simple query
253async fn check_database_health() -> Result<(), String> {
254    use crate::database::DB;
255    use sea_orm::ConnectionTrait;
256
257    if !DB::is_connected() {
258        return Err("Database not initialized".to_string());
259    }
260
261    let conn = DB::connection().map_err(|e| e.to_string())?;
262
263    // Execute a simple query to verify connection is alive
264    conn.inner()
265        .execute_unprepared("SELECT 1")
266        .await
267        .map_err(|e| format!("Database query failed: {e}"))?;
268
269    Ok(())
270}