Skip to main content

ullun/
app.rs

1//! Main application orchestrator
2
3use crate::error::{Error, Result};
4use crate::handler::{BoxFuture, DynHandler, Handler, HandlerWithParams};
5use crate::middleware::{Layered, Middleware, Next};
6use crate::openapi::{self, OpenApiInfo, Operation, RouteKind};
7use crate::request::{Params, Request, StateMap};
8use crate::response::Response;
9use crate::router::Router;
10use crate::shutdown::{Context, Shutdown};
11use http_body_util::BodyExt;
12use hyper::server::conn::http1;
13use hyper::service::service_fn;
14use hyper_util::rt::TokioIo;
15use std::collections::HashMap;
16use std::convert::Infallible;
17use std::net::SocketAddr;
18use std::sync::Arc;
19use std::time::Duration;
20use tokio::net::TcpListener;
21
22/// Main application struct
23pub struct App {
24    router: Router,
25    middlewares: Vec<Arc<dyn Middleware>>,
26    max_body_size: Option<usize>, // in bytes
27    docs: HashMap<(String, String), Operation>,
28    openapi: Option<OpenApiInfo>,
29    state: StateMap,
30    background: Vec<BackgroundTask>,
31    shutdown_grace: Duration,
32}
33
34type BackgroundTask = Box<dyn FnOnce(Context) -> BoxFuture<'static, ()> + Send>;
35
36/// Everything a running server shares between connections
37struct Shared {
38    router: Router,
39    middlewares: Vec<Arc<dyn Middleware>>,
40    max_body_size: Option<usize>,
41    state: Arc<StateMap>,
42    shutdown: Shutdown,
43}
44
45/// Route group builder
46pub struct RouteGroup {
47    prefix: String,
48    routes: Vec<(String, String, DynHandler)>, // (method, path, handler)
49    middlewares: Vec<Arc<dyn Middleware>>,
50    docs: Vec<(String, String, Operation)>, // (method, path relative to prefix, docs)
51}
52
53impl App {
54    pub fn new() -> Self {
55        Self {
56            router: Router::new(),
57            middlewares: Vec::new(),
58            max_body_size: Some(10 * 1024 * 1024), // 10 MB default
59            docs: HashMap::new(),
60            openapi: None,
61            state: StateMap::default(),
62            background: Vec::new(),
63            shutdown_grace: Duration::from_secs(10),
64        }
65    }
66
67    /// Set maximum request body size in bytes (None = unlimited)
68    pub fn max_body_size(mut self, size: Option<usize>) -> Self {
69        self.max_body_size = size;
70        self
71    }
72
73    /// Register shared application state, readable in handlers and
74    /// middleware with [`Request::state`]
75    ///
76    /// One value per type; registering the same type again replaces it. Use
77    /// interior mutability (`Mutex`, `RwLock`, atomics) for mutable state: the
78    /// value is shared by all requests and is not cloned.
79    ///
80    /// ```
81    /// use ullun::prelude::*;
82    /// use std::sync::atomic::{AtomicU64, Ordering};
83    ///
84    /// #[derive(Default)]
85    /// struct Hits(AtomicU64);
86    ///
87    /// async fn count(req: Request) -> Result<Response> {
88    ///     let hits = req.state::<Hits>()?.0.fetch_add(1, Ordering::Relaxed) + 1;
89    ///     Ok(Response::text(hits.to_string()))
90    /// }
91    ///
92    /// let app = App::new().state(Hits::default()).get("/", count);
93    /// ```
94    pub fn state<T: Send + Sync + 'static>(mut self, value: T) -> Self {
95        self.state.insert(value);
96        self
97    }
98
99    /// Run `task` alongside the server, started when it starts serving
100    ///
101    /// The task gets a [`Context`] with the shared state and the shutdown
102    /// signal; it should return when `ctx.sleep(..)` reports `false` or
103    /// `ctx.shutdown().wait()` completes. Finished tasks are awaited during
104    /// shutdown, for at most [`App::shutdown_grace`].
105    ///
106    /// ```
107    /// use std::time::Duration;
108    /// use ullun::prelude::*;
109    ///
110    /// struct Jobs;
111    ///
112    /// let app = App::new().state(Jobs).background(|ctx: Context| async move {
113    ///     while ctx.sleep(Duration::from_secs(60)).await {
114    ///         let _jobs = ctx.state::<Jobs>().unwrap();
115    ///         // purge expired sessions, refresh caches, ...
116    ///     }
117    /// });
118    /// ```
119    pub fn background<F, Fut>(mut self, task: F) -> Self
120    where
121        F: FnOnce(Context) -> Fut + Send + 'static,
122        Fut: std::future::Future<Output = ()> + Send + 'static,
123    {
124        self.background
125            .push(Box::new(move |ctx| Box::pin(task(ctx))));
126        self
127    }
128
129    /// How long shutdown waits for in-flight requests and background tasks
130    /// (default 10 seconds)
131    pub fn shutdown_grace(mut self, grace: Duration) -> Self {
132        self.shutdown_grace = grace;
133        self
134    }
135
136    /// Create a route group with a common prefix
137    pub fn group(mut self, prefix: &str, configure: impl FnOnce(RouteGroup) -> RouteGroup) -> Self {
138        let group = RouteGroup::new(prefix);
139        let group = configure(group);
140
141        for (method, path, op) in group.docs {
142            let full_path = format!("{}{}", group.prefix, path);
143            self.docs.insert((method, full_path), op);
144        }
145
146        // Group middlewares wrap only this group's routes (inside the global ones)
147        let middlewares = group.middlewares;
148        for (method, path, handler) in group.routes {
149            let full_path = format!("{}{}", group.prefix, path);
150            let handler: DynHandler = if middlewares.is_empty() {
151                handler
152            } else {
153                Arc::new(Layered::new(middlewares.clone(), handler))
154            };
155            if let Err(e) = self.router.insert(&method, &full_path, handler) {
156                panic!("Failed to register {} route {}: {}", method, full_path, e);
157            }
158        }
159
160        self
161    }
162
163    /// Describe a registered route for the OpenAPI document
164    ///
165    /// `path` is the path exactly as registered, e.g. `/users/{id}`.
166    pub fn doc(mut self, method: &str, path: &str, operation: Operation) -> Self {
167        self.docs
168            .insert((method.to_ascii_uppercase(), path.to_string()), operation);
169        self
170    }
171
172    /// Publish an OpenAPI 3.1 document of all routes (and optionally Swagger UI)
173    ///
174    /// See the [`openapi`] module.
175    pub fn openapi(mut self, info: OpenApiInfo) -> Self {
176        self.openapi = Some(info);
177        self
178    }
179
180    /// Add GET route
181    pub fn get<H>(mut self, path: &str, handler: H) -> Self
182    where
183        H: Handler,
184    {
185        if let Err(e) = self
186            .router
187            .insert("GET", path, Arc::new(handler) as DynHandler)
188        {
189            panic!("Failed to register GET route {}: {}", path, e);
190        }
191        self
192    }
193
194    /// Add GET route with path params extractor
195    pub fn get_with_params<F, Fut>(mut self, path: &str, handler: F) -> Self
196    where
197        F: Fn(Params) -> Fut + Send + Sync + 'static,
198        Fut: std::future::Future<Output = Result<Response>> + Send + 'static,
199    {
200        if let Err(e) = self.router.insert(
201            "GET",
202            path,
203            Arc::new(HandlerWithParams::new(handler)) as DynHandler,
204        ) {
205            panic!("Failed to register GET route {}: {}", path, e);
206        }
207        self
208    }
209
210    /// Add POST route
211    pub fn post<H>(mut self, path: &str, handler: H) -> Self
212    where
213        H: Handler,
214    {
215        if let Err(e) = self
216            .router
217            .insert("POST", path, Arc::new(handler) as DynHandler)
218        {
219            panic!("Failed to register POST route {}: {}", path, e);
220        }
221        self
222    }
223
224    /// Add PUT route
225    pub fn put<H>(mut self, path: &str, handler: H) -> Self
226    where
227        H: Handler,
228    {
229        if let Err(e) = self
230            .router
231            .insert("PUT", path, Arc::new(handler) as DynHandler)
232        {
233            panic!("Failed to register PUT route {}: {}", path, e);
234        }
235        self
236    }
237
238    /// Add DELETE route
239    pub fn delete<H>(mut self, path: &str, handler: H) -> Self
240    where
241        H: Handler,
242    {
243        if let Err(e) = self
244            .router
245            .insert("DELETE", path, Arc::new(handler) as DynHandler)
246        {
247            panic!("Failed to register DELETE route {}: {}", path, e);
248        }
249        self
250    }
251
252    /// Add PATCH route
253    pub fn patch<H>(mut self, path: &str, handler: H) -> Self
254    where
255        H: Handler,
256    {
257        if let Err(e) = self
258            .router
259            .insert("PATCH", path, Arc::new(handler) as DynHandler)
260        {
261            panic!("Failed to register PATCH route {}: {}", path, e);
262        }
263        self
264    }
265
266    /// Accept WebSocket connections on `path` (feature `websocket`)
267    ///
268    /// The handler receives the original request (path params, query, headers,
269    /// extensions set by middleware) and the upgraded socket, and runs on its
270    /// own task until it returns.
271    #[cfg(feature = "websocket")]
272    pub fn ws<F, Fut>(self, path: &str, handler: F) -> Self
273    where
274        F: Fn(Request, crate::websocket::WebSocket) -> Fut + Send + Sync + 'static,
275        Fut: std::future::Future<Output = ()> + Send + 'static,
276    {
277        self.ws_with(path, crate::websocket::WsOptions::default(), handler)
278    }
279
280    /// Like [`App::ws`] with explicit size limit and allowed origins
281    #[cfg(feature = "websocket")]
282    pub fn ws_with<F, Fut>(
283        mut self,
284        path: &str,
285        options: crate::websocket::WsOptions,
286        handler: F,
287    ) -> Self
288    where
289        F: Fn(Request, crate::websocket::WebSocket) -> Fut + Send + Sync + 'static,
290        Fut: std::future::Future<Output = ()> + Send + 'static,
291    {
292        let route = crate::websocket::WsRoute::new(handler, options);
293        if let Err(e) = self.router.insert_as(
294            RouteKind::WebSocket,
295            "GET",
296            path,
297            Arc::new(route) as DynHandler,
298        ) {
299            panic!("Failed to register WebSocket route {}: {}", path, e);
300        }
301        self
302    }
303
304    /// Add middleware
305    pub fn middleware<M>(mut self, middleware: M) -> Self
306    where
307        M: Middleware,
308    {
309        self.middlewares.push(Arc::new(middleware));
310        self
311    }
312
313    /// Serve static files from a directory
314    pub fn serve_static(mut self, route_prefix: &str, dir_path: &str) -> Self {
315        use crate::static_files::StaticFileHandler;
316        let handler = StaticFileHandler::new(dir_path);
317        // matchit 0.9 catch-all syntax: {*param} matches the rest of the path
318        let prefix = route_prefix.trim_end_matches('/');
319        let handler: DynHandler = Arc::new(handler);
320        let route = format!("{}/{{*filepath}}", prefix);
321        if let Err(e) = self
322            .router
323            .insert_as(RouteKind::Hidden, "GET", &route, handler.clone())
324        {
325            panic!("Failed to register static file route {}: {}", route, e);
326        }
327        // The catch-all needs at least one segment, so also serve the prefix
328        // root (-> index.html). Skipped when the user already owns that route.
329        let root = if prefix.is_empty() { "/" } else { prefix };
330        let _ = self
331            .router
332            .insert_as(RouteKind::Hidden, "GET", root, handler.clone());
333        if !prefix.is_empty() {
334            let _ =
335                self.router
336                    .insert_as(RouteKind::Hidden, "GET", &format!("{}/", prefix), handler);
337        }
338        self
339    }
340
341    /// Run the server until CTRL+C or SIGTERM, then shut down gracefully
342    ///
343    /// Stops accepting connections, lets in-flight requests finish and waits
344    /// for background tasks, each for at most [`App::shutdown_grace`].
345    pub async fn run(self, addr: &str) -> Result<()> {
346        let addr: SocketAddr = addr
347            .parse()
348            .map_err(|e| Error::internal(format!("Invalid address: {}", e)))?;
349
350        let listener = TcpListener::bind(addr)
351            .await
352            .map_err(|e| Error::internal(format!("Failed to bind: {}", e)))?;
353
354        println!("šŸš€ ullun-api listening on http://{}", addr);
355
356        let result = self
357            .serve_with_shutdown(listener, async {
358                termination_signal().await;
359                println!("\nšŸ›‘ Shutting down gracefully...");
360            })
361            .await;
362        println!("āœ… Server stopped");
363        result
364    }
365
366    /// Serve on an already bound listener until the future is dropped.
367    ///
368    /// Useful for tests, benchmarks and embedding: bind to port `0`, read the
369    /// assigned port from `listener.local_addr()`, then call `serve`.
370    /// Unlike [`App::run`], this does not install signal handlers; use
371    /// [`App::serve_with_shutdown`] for a graceful stop.
372    pub async fn serve(self, listener: TcpListener) -> Result<()> {
373        self.serve_with_shutdown(listener, std::future::pending())
374            .await
375    }
376
377    /// Serve until `signal` completes, then shut down gracefully
378    ///
379    /// New connections are refused, idle keep-alive connections are closed,
380    /// running requests finish, the shutdown signal reaches
381    /// [`Request::shutdown_signal`] and background tasks, and the call returns
382    /// once everything has stopped or [`App::shutdown_grace`] has passed.
383    pub async fn serve_with_shutdown(
384        mut self,
385        listener: TcpListener,
386        signal: impl std::future::Future<Output = ()>,
387    ) -> Result<()> {
388        self = self.finalize();
389        let grace = self.shutdown_grace;
390        let background = std::mem::take(&mut self.background);
391        let (trigger, rx) = tokio::sync::watch::channel(false);
392        let shutdown = Shutdown::new(rx);
393        let shared = Arc::new(self.into_shared(shutdown.clone()));
394
395        let tasks: Vec<_> = background
396            .into_iter()
397            .map(|task| {
398                let ctx = Context::new(shared.state.clone(), shutdown.clone());
399                tokio::spawn(task(ctx))
400            })
401            .collect();
402
403        // every connection holds a clone; the channel closes when all are done
404        let (done_tx, mut done_rx) = tokio::sync::mpsc::channel::<()>(1);
405        tokio::pin!(signal);
406        loop {
407            tokio::select! {
408                _ = &mut signal => break,
409                accepted = listener.accept() => match accepted {
410                    Ok((stream, peer)) => {
411                        tokio::spawn(serve_connection(
412                            stream,
413                            peer,
414                            shared.clone(),
415                            shutdown.clone(),
416                            done_tx.clone(),
417                        ));
418                    }
419                    Err(e) => {
420                        // typically out of file descriptors: back off instead of dying
421                        eprintln!("Failed to accept connection: {}", e);
422                        tokio::time::sleep(Duration::from_millis(100)).await;
423                    }
424                },
425            }
426        }
427
428        let _ = trigger.send(true);
429        drop(done_tx);
430        drop(listener);
431        let drain = async {
432            let _ = done_rx.recv().await;
433            for task in tasks {
434                let _ = task.await;
435            }
436        };
437        if tokio::time::timeout(grace, drain).await.is_err() {
438            eprintln!(
439                "Shutdown grace period of {:?} elapsed, stopping anyway",
440                grace
441            );
442        }
443        Ok(())
444    }
445
446    fn into_shared(self, shutdown: Shutdown) -> Shared {
447        Shared {
448            router: self.router,
449            middlewares: self.middlewares,
450            max_body_size: self.max_body_size,
451            state: Arc::new(self.state),
452            shutdown,
453        }
454    }
455
456    /// Last step before serving: publish the OpenAPI document, now that all
457    /// routes are known.
458    fn finalize(mut self) -> Self {
459        let Some(info) = self.openapi.take() else {
460            return self;
461        };
462        let spec = openapi::build(&info, self.router.registered(), &self.docs);
463        let spec = bytes::Bytes::from(serde_json::to_vec(&spec).unwrap_or_default());
464
465        let spec_handler = {
466            let spec = spec.clone();
467            move |_req: Request| {
468                let spec = spec.clone();
469                async move {
470                    Ok(Response::ok()
471                        .with_header("content-type", "application/json")
472                        .with_body(spec))
473                }
474            }
475        };
476        self.insert_hidden("GET", &info.spec_path, Arc::new(spec_handler));
477
478        if let Some(docs_path) = &info.docs_path {
479            let page = openapi::swagger_ui_html(&info.title, &info.spec_path);
480            let page_handler = move |_req: Request| {
481                let page = page.clone();
482                async move { Ok(Response::html(page)) }
483            };
484            self.insert_hidden("GET", docs_path, Arc::new(page_handler));
485        }
486        self
487    }
488
489    fn insert_hidden(&mut self, method: &str, path: &str, handler: DynHandler) {
490        if let Err(e) = self
491            .router
492            .insert_as(RouteKind::Hidden, method, path, handler)
493        {
494            panic!("Failed to register {} route {}: {}", method, path, e);
495        }
496    }
497}
498
499/// One connection, until it closes or shutdown drains it
500async fn serve_connection(
501    stream: tokio::net::TcpStream,
502    peer: SocketAddr,
503    shared: Arc<Shared>,
504    shutdown: Shutdown,
505    _done: tokio::sync::mpsc::Sender<()>,
506) {
507    let io = TokioIo::new(stream);
508    let service = service_fn(|req| {
509        let shared = shared.clone();
510        async move { Ok::<_, Infallible>(handle_request(req, peer, shared).await) }
511    });
512    let conn = http1::Builder::new()
513        .serve_connection(io, service)
514        .with_upgrades();
515    tokio::pin!(conn);
516
517    let mut draining = false;
518    loop {
519        tokio::select! {
520            result = conn.as_mut() => {
521                if let Err(err) = result {
522                    eprintln!("Error serving connection: {:?}", err);
523                }
524                return;
525            }
526            _ = shutdown.wait(), if !draining => {
527                // idle connections close now, busy ones after their response
528                conn.as_mut().graceful_shutdown();
529                draining = true;
530            }
531        }
532    }
533}
534
535/// CTRL+C everywhere, plus SIGTERM on Unix (what container runtimes send)
536async fn termination_signal() {
537    #[cfg(unix)]
538    {
539        use tokio::signal::unix::{signal, SignalKind};
540        match signal(SignalKind::terminate()) {
541            Ok(mut term) => {
542                tokio::select! {
543                    _ = tokio::signal::ctrl_c() => {}
544                    _ = term.recv() => {}
545                }
546                return;
547            }
548            Err(e) => eprintln!("Cannot listen for SIGTERM: {}", e),
549        }
550    }
551    let _ = tokio::signal::ctrl_c().await;
552}
553
554impl Default for App {
555    fn default() -> Self {
556        Self::new()
557    }
558}
559
560impl RouteGroup {
561    fn new(prefix: &str) -> Self {
562        let prefix = prefix.trim_end_matches('/');
563        Self {
564            prefix: prefix.to_string(),
565            routes: Vec::new(),
566            middlewares: Vec::new(),
567            docs: Vec::new(),
568        }
569    }
570
571    /// Describe a route of this group for the OpenAPI document; `path` is
572    /// relative to the group prefix, as given to `get`/`post`/...
573    pub fn doc(mut self, method: &str, path: &str, operation: Operation) -> Self {
574        self.docs
575            .push((method.to_ascii_uppercase(), path.to_string(), operation));
576        self
577    }
578
579    pub fn get<H>(mut self, path: &str, handler: H) -> Self
580    where
581        H: Handler,
582    {
583        self.routes.push((
584            "GET".to_string(),
585            path.to_string(),
586            Arc::new(handler) as DynHandler,
587        ));
588        self
589    }
590
591    pub fn post<H>(mut self, path: &str, handler: H) -> Self
592    where
593        H: Handler,
594    {
595        self.routes.push((
596            "POST".to_string(),
597            path.to_string(),
598            Arc::new(handler) as DynHandler,
599        ));
600        self
601    }
602
603    pub fn put<H>(mut self, path: &str, handler: H) -> Self
604    where
605        H: Handler,
606    {
607        self.routes.push((
608            "PUT".to_string(),
609            path.to_string(),
610            Arc::new(handler) as DynHandler,
611        ));
612        self
613    }
614
615    pub fn delete<H>(mut self, path: &str, handler: H) -> Self
616    where
617        H: Handler,
618    {
619        self.routes.push((
620            "DELETE".to_string(),
621            path.to_string(),
622            Arc::new(handler) as DynHandler,
623        ));
624        self
625    }
626
627    pub fn patch<H>(mut self, path: &str, handler: H) -> Self
628    where
629        H: Handler,
630    {
631        self.routes.push((
632            "PATCH".to_string(),
633            path.to_string(),
634            Arc::new(handler) as DynHandler,
635        ));
636        self
637    }
638
639    /// Add middleware that runs only for the routes of this group
640    ///
641    /// It runs after the application-wide middleware, in registration order,
642    /// regardless of whether it is added before or after the routes.
643    pub fn middleware<M>(mut self, middleware: M) -> Self
644    where
645        M: Middleware,
646    {
647        self.middlewares.push(Arc::new(middleware));
648        self
649    }
650}
651
652async fn handle_request(
653    req: hyper::Request<hyper::body::Incoming>,
654    peer: SocketAddr,
655    shared: Arc<Shared>,
656) -> hyper::Response<crate::response::HyperBody> {
657    let Shared {
658        router,
659        middlewares,
660        max_body_size,
661        state,
662        shutdown,
663    } = &*shared;
664    let max_body_size = *max_body_size;
665    let method = req.method().to_string();
666    let path = req.uri().path().to_string();
667
668    // Convert hyper request to our Request type
669    let (parts, body) = req.into_parts();
670
671    // Enforce body size limit during streaming
672    let body_bytes = if let Some(max_size) = max_body_size {
673        // Check Content-Length header first (fast path)
674        if let Some(content_length) = parts.headers.get(http::header::CONTENT_LENGTH) {
675            if let Ok(length_str) = content_length.to_str() {
676                if let Ok(length) = length_str.parse::<usize>() {
677                    if length > max_size {
678                        let error_response = Response::from_error(Error::custom(
679                            413,
680                            format!("Request body too large (max {} bytes)", max_size),
681                        ));
682                        return error_response.into_hyper();
683                    }
684                }
685            }
686        }
687
688        // Limit actual stream reading (prevents bypass via chunked encoding)
689        let limited = http_body_util::Limited::new(body, max_size);
690        match limited.collect().await {
691            Ok(collected) => collected.to_bytes(),
692            Err(_) => {
693                // Body exceeded limit during reading
694                let error_response = Response::from_error(Error::custom(
695                    413,
696                    format!("Request body too large (max {} bytes)", max_size),
697                ));
698                return error_response.into_hyper();
699            }
700        }
701    } else {
702        match body.collect().await {
703            Ok(collected) => collected.to_bytes(),
704            Err(_) => bytes::Bytes::new(),
705        }
706    };
707
708    let mut request = Request::new(parts.method, parts.uri, parts.headers, body_bytes);
709    request.extensions = parts.extensions;
710    request.remote_addr = Some(peer);
711    request.state = Some(state.clone());
712    request.shutdown = Some(shutdown.clone());
713
714    // Match route. A miss still runs the middleware chain (CORS preflight,
715    // request ids, logging, security headers) and ends in a 404.
716    let handler: Option<DynHandler> = match router.match_route(&method, &path) {
717        Ok((handler, params)) => {
718            request.params = params;
719            Some(handler)
720        }
721        Err(_) => None,
722    };
723
724    let result = if middlewares.is_empty() {
725        dispatch(handler, request).await
726    } else {
727        // Route handler (or the 404) runs as the last link of the chain
728        let mut chain = middlewares.to_vec();
729        let final_handler = Arc::new(move |req: Request, _next: Next| {
730            let handler = handler.clone();
731            async move { dispatch(handler, req).await }
732        });
733        chain.push(final_handler as Arc<dyn Middleware>);
734
735        Next::new(Arc::new(chain)).run(request).await
736    };
737
738    // Convert Result to Response
739    let response = match result {
740        Ok(resp) => resp,
741        Err(err) => Response::from_error(err),
742    };
743
744    response.into_hyper()
745}
746
747async fn dispatch(handler: Option<DynHandler>, req: Request) -> Result<Response> {
748    match handler {
749        Some(handler) => handler.call(req).await,
750        None => Err(Error::not_found("Route not found")),
751    }
752}