ripress/app/mod.rs
1//! # App Module
2//!
3//! The core application module for Ripress, providing Express.js-like functionality
4//! for building HTTP servers in Rust. This module contains the main [`App`] struct
5//! and [`Middleware`] definitions that form the foundation of a Ripress web application.
6//!
7//! ## Key Features
8//!
9//! - Express.js-like routing and middleware system
10//! - Built-in middleware for common tasks (CORS, logging, rate limiting, etc.)
11//! - Static file serving capabilities
12//! - WebSocket support (with `wynd` feature)
13//! - Async/await support throughout
14//!
15//! ## Basic Usage
16//!
17//! ```no_run
18//! use ripress::app::App;
19//! use ripress::types::RouterFns;
20//!
21//! #[tokio::main]
22//! async fn main() {
23//! let mut app = App::new();
24//!
25//! app.get("/", |_req, res| async move {
26//! res.ok().text("Hello, World!")
27//! });
28//!
29//! app.listen(3000, || {
30//! println!("Server running on http://localhost:3000");
31//! }).await;
32//! }
33//! ```
34
35#![warn(missing_docs)]
36
37use crate::app::api_error::ApiError;
38
39use crate::helpers::{box_future_middleware, exec_post_middleware, exec_pre_middleware};
40#[cfg(feature = "with-wynd")]
41use crate::middlewares::WyndMiddleware;
42use crate::middlewares::body_limit::body_limit;
43#[cfg(feature = "compression")]
44use crate::middlewares::compression::{CompressionConfig, compression};
45use crate::middlewares::cors::{CorsConfig, cors};
46#[cfg(feature = "logger")]
47use crate::middlewares::logger::{LoggerConfig, logger};
48use crate::middlewares::rate_limiter::{RateLimiterConfig, rate_limiter};
49use crate::middlewares::shield::{ShieldConfig, shield};
50use crate::middlewares::{Middleware, MiddlewareType};
51use crate::req::HttpRequest;
52use crate::res::HttpResponse;
53use crate::router::Router;
54#[cfg(feature = "with-wynd")]
55use crate::types::WyndMiddlewareHandler;
56use crate::types::{HandlerMiddleware, HttpMethods, RouterFns, Routes};
57use bytes::Bytes;
58use http_body_util::{BodyExt, Full};
59use hyper::http::StatusCode;
60use hyper::server::conn::http1::Builder;
61use hyper::service::Service;
62use hyper::{Method, header};
63use hyper::{Request, Response};
64use hyper_staticfile::Static;
65use hyper_util::rt::TokioIo;
66use routerify_ng::RouterService;
67use routerify_ng::ext::RequestExt;
68use std::collections::HashMap;
69use std::net::SocketAddr;
70use std::path::Path;
71use std::sync::Arc;
72use tokio::net::TcpListener;
73
74pub(crate) mod api_error;
75
76/// The App struct is the core of Ripress, providing a simple interface for creating HTTP servers and handling requests.
77///
78/// It follows an Express-like pattern for route handling and middleware management. The App struct
79/// manages routes, middlewares, static file serving, and server lifecycle.
80///
81/// ## Features
82///
83/// - **Routing**: HTTP method-based routing (GET, POST, PUT, DELETE, etc.)
84/// - **Middleware**: Pre and post-processing middleware with path-based matching
85/// - **Static Files**: Serve static assets with proper headers and caching
86/// - **WebSocket Support**: Optional WebSocket support via the `wynd` crate
87/// - **Built-in Middleware**: CORS, logging, rate limiting, compression, and security headers
88///
89/// ## Example
90///
91/// ```ignore
92/// use ripress::app::App;
93/// use ripress::types::RouterFns;
94///
95/// #[tokio::main]
96/// async fn main() {
97/// let mut app = App::new();
98///
99/// // Add middleware
100/// app.use_cors(None);
101/// app.use_logger(None);
102///
103/// // Add routes
104/// app.get("/", |_req, res| async move {
105/// res.ok().text("Hello, World!")
106/// });
107///
108/// app.post("/api/users", |req, res| async move {
109/// // Handle user creation
110/// res.ok().json("User created")
111/// });
112///
113/// // Serve static files
114/// app.static_files("/public", "./public").unwrap();
115///
116/// // Start server
117/// app.listen(3000, || {
118/// println!("Server running on http://localhost:3000");
119/// }).await;
120/// }
121/// ```
122pub struct App {
123 /// The collection of registered routes organized by path and HTTP method.
124 routes: Routes,
125 /// The list of middleware functions to be applied to requests.
126 pub(crate) middlewares: Vec<Middleware>,
127 /// Static file mappings from mount path to filesystem path.
128 pub(crate) static_files: HashMap<&'static str, &'static str>,
129
130 pub(crate) graceful_shutdown: bool,
131 #[cfg(feature = "with-wynd")]
132 /// Optional WebSocket middleware (only available with `wynd` feature).
133 pub(crate) wynd_middleware: Option<WyndMiddleware>,
134}
135
136impl RouterFns for App {
137 fn routes(&mut self) -> &mut Routes {
138 &mut self.routes
139 }
140}
141
142impl App {
143 /// Creates a new App instance with empty routes and middleware.
144 ///
145 /// This is the starting point for building a Ripress application. The returned
146 /// App instance has no routes or middleware configured and is ready to be customized.
147 ///
148 /// ## Example
149 ///
150 /// ```
151 /// use ripress::app::App;
152 ///
153 /// let mut app = App::new();
154 /// ```
155 pub fn new() -> Self {
156 App {
157 routes: HashMap::new(),
158 middlewares: Vec::new(),
159 static_files: HashMap::new(),
160 graceful_shutdown: false,
161 #[cfg(feature = "with-wynd")]
162 wynd_middleware: None,
163 }
164 }
165
166 /// Adds a middleware to the application (deprecated).
167 ///
168 /// ## Deprecation Notice
169 ///
170 /// This method is deprecated since version 1.9.0. Use [`use_pre_middleware`] instead
171 /// for better clarity about middleware execution order.
172 ///
173 /// ## Arguments
174 ///
175 /// * `path` - Optional path prefix where the middleware should apply. Defaults to "/" (all paths)
176 /// * `middleware` - The middleware function to add
177 ///
178 /// ## Example
179 ///
180 /// ```
181 /// use ripress::app::App;
182 ///
183 /// let mut app = App::new();
184 ///
185 /// // This is deprecated - use use_pre_middleware instead
186 /// app.use_middleware(Some("/api"), |req, res| async move {
187 /// println!("Processing API request");
188 /// (req, None)
189 /// });
190 /// ```
191 #[deprecated(since = "1.9.0", note = "Use `use_pre_middleware` instead")]
192 pub fn use_middleware<F, Fut, P>(&mut self, path: P, middleware: F) -> &mut Self
193 where
194 P: Into<Option<&'static str>>,
195 F: Fn(HttpRequest, HttpResponse) -> Fut + Send + Sync + 'static,
196 Fut: std::future::Future<Output = (HttpRequest, Option<HttpResponse>)> + Send + 'static,
197 {
198 let path = path.into().unwrap_or("/").to_string();
199 self.middlewares.push(Middleware {
200 func: Self::middleware_from_closure(middleware),
201 path,
202 middleware_type: MiddlewareType::Pre,
203 });
204 self
205 }
206
207 /// Enables graceful shutdown for the application.
208 ///
209 /// When graceful shutdown is enabled, the server will listen for a shutdown signal
210 /// (such as Ctrl+C) and attempt to shut down cleanly, finishing any in-flight requests
211 /// before exiting. This is useful for production environments where you want to avoid
212 /// abruptly terminating active connections.
213 ///
214 /// ## Example
215 ///
216 /// ```
217 /// use ripress::app::App;
218 ///
219 /// let mut app = App::new();
220 /// app.with_graceful_shutdown();
221 /// ```
222 pub fn with_graceful_shutdown(&mut self) {
223 self.graceful_shutdown = true
224 }
225
226 /// Adds a pre-execution middleware to the application.
227 ///
228 /// Pre-middlewares are executed before the route handler. They can modify the request,
229 /// short-circuit the processing by returning a response, or pass control to the next
230 /// middleware in the chain.
231 ///
232 /// ## Arguments
233 ///
234 /// * `path` - Optional path prefix where the middleware should apply. If `None`, defaults to "/" (all paths)
235 /// * `middleware` - The middleware function that receives `(HttpRequest, HttpResponse)` and returns a future
236 /// resolving to `(HttpRequest, Option<HttpResponse>)`. If `Some(response)` is returned, processing stops
237 /// and the response is sent. If `None` is returned, processing continues.
238 ///
239 /// ## Example
240 ///
241 /// ```
242 /// use ripress::app::App;
243 ///
244 /// let mut app = App::new();
245 ///
246 /// // Authentication middleware for API routes
247 /// app.use_pre_middleware(Some("/api"), |req, res| async move {
248 /// if req.headers.get("authorization").is_none() {
249 /// return (req, Some(res.unauthorized().text("Missing authorization header")));
250 /// }
251 /// (req, None) // Continue processing
252 /// });
253 ///
254 /// // Logging middleware for all routes
255 /// app.use_pre_middleware(None, |req, res| async move {
256 /// println!("Request: {} {}", req.method, req.path);
257 /// (req, None)
258 /// });
259 /// ```
260 pub fn use_pre_middleware<F, Fut, P>(&mut self, path: P, middleware: F) -> &mut Self
261 where
262 P: Into<Option<&'static str>>,
263 F: Fn(HttpRequest, HttpResponse) -> Fut + Send + Sync + 'static,
264 Fut: std::future::Future<Output = (HttpRequest, Option<HttpResponse>)> + Send + 'static,
265 {
266 let path = path.into().unwrap_or("/").to_string();
267 self.middlewares.push(Middleware {
268 func: Self::middleware_from_closure(middleware),
269 path: path,
270 middleware_type: MiddlewareType::Pre,
271 });
272 self
273 }
274
275 /// Adds a post-execution middleware to the application.
276 ///
277 /// Post-middlewares are executed after the route handler has processed the request.
278 /// They can modify the response or perform cleanup operations. They cannot short-circuit
279 /// processing since the route handler has already run.
280 ///
281 /// ## Arguments
282 ///
283 /// * `path` - Optional path prefix where the middleware should apply. If `None`, defaults to "/" (all paths)
284 /// * `middleware` - The middleware function that receives `(HttpRequest, HttpResponse)` where the response
285 /// has been populated by the route handler. Returns a future resolving to `(HttpRequest, Option<HttpResponse>)`.
286 ///
287 /// ## Example
288 ///
289 /// ```
290 /// use ripress::app::App;
291 ///
292 /// let mut app = App::new();
293 ///
294 /// // Add security headers to all responses
295 /// app.use_post_middleware(None, |req, mut res| async move {
296 /// res = res.set_header("X-Frame-Options", "DENY")
297 /// .set_header("X-Content-Type-Options", "nosniff");
298 /// (req, Some(res))
299 /// });
300 ///
301 /// // Log response status for API routes
302 /// app.use_post_middleware(Some("/api"), |req, res| async move {
303 /// println!("API Response: {}", req.path);
304 /// (req, Some(res))
305 /// });
306 /// ```
307 pub fn use_post_middleware<F, Fut, P>(&mut self, path: P, middleware: F) -> &mut Self
308 where
309 P: Into<Option<&'static str>>,
310 F: Fn(HttpRequest, HttpResponse) -> Fut + Send + Sync + 'static,
311 Fut: std::future::Future<Output = (HttpRequest, Option<HttpResponse>)> + Send + 'static,
312 {
313 let path = path.into().unwrap_or("/").to_string();
314 self.middlewares.push(Middleware {
315 func: Self::middleware_from_closure(middleware),
316 path: path,
317 middleware_type: MiddlewareType::Post,
318 });
319 self
320 }
321
322 /// Adds a logger middleware to the application.
323 ///
324 /// The logger middleware logs incoming HTTP requests with configurable options.
325 /// It uses the `tracing` crate for logging, so make sure to initialize a tracing
326 /// subscriber in your application.
327 ///
328 /// ## Arguments
329 ///
330 /// * `config` - Optional [`LoggerConfig`] to customize logging behavior. If `None`,
331 /// default settings are used which log basic request information.
332 ///
333 /// ## Example
334 ///
335 /// ```
336 /// use ripress::app::App;
337 /// use ripress::middlewares::logger::LoggerConfig;
338 ///
339 /// // Initialize tracing (required for logging to work)
340 /// tracing_subscriber::fmt::init();
341 ///
342 /// let mut app = App::new();
343 ///
344 /// // Use default logger settings
345 /// app.use_logger(None);
346 ///
347 /// // Use custom logger configuration
348 /// app.use_logger(Some(LoggerConfig {
349 /// method: true, // Log HTTP method
350 /// path: true, // Log request path
351 /// status: true, // Log response status
352 /// ..Default::default()
353 /// }));
354 /// ```
355 ///
356 /// ## Default Behavior
357 ///
358 /// - Logs to the `info` level
359 /// - Includes HTTP method, path, and response status
360 /// - Applied to all routes ("/")
361 /// - Executed as post-middleware (after route handling)
362 #[cfg(feature = "logger")]
363 pub fn use_logger(&mut self, config: Option<LoggerConfig>) -> &mut Self {
364 self.middlewares.push(Middleware {
365 func: Self::middleware_from_closure(logger(config)),
366 path: "/".to_string(),
367 middleware_type: MiddlewareType::Post,
368 });
369 self
370 }
371
372 /// Adds a CORS (Cross-Origin Resource Sharing) middleware to the application.
373 ///
374 /// CORS middleware handles cross-origin requests by setting appropriate headers
375 /// and responding to preflight OPTIONS requests. This is essential for web applications
376 /// that need to accept requests from different domains.
377 ///
378 /// ## Arguments
379 ///
380 /// * `config` - Optional [`CorsConfig`] to customize CORS behavior. If `None`,
381 /// permissive default settings are used.
382 ///
383 /// ## Example
384 ///
385 /// ```
386 /// use ripress::app::App;
387 /// use ripress::middlewares::cors::CorsConfig;
388 ///
389 /// let mut app = App::new();
390 ///
391 /// // Use permissive default CORS settings (allows all origins)
392 /// app.use_cors(None);
393 ///
394 /// // Use custom CORS configuration
395 /// app.use_cors(Some(CorsConfig {
396 /// allowed_origin: "https://example.com",
397 /// allowed_methods: "GET, POST, PUT, DELETE, OPTIONS, HEAD",
398 /// allowed_headers: "Content-Type, Authorization",
399 /// ..Default::default()
400 /// }));
401 /// ```
402 ///
403 /// ## Default Behavior
404 ///
405 /// - Allows all origins (`*`)
406 /// - Allows common HTTP methods
407 /// - Applied to all routes ("/")
408 /// - Executed as pre-middleware
409 /// - Automatically handles OPTIONS preflight requests
410 pub fn use_cors(&mut self, config: Option<CorsConfig>) -> &mut Self {
411 self.middlewares.push(Middleware {
412 func: Self::middleware_from_closure(cors(config)),
413 path: "/".to_string(),
414 middleware_type: MiddlewareType::Pre,
415 });
416 self
417 }
418
419 /// Adds a request body size limit middleware to the application.
420 ///
421 /// This middleware enforces a maximum size limit on incoming request bodies to prevent
422 /// memory exhaustion attacks and manage resource usage. Requests exceeding the limit
423 /// are rejected with a 413 Payload Too Large status.
424 ///
425 /// ## Arguments
426 ///
427 /// * `config` - Optional maximum size in bytes for request bodies. If `None`,
428 /// the default limit is 1 MB (1,048,576 bytes).
429 ///
430 /// ## Example
431 ///
432 /// ```
433 /// use ripress::app::App;
434 ///
435 /// let mut app = App::new();
436 ///
437 /// // Use the default 1 MB limit
438 /// app.use_body_limit(None);
439 ///
440 /// // Set a custom limit (e.g., 2 MB for file uploads)
441 /// app.use_body_limit(Some(2 * 1024 * 1024));
442 ///
443 /// // Very restrictive limit for API endpoints (100 KB)
444 /// app.use_body_limit(Some(100 * 1024));
445 /// ```
446 ///
447 /// ## Behavior
448 ///
449 /// - Applied to all routes ("/")
450 /// - Executed as pre-middleware (before route processing)
451 /// - Returns 413 Payload Too Large for requests exceeding the limit
452 /// - Does not affect GET requests or requests without bodies
453 pub fn use_body_limit(&mut self, config: Option<usize>) -> &mut Self {
454 self.middlewares.push(Middleware {
455 func: Self::middleware_from_closure(body_limit(config)),
456 path: "/".to_string(),
457 middleware_type: MiddlewareType::Pre,
458 });
459 self
460 }
461
462 #[cfg(feature = "with-wynd")]
463 /// Adds WebSocket middleware to the application using the Wynd WebSocket library.
464 ///
465 /// This method enables WebSocket support for your application by integrating with
466 /// the Wynd WebSocket library. WebSocket connections will be handled at the specified path.
467 ///
468 /// ## Feature Requirement
469 ///
470 /// This method is only available when the `with-wynd` feature is enabled in your `Cargo.toml`:
471 ///
472 /// ```toml
473 /// [dependencies]
474 /// ripress = { version = "*", features = ["with-wynd"] }
475 /// wynd = { version = "*", features = ["with-ripress"] }
476 /// ```
477 ///
478 /// ## Arguments
479 ///
480 /// * `path` - The path where WebSocket connections should be accepted (e.g., "/ws", "/websocket")
481 /// * `handler` - A Wynd WebSocket handler function that processes WebSocket connections
482 ///
483 /// ## Example
484 ///
485 /// ```ignore
486 /// use ripress::{app::App, types::RouterFns};
487 /// use wynd::wynd::Wynd;
488 ///
489 /// #[tokio::main]
490 /// async fn main() {
491 /// let mut app = App::new();
492 /// let mut wynd = Wynd::new();
493 ///
494 /// // Configure WebSocket event handlers
495 /// wynd.on_connection(|conn| async move {
496 /// println!("New WebSocket connection");
497 ///
498 /// conn.on_text(|event, handle| async move {
499 /// println!("Received message: {}", event.data);
500 /// // Echo the message back
501 /// handle.send_text(event.data).await.ok();
502 /// });
503 ///
504 /// conn.on_close(|_event| async move {
505 /// println!("WebSocket connection closed");
506 /// });
507 /// });
508 ///
509 /// // Add regular HTTP routes
510 /// app.get("/", |_, res| async move {
511 /// res.ok().text("WebSocket server running")
512 /// });
513 ///
514 /// // Add WebSocket support at /ws
515 /// app.use_wynd("/ws", wynd.handler());
516 ///
517 /// app.listen(3000, || {
518 /// println!("Server with WebSocket support running on http://localhost:3000");
519 /// println!("WebSocket endpoint: ws://localhost:3000/ws");
520 /// }).await;
521 /// }
522 /// ```
523 ///
524 /// ## Client Connection
525 ///
526 /// Clients can connect to the WebSocket endpoint using:
527 ///
528 /// ```javascript
529 /// const ws = new WebSocket('ws://localhost:3000/ws');
530 /// ws.onmessage = (event) => console.log('Received:', event.data);
531 /// ws.send('Hello WebSocket!');
532 /// ```
533
534 pub fn use_wynd<F, Fut>(&mut self, path: &'static str, handler: F) -> &mut Self
535 where
536 F: Fn(hyper::Request<Full<Bytes>>) -> Fut + Send + Sync + 'static,
537 Fut: std::future::Future<Output = hyper::Result<hyper::Response<Full<hyper::body::Bytes>>>>
538 + Send
539 + 'static,
540 {
541 self.wynd_middleware = Some(WyndMiddleware {
542 func: Self::wynd_middleware_from_closure(handler),
543 path: path.to_string(),
544 });
545 self
546 }
547
548 /// Adds a rate limiting middleware to the application.
549 ///
550 /// Rate limiting helps protect your application from abuse by limiting the number
551 /// of requests a client can make within a specified time window. Requests exceeding
552 /// the limit are rejected with a 429 Too Many Requests status.
553 ///
554 /// ## Arguments
555 ///
556 /// * `config` - Optional [`RateLimiterConfig`] to customize rate limiting behavior.
557 /// If `None`, default settings are used.
558 ///
559 /// ## Example
560 ///
561 /// ```no_run
562 /// use ripress::app::App;
563 /// use ripress::middlewares::rate_limiter::RateLimiterConfig;
564 /// use std::time::Duration;
565 ///
566 ///
567 /// let mut app = App::new();
568 ///
569 /// // Use default rate limiting (typically 100 requests per minute)
570 /// app.use_rate_limiter(None);
571 ///
572 /// // Custom rate limiting configuration
573 /// app.use_rate_limiter(Some(RateLimiterConfig {
574 /// max_requests: 10, // Allow 10 requests
575 /// window_ms: Duration::from_secs(60), // Per 60 seconds
576 /// message: "Rate limit exceeded".to_string(),
577 /// ..Default::default()
578 /// }));
579 /// ```
580 ///
581 /// ## Default Behavior
582 ///
583 /// - Applied to all routes ("/")
584 /// - Executed as pre-middleware
585 /// - Uses client IP address for rate limiting
586 /// - Returns 429 Too Many Requests when limit is exceeded
587 /// - Includes rate limit headers in responses
588 ///
589 /// ## Rate Limit Headers
590 ///
591 /// The middleware adds these headers to responses:
592 /// - `X-RateLimit-Limit`: Maximum requests allowed
593 /// - `X-RateLimit-Remaining`: Requests remaining in current window
594 /// - `X-RateLimit-Reset`: Time when the rate limit window resets
595 pub fn use_rate_limiter(&mut self, config: Option<RateLimiterConfig>) -> &mut Self {
596 self.middlewares.push(Middleware {
597 func: Self::middleware_from_closure(rate_limiter(config)),
598 path: "/".to_string(),
599 middleware_type: MiddlewareType::Pre,
600 });
601 self
602 }
603
604 /// Adds a security middleware (shield) to the application.
605 ///
606 /// The shield middleware helps protect your application from common web vulnerabilities
607 /// by setting various HTTP security headers and applying security best practices. This
608 /// is essential for production applications.
609 ///
610 /// ## Arguments
611 ///
612 /// * `config` - Optional [`ShieldConfig`] to customize the shield middleware's behavior.
613 /// If `None`, secure default settings are applied.
614 ///
615 /// ## Example
616 ///
617 /// ```
618 /// use ripress::app::App;
619 /// use ripress::middlewares::shield::{ShieldConfig, Hsts};
620 ///
621 /// let mut app = App::new();
622 ///
623 /// // Use default shield settings (recommended for most applications)
624 /// app.use_shield(None);
625 ///
626 /// // Custom shield configuration
627 /// app.use_shield(Some(ShieldConfig {
628 /// hsts: Hsts {
629 /// enabled: true,
630 /// max_age: 31536000, // 1 year
631 /// include_subdomains: true,
632 /// preload: true,
633 /// ..Default::default()
634 /// },
635 /// ..Default::default()
636 /// }));
637 /// ```
638 ///
639 /// ## Security Headers Applied
640 ///
641 /// The shield middleware can set the following security headers:
642 ///
643 /// - `Strict-Transport-Security`: Forces HTTPS connections
644 /// - `X-Content-Type-Options`: Prevents MIME type sniffing
645 /// - `X-Frame-Options`: Prevents clickjacking attacks
646 /// - `X-XSS-Protection`: Enables cross-site scripting filtering
647 /// - `Referrer-Policy`: Controls referrer information
648 /// - `Content-Security-Policy`: Prevents various injection attacks
649 ///
650 /// ## Default Behavior
651 ///
652 /// - Applied to all routes ("/")
653 /// - Executed as pre-middleware
654 /// - Uses secure defaults suitable for most web applications
655 /// - Can be customized per security requirements
656 pub fn use_shield(&mut self, config: Option<ShieldConfig>) -> &mut Self {
657 self.middlewares.push(Middleware {
658 func: Self::middleware_from_closure(shield(config)),
659 path: "/".to_string(),
660 middleware_type: MiddlewareType::Pre,
661 });
662 self
663 }
664
665 /// Adds a compression middleware to the application.
666 ///
667 /// Compression middleware automatically compresses response bodies using algorithms
668 /// like gzip or deflate, reducing bandwidth usage and improving response times for
669 /// clients that support compression.
670 ///
671 /// ## Arguments
672 ///
673 /// * `config` - Optional [`CompressionConfig`] to customize compression behavior.
674 /// If `None`, default settings are used with common compression algorithms enabled.
675 ///
676 /// ## Example
677 ///
678 /// ```
679 /// use ripress::app::App;
680 /// use ripress::middlewares::compression::CompressionConfig;
681 ///
682 /// let mut app = App::new();
683 ///
684 /// // Use default compression settings (gzip, deflate)
685 /// app.use_compression(None);
686 ///
687 /// // Custom compression configuration
688 /// app.use_compression(Some(CompressionConfig {
689 /// level: 6, // Compression level (0-9)
690 /// threshold: 1024, // Minimum bytes to compress
691 /// ..Default::default()
692 /// }));
693 /// ```
694 ///
695 /// ## Default Behavior
696 ///
697 /// - Applied to all routes ("/")
698 /// - Executed as post-middleware (after response generation)
699 /// - Supports gzip and deflate compression
700 /// - Automatically negotiates compression based on `Accept-Encoding` header
701 /// - Only compresses responses above a minimum size threshold
702 /// - Skips compression for already-compressed content types
703 ///
704 /// ## Content Type Handling
705 ///
706 /// By default, the middleware:
707 /// - Compresses text-based content (HTML, CSS, JavaScript, JSON, XML)
708 /// - Skips binary content that's already compressed (images, videos, archives)
709 /// - Respects the client's `Accept-Encoding` header preferences
710 /// - Adds appropriate `Content-Encoding` headers to compressed responses
711 #[cfg(feature = "compression")]
712 pub fn use_compression(&mut self, config: Option<CompressionConfig>) -> &mut Self {
713 self.middlewares.push(Middleware {
714 func: Self::middleware_from_closure(compression(config)),
715 path: "/".to_string(),
716 middleware_type: MiddlewareType::Post,
717 });
718 self
719 }
720
721 /// Converts a closure into a middleware handler function.
722 ///
723 /// This is an internal helper method that wraps user-provided middleware functions
724 /// into the expected format for the middleware system.
725 fn middleware_from_closure<F, Fut>(f: F) -> HandlerMiddleware
726 where
727 F: Fn(HttpRequest, HttpResponse) -> Fut + Send + Sync + 'static,
728 Fut: std::future::Future<Output = (HttpRequest, Option<HttpResponse>)> + Send + 'static,
729 {
730 Arc::new(move |req, res| box_future_middleware(f(req, res)))
731 }
732
733 #[cfg(feature = "with-wynd")]
734 /// Converts a WebSocket handler closure into a Wynd middleware handler.
735 ///
736 /// This is an internal helper method for the WebSocket functionality.
737 fn wynd_middleware_from_closure<F, Fut>(f: F) -> WyndMiddlewareHandler
738 where
739 F: Fn(hyper::Request<Full<Bytes>>) -> Fut + Send + Sync + 'static,
740 Fut: std::future::Future<Output = hyper::Result<hyper::Response<Full<hyper::body::Bytes>>>>
741 + Send
742 + 'static,
743 {
744 Arc::new(move |req| Box::pin(f(req)))
745 }
746
747 /// Mounts a [`Router`] at a specific base path, registering all of its routes onto the application.
748 ///
749 /// This method allows you to modularly organize and group routes using separate routers,
750 /// then attach them to your application. Each route registered with the router will be
751 /// prefixed by the router's base path. This is useful for API versioning, feature groupings,
752 /// or splitting logic into modules. The router's routes are incorporated into the main
753 /// application's route table, and will take precedence over static file handlers.
754 ///
755 /// # Example
756 /// ```
757 /// use ripress::{app::App, router::Router};
758 /// use ripress::{req::HttpRequest, res::HttpResponse};
759 /// use ripress::types::RouterFns;
760 ///
761 /// async fn v1_status(_req: HttpRequest, res: HttpResponse) -> HttpResponse {
762 /// res.ok().json(serde_json::json!({"status": "ok"}))
763 /// }
764 ///
765 /// #[tokio::main]
766 /// async fn main() {
767 /// let mut api_router = Router::new("/api/v1");
768 /// api_router.get("/status", v1_status);
769 ///
770 /// let mut app = App::new();
771 /// app.router(api_router);
772 /// }
773 /// ```
774 ///
775 /// # Arguments
776 ///
777 /// * `router` - The [`Router`] instance whose routes will be registered onto this application.
778 ///
779 /// # Panics
780 ///
781 /// This method does not panic.
782 pub fn router(&mut self, mut router: Router) {
783 let base_path = router.base_path;
784 for (path, methods) in router.routes() {
785 for (method, handler) in methods.to_owned() {
786 let full_path = format!("{}{}", base_path, path);
787 self.add_route(method, &full_path, move |req, res| (handler)(req, res));
788 }
789 }
790 }
791
792 /// Configures static file serving for the application.
793 ///
794 /// This method allows you to serve static assets (HTML, CSS, JavaScript, images, etc.)
795 /// from the filesystem. Files are served with appropriate MIME types, caching headers,
796 /// and ETag support for efficient client-side caching.
797 ///
798 /// ## Arguments
799 ///
800 /// * `path` - The URL path where static files should be mounted (e.g., "/public", "/static", "/")
801 /// * `file` - The filesystem directory path containing the static files (e.g., "./public", "./dist")
802 ///
803 /// ## Returns
804 ///
805 /// * `Ok(())` - If the static file configuration was successful
806 /// * `Err(&'static str)` - If there was a validation error with the provided paths
807 ///
808 /// ## Errors
809 ///
810 /// This method returns an error in the following cases:
811 /// - `file` parameter is "/" (serving from filesystem root is blocked for security)
812 /// - `path` parameter is empty
813 /// - `file` parameter is empty
814 /// - `path` parameter doesn't start with "/"
815 ///
816 /// ## Example
817 ///
818 /// ```
819 /// use ripress::app::App;
820 ///
821 /// let mut app = App::new();
822 ///
823 /// // Serve files from ./public directory at /public URL path
824 /// app.static_files("/public", "./public").unwrap();
825 ///
826 /// // Serve CSS and JS assets
827 /// app.static_files("/assets", "./dist/assets").unwrap();
828 ///
829 /// // Serve a Single Page Application (SPA) from root
830 /// // API routes take precedence, static files serve as fallback
831 /// app.static_files("/", "./dist").unwrap();
832 ///
833 /// // Multiple static directories
834 /// app.static_files("/images", "./uploads/images").unwrap();
835 /// app.static_files("/docs", "./documentation").unwrap();
836 /// ```
837 ///
838 /// ## Behavior
839 ///
840 /// - **Route Precedence**: API routes defined with `get()`, `post()`, etc. take precedence over static files
841 /// - **Fallback Serving**: When mounted at "/", static files serve as fallback for unmatched routes
842 /// - **MIME Types**: Automatically sets appropriate `Content-Type` headers based on file extensions
843 /// - **Caching**: Includes `Cache-Control` and `ETag` headers for efficient browser caching
844 /// - **Security**: Prevents directory traversal attacks and blocks serving from filesystem root
845 ///
846 /// ## File System Layout Example
847 ///
848 /// ```text
849 /// project/
850 /// ├── src/main.rs
851 /// ├── public/ <- app.static_files("/public", "./public")
852 /// │ ├── index.html <- Accessible at /public/index.html
853 /// │ ├── style.css <- Accessible at /public/style.css
854 /// │ └── script.js <- Accessible at /public/script.js
855 /// └── dist/ <- app.static_files("/", "./dist")
856 /// ├── index.html <- Accessible at / (fallback)
857 /// └── favicon.ico <- Accessible at /favicon.ico
858 /// ```
859 ///
860 /// ## Security Considerations
861 ///
862 /// - Never use "/" as the `file` parameter - this is blocked for security reasons
863 /// - Use specific directories like "./public" or "./assets"
864 /// - The static file server prevents directory traversal (../) attacks automatically
865 /// - Consider using a reverse proxy like nginx for serving static files in production
866 pub fn static_files(
867 &mut self,
868 path: &'static str,
869 file: &'static str,
870 ) -> Result<(), &'static str> {
871 // Validate inputs
872 if file == "/" {
873 return Err("Serving from filesystem root '/' is not allowed for security reasons");
874 }
875 if path.is_empty() {
876 return Err("Mount path cannot be empty");
877 }
878 if file.is_empty() {
879 return Err("File path cannot be empty");
880 }
881 // Require paths to start with '/'
882 if !path.starts_with('/') {
883 return Err("Mount path must start with '/'");
884 }
885 self.static_files.insert(path, file);
886 Ok(())
887 }
888
889 /// Starts the HTTP server and begins listening for incoming requests.
890 ///
891 /// This method builds the complete router with all configured routes, middleware,
892 /// and static file handlers, then starts the HTTP server on the specified port.
893 /// The server runs indefinitely until the process is terminated.
894 ///
895 /// ## Arguments
896 ///
897 /// * `port` - The port number to listen on (e.g., 3000, 8080)
898 /// * `cb` - A callback function that's executed once the server is ready to accept connections
899 ///
900 /// ## Example
901 ///
902 /// ```no_run
903 /// use ripress::app::App;
904 /// use ripress::types::RouterFns;
905 ///
906 /// #[tokio::main]
907 /// async fn main() {
908 /// let mut app = App::new();
909 ///
910 /// app.get("/", |_req, res| async move {
911 /// res.ok().text("Hello, World!")
912 /// });
913 ///
914 /// app.get("/health", |_req, res| async move {
915 /// res.ok().json(serde_json::json!({"status": "healthy"}))
916 /// });
917 ///
918 /// // Start server with startup message
919 /// app.listen(3000, || {
920 /// println!("🚀 Server running on http://localhost:3000");
921 /// println!("📊 Health check: http://localhost:3000/health");
922 /// }).await;
923 /// }
924 /// ```
925 ///
926 /// ## Server Initialization Order
927 ///
928 /// 1. **WebSocket Middleware**: Applied first (if `wynd` feature is enabled)
929 /// 2. **Application Middleware**: Applied in registration order
930 /// - Pre-middleware (before route handlers)
931 /// - Post-middleware (after route handlers)
932 /// 3. **API Routes**: Registered with exact path matching
933 /// 4. **Static File Routes**: Registered as fallback handlers
934 /// 5. **Error Handler**: Global error handling for the application
935 ///
936 /// ## Network Configuration
937 ///
938 /// - **Bind Address**: The server binds to `127.0.0.1:port` (localhost only)
939 /// - **Protocol**: HTTP/1.1 (HTTP/2 support may be added in future versions)
940 /// - **Concurrent Connections**: Handled asynchronously with Tokio
941 ///
942 /// ## Error Handling
943 ///
944 /// If the server fails to start (e.g., port already in use), the error is printed
945 /// to stderr and the process continues. You may want to handle this more gracefully:
946 ///
947 /// ```no_run
948 /// # use ripress::app::App;
949 /// # #[tokio::main]
950 /// # async fn main() {
951 /// # let app = App::new();
952 /// // The server will print errors but won't panic
953 /// app.listen(3000, || println!("Server starting...")).await;
954 /// // This line is reached if server fails to start
955 /// eprintln!("Server failed to start or has shut down");
956 /// # }
957 /// ```
958 ///
959 /// ## Production Considerations
960 ///
961 /// - Consider using environment variables for port configuration
962 /// - Implement graceful shutdown handling
963 /// - Use a process manager like systemd or PM2
964 /// - Configure reverse proxy (nginx, Apache) for production
965 /// - Enable logging middleware to monitor requests
966 pub async fn listen<F: FnOnce()>(&self, port: u16, cb: F) {
967 let mut router = routerify_ng::Router::<ApiError>::builder();
968
969 #[cfg(feature = "with-wynd")]
970 if let Some(middleware) = &self.wynd_middleware {
971 router = router.middleware(routerify_ng::Middleware::pre({
972 use crate::helpers::exec_wynd_middleware;
973
974 let middleware = middleware.clone();
975 move |req| exec_wynd_middleware(req, middleware.clone())
976 }));
977 }
978
979 // Apply middlewares first
980 for middleware in self.middlewares.iter() {
981 let middleware = middleware.clone();
982
983 if middleware.middleware_type == MiddlewareType::Post {
984 router = router.middleware(routerify_ng::Middleware::post_with_info({
985 let middleware = middleware.clone();
986 move |res, info| exec_post_middleware(res, middleware.clone(), info)
987 }));
988 } else {
989 router = router.middleware(routerify_ng::Middleware::pre({
990 let middleware = middleware.clone();
991 move |req| exec_pre_middleware(req, middleware.clone())
992 }));
993 }
994 }
995
996 // Register API routes FIRST (before static files)
997 // This ensures API routes take precedence over static file serving
998 for (path, methods) in &self.routes {
999 for (method, handler) in methods {
1000 let handler = Arc::clone(handler);
1001
1002 let method = match method {
1003 HttpMethods::GET => Method::GET,
1004 HttpMethods::POST => Method::POST,
1005 HttpMethods::PUT => Method::PUT,
1006 HttpMethods::DELETE => Method::DELETE,
1007 HttpMethods::PATCH => Method::PATCH,
1008 HttpMethods::HEAD => Method::HEAD,
1009 HttpMethods::OPTIONS => Method::OPTIONS,
1010 };
1011
1012 router = router.add(path, vec![method], move |mut req| {
1013 let handler = Arc::clone(&handler);
1014
1015 async move {
1016 let mut our_req = match HttpRequest::from_hyper_request(&mut req).await {
1017 Ok(r) => r,
1018 Err(e) => {
1019 return Err(ApiError::Generic(
1020 HttpResponse::new().bad_request().text(e.to_string()),
1021 ));
1022 }
1023 };
1024
1025 req.params().iter().for_each(|(key, value)| {
1026 our_req.set_param(key, value);
1027 });
1028
1029 let response = handler(our_req, HttpResponse::new()).await;
1030
1031 let hyper_response = response.to_hyper_response().await;
1032 // Infallible means this can never fail, so unwrap is safe
1033 Ok(hyper_response.unwrap())
1034 }
1035 });
1036 }
1037 }
1038
1039 for (mount_path, serve_from) in self.static_files.iter() {
1040 let serve_from = (*serve_from).to_string();
1041 let mount_root = (*mount_path).to_string();
1042
1043 let route_pattern_owned = if mount_root == "/" {
1044 "/*".to_string()
1045 } else {
1046 format!("{}/{}", mount_root, "*")
1047 };
1048
1049 let route_pattern: &'static str = Box::leak(route_pattern_owned.into_boxed_str());
1050
1051 let serve_from_clone = serve_from.clone();
1052 let mount_root_clone = mount_root.clone();
1053
1054 router = router.get(route_pattern, move |req| {
1055 let serve_from = serve_from_clone.clone();
1056 let mount_root = mount_root_clone.clone();
1057 async move {
1058 match Self::serve_static_with_headers(req, mount_root, serve_from).await {
1059 Ok(res) => Ok(res),
1060 Err(e) => Err(ApiError::Generic(
1061 HttpResponse::new()
1062 .internal_server_error()
1063 .text(e.to_string()),
1064 )),
1065 }
1066 }
1067 });
1068 }
1069
1070 router = router.err_handler(Self::error_handler);
1071 let router = router.build().unwrap();
1072 cb();
1073
1074 let addr = SocketAddr::from(([127, 0, 0, 1], port));
1075
1076 let listener = TcpListener::bind(addr).await;
1077
1078 if let Err(e) = listener {
1079 eprintln!("Error binding to address {}: {}", addr, e);
1080 return;
1081 }
1082
1083 let listener = listener.unwrap();
1084
1085 let router_service = Arc::new(RouterService::new(router).unwrap());
1086
1087 if self.graceful_shutdown {
1088 let mut shutdown = Box::pin(tokio::signal::ctrl_c());
1089
1090 loop {
1091 tokio::select! {
1092 result = listener.accept() => {
1093 match result {
1094 Ok((stream, _)) => {
1095 let service = Arc::clone(&router_service);
1096
1097 tokio::task::spawn(async move {
1098 // Now service is Arc<RouterService> and not moved
1099 let request_service = match service.call(&stream).await {
1100 Ok(svc) => svc,
1101 Err(err) => {
1102 eprintln!("Error creating per-connection service: {:?}", err);
1103 return;
1104 }
1105 };
1106
1107
1108 // Wrap the stream in TokioIo for hyper
1109 let io = TokioIo::new(stream);
1110 let mut builder = Builder::new();
1111 builder.keep_alive(true);
1112
1113 // Serve the connection with upgrades enabled for WebSocket support
1114 let connection = builder.serve_connection(io, request_service).with_upgrades();
1115 if let Err(err) = connection.await {
1116 eprintln!("Error serving connection: {:?}", err);
1117 }
1118 });
1119 }
1120 Err(e) => {
1121 eprintln!("Error accepting connection: {}", e);
1122 }
1123 }
1124 }
1125 _ = shutdown.as_mut() => {
1126 break;
1127 }
1128 }
1129 }
1130 } else {
1131 loop {
1132 match listener.accept().await {
1133 Ok((stream, _)) => {
1134 let service = Arc::clone(&router_service);
1135
1136 tokio::task::spawn(async move {
1137 let request_service = match service.call(&stream).await {
1138 Ok(svc) => svc,
1139 Err(err) => {
1140 eprintln!("Error creating per-connection service: {:?}", err);
1141 return;
1142 }
1143 };
1144
1145 // Wrap the stream in TokioIo for hyper
1146 let io = TokioIo::new(stream);
1147 let mut builder = Builder::new();
1148 builder.keep_alive(true);
1149
1150 // Serve the connection with upgrades enabled for WebSocket support
1151 let connection = builder
1152 .serve_connection(io, request_service)
1153 .with_upgrades();
1154 if let Err(err) = connection.await {
1155 eprintln!("Error serving connection: {:?}", err);
1156 }
1157 });
1158 }
1159 Err(e) => {
1160 eprintln!("Error accepting connection: {}", e);
1161 }
1162 }
1163 }
1164 }
1165 }
1166
1167 /// Internal error handler for the router.
1168 ///
1169 /// This method processes routing errors and converts them into appropriate HTTP responses.
1170 /// It handles both generic API errors and unexpected system errors.
1171 pub(crate) async fn error_handler(
1172 err: routerify_ng::RouteError,
1173 ) -> Response<Full<hyper::body::Bytes>> {
1174 let api_err = err.downcast::<ApiError>().unwrap_or_else(|_| {
1175 return Box::new(ApiError::Generic(
1176 HttpResponse::new()
1177 .internal_server_error()
1178 .text("Unhandled error"),
1179 ));
1180 });
1181
1182 println!("api_err: {:?}", api_err);
1183
1184 // For WebSocket upgrades, we need to take ownership to avoid breaking the upgrade mechanism
1185 // Cloning the response breaks the upgrade connection, so we must move it
1186 match *api_err {
1187 ApiError::WebSocketUpgrade(response) => {
1188 // Return the response directly without cloning to preserve the upgrade mechanism
1189 response
1190 }
1191 ApiError::Generic(res) => {
1192 let hyper_res = <HttpResponse as Clone>::clone(&res)
1193 .to_hyper_response()
1194 .await
1195 .map_err(ApiError::from)
1196 .unwrap();
1197
1198 hyper_res
1199 }
1200 }
1201 }
1202
1203 /// Internal method for serving static files with proper headers and caching support.
1204 ///
1205 /// This method handles the complex logic of serving static files, including:
1206 /// - URL path rewriting to map mount points to filesystem paths
1207 /// - ETag-based conditional requests (304 Not Modified responses)
1208 /// - Proper caching headers
1209 /// - Error handling for missing files
1210 ///
1211 /// ## Arguments
1212 ///
1213 /// * `req` - The incoming HTTP request
1214 /// * `mount_root` - The URL path where static files are mounted
1215 /// * `fs_root` - The filesystem directory containing the static files
1216 ///
1217 /// ## Returns
1218 ///
1219 /// * `Ok(Response<Body>)` - Successfully served file or 304 Not Modified
1220 /// * `Err(std::io::Error)` - File not found or other I/O error
1221 pub(crate) async fn serve_static_with_headers<B>(
1222 req: Request<B>,
1223 mount_root: String,
1224 fs_root: String,
1225 ) -> Result<Response<Full<hyper::body::Bytes>>, std::io::Error>
1226 where
1227 B: hyper::body::Body<Data = hyper::body::Bytes> + Send + 'static,
1228 B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
1229 {
1230 // Rewrite the request URI by stripping the mount_root prefix so that
1231 // "/static/index.html" maps to "fs_root/index.html" rather than
1232 // "fs_root/static/index.html".
1233 let (mut parts, body) = req.into_parts();
1234 let original_uri = parts.uri.clone();
1235 let original_path = original_uri.path();
1236 let if_none_match = parts
1237 .headers
1238 .get(header::IF_NONE_MATCH)
1239 .and_then(|v| v.to_str().ok())
1240 .map(|s| s.to_string());
1241
1242 let trimmed_path = if mount_root == "/" {
1243 // If mounting at root, serve the path as-is
1244 original_path
1245 } else if original_path.starts_with(&mount_root) {
1246 // Strip the mount root prefix, but ensure we don't create an empty path
1247 let remaining = &original_path[mount_root.len()..];
1248 if remaining.is_empty() { "/" } else { remaining }
1249 } else {
1250 // Path doesn't match mount root - this shouldn't happen in normal routing
1251 original_path
1252 };
1253
1254 let normalized_path = if trimmed_path.is_empty() {
1255 "/"
1256 } else {
1257 trimmed_path
1258 };
1259
1260 let new_path_and_query = if let Some(query) = original_uri.query() {
1261 format!("{}?{}", normalized_path, query)
1262 } else {
1263 normalized_path.to_string()
1264 };
1265
1266 parts.uri = match new_path_and_query.parse() {
1267 Ok(uri) => uri,
1268 Err(e) => {
1269 eprintln!(
1270 "Error parsing URI: {} (original: {}, mount_root: {}, trimmed: {}, normalized: {})",
1271 e, original_path, mount_root, trimmed_path, normalized_path
1272 );
1273 return Err(std::io::Error::new(
1274 std::io::ErrorKind::InvalidInput,
1275 format!("Invalid URI after rewriting: {}", e),
1276 ));
1277 }
1278 };
1279
1280 let rewritten_req = Request::from_parts(parts, body);
1281
1282 let static_service = Static::new(Path::new(fs_root.as_str()));
1283
1284 match static_service.serve(rewritten_req).await {
1285 Ok(mut response) => {
1286 response
1287 .headers_mut()
1288 .insert("Cache-Control", "public, max-age=86400".parse().unwrap());
1289 response
1290 .headers_mut()
1291 .insert("X-Served-By", "hyper-staticfile".parse().unwrap());
1292 // Handle conditional request with If-None-Match since hyper-staticfile 0.9
1293 // does not evaluate it. If ETag matches, return 304 with empty body.
1294 if let Some(if_none_match_value) = if_none_match {
1295 if let Some(etag) = response.headers().get(header::ETAG) {
1296 if let Ok(etag_value) = etag.to_str() {
1297 if if_none_match_value == etag_value {
1298 let mut builder =
1299 Response::builder().status(StatusCode::NOT_MODIFIED);
1300 if let Some(h) = builder.headers_mut() {
1301 // carry forward ETag, Cache-Control, Last-Modified, etc.
1302 for (k, v) in response.headers().iter() {
1303 h.insert(k.clone(), v.clone());
1304 }
1305 h.remove(header::CONTENT_LENGTH);
1306 }
1307 return Ok(builder.body(Full::from(Bytes::new())).unwrap());
1308 }
1309 }
1310 }
1311 }
1312 // Convert hyper_staticfile::Body to Full<Bytes>
1313 let (parts, body) = response.into_parts();
1314 let collected = body.collect().await.map_err(|e| {
1315 std::io::Error::new(
1316 std::io::ErrorKind::Other,
1317 format!("Failed to collect body: {}", e),
1318 )
1319 })?;
1320 let body_bytes = collected.to_bytes();
1321 let full_body = Full::from(body_bytes);
1322 Ok(Response::from_parts(parts, full_body))
1323 }
1324 Err(e) => Err(e),
1325 }
1326 }
1327
1328 /// Internal method for building a router instance.
1329 ///
1330 /// This is used internally for testing and development purposes.
1331 pub(crate) fn _build_router(&self) -> routerify_ng::Router<ApiError> {
1332 routerify_ng::Router::builder()
1333 .err_handler(Self::error_handler)
1334 .build()
1335 .unwrap()
1336 }
1337}