ripress 2.5.1

An Express.js-inspired web framework for Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
//! # App Module
//!
//! The core application module for Ripress, providing Express.js-like functionality
//! for building HTTP servers in Rust. This module contains the main [`App`] struct
//! and [`Middleware`] definitions that form the foundation of a Ripress web application.
//!
//! ## Key Features
//!
//! - Express.js-like routing and middleware system
//! - Built-in middleware for common tasks (CORS, logging, rate limiting, etc.)
//! - Static file serving capabilities
//! - WebSocket support (with `wynd` feature)
//! - Async/await support throughout
//!
//! ## Basic Usage
//!
//! ```no_run
//! use ripress::app::App;
//! use ripress::types::RouterFns;
//! use ripress::req::HttpRequest;
//!
//! #[tokio::main]
//! async fn main() {
//!     let mut app = App::new();
//!
//!     app.get("/", |_req: HttpRequest, res| async move {
//!         res.ok().text("Hello, World!")
//!     });
//!
//!     app.listen(3000, || {
//!         println!("Server running on http://localhost:3000");
//!     }).await;
//! }
//! ```

#![warn(missing_docs)]

use crate::app::{api_error::ApiError, settings::Http2Config};
use std::cell::RefCell;

use crate::{
    helpers::{exec_post_middleware, exec_pre_middleware},
    middlewares::{Middleware, MiddlewareType},
    req::HttpRequest,
    res::HttpResponse,
    router::Router,
    types::{HttpMethods, RouterFns, Routes},
};
use bytes::Bytes;
use http_body_util::{BodyExt, Full};
use hyper::{header, http::StatusCode, Method, Request, Response};
use hyper_staticfile::Static;
use routerify_ng::{ext::RequestExt, RouterService};
use settings::AppSettings;
use std::{collections::HashMap, net::SocketAddr, path::Path, sync::Arc};
use tokio::net::TcpListener;

pub(crate) mod api_error;

mod h2;
/// Handler module for managing server connections, HTTP/2/1 serving logic, and connection-level configuration.
pub mod handler;
/// Middleware support for the App struct, including common and user-defined middleware functionality.
pub mod middlewares;
/// Module for defining the settings of the App Struct.
pub mod settings;

/// The App struct is the core of Ripress, providing a simple interface for creating HTTP servers and handling requests.
///
/// It follows an Express-like pattern for route handling and middleware management. The App struct
/// manages routes, middlewares, static file serving, and server lifecycle.
///
/// ## Features
///
/// - **Routing**: HTTP method-based routing (GET, POST, PUT, DELETE, etc.)
/// - **Middleware**: Pre and post-processing middleware with path-based matching
/// - **Static Files**: Serve static assets with proper headers and caching
/// - **WebSocket Support**: Optional WebSocket support via the `wynd` crate
/// - **Built-in Middleware**: CORS, logging, rate limiting, compression, and security headers
///
/// ## Example
///
/// ```ignore
/// use ripress::app::App;
/// use ripress::types::RouterFns;
/// use ripress::req::HttpRequest;
///
/// #[tokio::main]
/// async fn main() {
///     let mut app = App::new();
///
///     // Add middleware
///     app.use_cors(None);
///     app.use_logger(None);
///
///     // Add routes
///     app.get("/", |_req: HttpRequest, res| async move {
///         res.ok().text("Hello, World!")
///     });
///
///     app.post("/api/users", |req: HttpRequest, res| async move {
///         // Handle user creation
///         res.ok().json("User created")
///     });
///
///     // Serve static files
///     app.static_files("/public", "./public").unwrap();
///
///     // Start server
///     app.listen(3000, || {
///         println!("Server running on http://localhost:3000");
///     }).await;
/// }
/// ```
pub struct App {
    routes: Routes,
    pub(crate) middlewares: Vec<Arc<Middleware>>,
    pub(crate) settings: AppSettings,
}

impl RouterFns for App {
    fn routes(&mut self) -> &mut Routes {
        &mut self.routes
    }
}

impl App {
    /// Creates a new App instance with empty routes and middleware.
    ///
    /// This is the starting point for building a Ripress application. The returned
    /// App instance has no routes or middleware configured and is ready to be customized.
    ///
    /// ## Example
    ///
    /// ```
    /// use ripress::app::App;
    ///
    /// let mut app = App::new();
    /// ```
    pub fn new() -> Self {
        App {
            routes: HashMap::new(),
            middlewares: Vec::new(),
            settings: AppSettings::default(),
        }
    }

    /// Sets the host address for the server to bind to.
    ///
    /// This method allows you to specify the network interface (host) that the Ripress server will listen on.
    /// By default, the server binds to `"0.0.0.0"` (all interfaces). You may want to bind to
    /// `"127.0.0.1"` (localhost only) or an external IP for remote access, depending on your deployment requirements.
    ///
    /// **Note:** If you use an empty string (`""`), the server may not bind properly. Use valid IPv4 or IPv6 addresses.
    ///
    /// # Arguments
    ///
    /// * `host` - The host address (e.g., `"127.0.0.1"`, `"0.0.0.0"`, or an IPv6 address like `"::1"`).
    ///
    /// # Examples
    ///
    /// ```
    /// use ripress::app::App;
    /// let app = App::new().host("127.0.0.1");
    /// ```
    pub fn host(&mut self, host: &str) -> &mut Self {
        self.settings.host = host.to_string();
        self
    }

    /// Applies advanced HTTP/2 configuration for the application.
    ///
    /// This method allows fine-tuning of HTTP/2 behavior such as maximum
    /// concurrent streams, flow-control windows, and keep-alive settings.
    /// All fields in [`Http2Config`] are optional; any `None` values will
    /// cause Hyper's defaults to be used for that setting.
    ///
    /// # Examples
    ///
    /// ```
    /// use std::time::Duration;
    /// use ripress::app::{App, settings::Http2Config};
    ///
    /// let mut app = App::new();
    ///
    /// app.http2_config(Http2Config {
    ///         http2_only: false,
    ///         max_concurrent_streams: Some(100),
    ///         keep_alive_interval: Some(Duration::from_secs(30)),
    ///         keep_alive_timeout: Some(Duration::from_secs(10)),
    ///         ..Default::default()
    ///     });
    /// ```
    pub fn http2_config(&mut self, config: Http2Config) -> &mut Self {
        self.settings.http2_config = config;
        self
    }

    /// Enables graceful shutdown for the application.
    ///
    /// When graceful shutdown is enabled, the server will listen for a shutdown signal
    /// (such as Ctrl+C) and attempt to shut down cleanly, finishing any in-flight requests
    /// before exiting. This is useful for production environments where you want to avoid
    /// abruptly terminating active connections.
    ///
    /// ## Example
    ///
    /// ```
    /// use ripress::app::App;
    ///
    /// let mut app = App::new();
    /// app.with_graceful_shutdown();
    /// ```
    pub fn with_graceful_shutdown(&mut self) {
        self.settings.graceful_shutdown = true
    }

    /// Mounts a [`Router`] at a specific base path, registering all of its routes onto the application.
    ///
    /// This method allows you to modularly organize and group routes using separate routers,
    /// then attach them to your application. Each route registered with the router will be
    /// prefixed by the router's base path. This is useful for API versioning, feature groupings,
    /// or splitting logic into modules. The router's routes are incorporated into the main
    /// application's route table, and will take precedence over static file handlers.
    ///
    /// # Example
    /// ```
    /// use ripress::{app::App, router::Router};
    /// use ripress::{req::HttpRequest, res::HttpResponse};
    /// use ripress::types::RouterFns;
    ///
    /// async fn v1_status(_req: HttpRequest, res: HttpResponse) -> HttpResponse {
    ///     res.ok().json(serde_json::json!({"status": "ok"}))
    /// }
    ///
    /// #[tokio::main]
    /// async fn main() {
    ///     let mut api_router = Router::new("/api/v1");
    ///     api_router.get("/status", v1_status);
    ///     
    ///     let mut app = App::new();
    ///     app.router(api_router);
    /// }
    /// ```
    ///
    /// # Arguments
    ///
    /// * `router` - The [`Router`] instance whose routes will be registered onto this application.
    ///
    /// # Panics
    ///
    /// This method does not panic.
    pub fn router(&mut self, mut router: Router) {
        let base_path = router.base_path;
        for (path, methods) in router.routes() {
            for (method, handler) in methods.to_owned() {
                if path == "/" {
                    self.add_route(method, &base_path, move |req: HttpRequest, res| {
                        (handler)(req, res)
                    });
                } else {
                    let full_path = format!("{}{}", base_path, path);
                    self.add_route(method, &full_path, move |req: HttpRequest, res| {
                        (handler)(req, res)
                    });
                }
            }
        }
    }

    /// Configures static file serving for the application.
    ///
    /// This method allows you to serve static assets (HTML, CSS, JavaScript, images, etc.)
    /// from the filesystem. Files are served with appropriate MIME types, caching headers,
    /// and ETag support for efficient client-side caching.
    ///
    /// ## Arguments
    ///
    /// * `path` - The URL path where static files should be mounted (e.g., "/public", "/static", "/")
    /// * `file` - The filesystem directory path containing the static files (e.g., "./public", "./dist")
    ///
    /// ## Returns
    ///
    /// * `Ok(())` - If the static file configuration was successful
    /// * `Err(&'static str)` - If there was a validation error with the provided paths
    ///
    /// ## Errors
    ///
    /// This method returns an error in the following cases:
    /// - `file` parameter is "/" (serving from filesystem root is blocked for security)
    /// - `path` parameter is empty
    /// - `file` parameter is empty
    /// - `path` parameter doesn't start with "/"
    ///
    /// ## Example
    ///
    /// ```
    /// use ripress::app::App;
    ///
    /// let mut app = App::new();
    ///
    /// // Serve files from ./public directory at /public URL path
    /// app.static_files("/public", "./public").unwrap();
    ///
    /// // Serve CSS and JS assets
    /// app.static_files("/assets", "./dist/assets").unwrap();
    ///
    /// // Serve a Single Page Application (SPA) from root
    /// // API routes take precedence, static files serve as fallback
    /// app.static_files("/", "./dist").unwrap();
    ///
    /// // Multiple static directories
    /// app.static_files("/images", "./uploads/images").unwrap();
    /// app.static_files("/docs", "./documentation").unwrap();
    /// ```
    ///
    /// ## Behavior
    ///
    /// - **Route Precedence**: API routes defined with `get()`, `post()`, etc. take precedence over static files
    /// - **Fallback Serving**: When mounted at "/", static files serve as fallback for unmatched routes
    /// - **MIME Types**: Automatically sets appropriate `Content-Type` headers based on file extensions
    /// - **Caching**: Includes `Cache-Control` and `ETag` headers for efficient browser caching
    /// - **Security**: Prevents directory traversal attacks and blocks serving from filesystem root
    ///
    /// ## File System Layout Example
    ///
    /// ```text
    /// project/
    /// ├── src/main.rs
    /// ├── public/           <- app.static_files("/public", "./public")
    /// │   ├── index.html    <- Accessible at /public/index.html
    /// │   ├── style.css     <- Accessible at /public/style.css
    /// │   └── script.js     <- Accessible at /public/script.js
    /// └── dist/             <- app.static_files("/", "./dist")
    ///     ├── index.html    <- Accessible at / (fallback)
    ///     └── favicon.ico   <- Accessible at /favicon.ico
    /// ```
    ///
    /// ## Security Considerations
    ///
    /// - Never use "/" as the `file` parameter - this is blocked for security reasons
    /// - Use specific directories like "./public" or "./assets"
    /// - The static file server prevents directory traversal (../) attacks automatically
    /// - Consider using a reverse proxy like nginx for serving static files in production
    pub fn static_files(
        &mut self,
        path: &'static str,
        file: &'static str,
    ) -> Result<(), &'static str> {
        if file == "/" {
            return Err("Serving from filesystem root '/' is not allowed for security reasons");
        }
        if path.is_empty() {
            return Err("Mount path cannot be empty");
        }
        if file.is_empty() {
            return Err("File path cannot be empty");
        }
        if !path.starts_with('/') {
            return Err("Mount path must start with '/'");
        }
        self.settings.static_files.insert(path, file);
        Ok(())
    }

    /// Disables HTTP/2 support for the application.
    ///
    /// This method disables HTTP/2 support for the application.
    /// HTTP/2 support can be re-enabled by calling [`enable_http2`].
    ///
    /// ## Example
    ///
    /// ```rust
    /// use ripress::app::App;
    ///
    /// let mut app = App::new();
    /// app.disable_http2();
    /// ```
    #[inline]
    pub fn disable_http2(&mut self) -> &mut Self {
        self.settings.http2_config.is_enabled = false;
        self
    }

    /// Starts the HTTP server and begins listening for incoming requests.
    ///
    /// This method builds the complete router with all configured routes, middleware,
    /// and static file handlers, then starts the HTTP server on the specified port.
    /// The server runs indefinitely until the process is terminated.
    ///
    /// ## Arguments
    ///
    /// * `port` - The port number to listen on (e.g., 3000, 8080)
    /// * `cb` - A callback function that's executed once the server is ready to accept connections
    ///
    /// ## Example
    ///
    /// ```no_run
    /// use ripress::app::App;
    /// use ripress::types::RouterFns;
    /// use ripress::req::HttpRequest;
    ///
    /// #[tokio::main]
    /// async fn main() {
    ///     let mut app = App::new();
    ///
    ///     app.get("/", |_req: HttpRequest, res| async move {
    ///         res.ok().text("Hello, World!")
    ///     });
    ///
    ///     app.get("/health", |_req: HttpRequest, res| async move {
    ///         res.ok().json(serde_json::json!({"status": "healthy"}))
    ///     });
    ///
    ///     // Start server with startup message
    ///     app.listen(3000, || {
    ///         println!("🚀 Server running on http://localhost:3000");
    ///         println!("📊 Health check: http://localhost:3000/health");
    ///     }).await;
    /// }
    /// ```
    ///
    /// ## Server Initialization Order
    ///
    /// 1. **WebSocket Middleware**: Applied first (if `with-wynd` feature is enabled)
    /// 2. **Application Middleware**: Applied in registration order
    ///    - Pre-middleware (before route handlers)
    ///    - Post-middleware (after route handlers)
    /// 3. **API Routes**: Registered with exact path matching
    /// 4. **Static File Routes**: Registered as fallback handlers
    /// 5. **Error Handler**: Global error handling for the application
    ///
    /// ## Network Configuration
    ///
    /// - **Bind Address**: By default, binds to `0.0.0.0:port` (all interfaces); configurable via [`App::host`]
    /// - **Protocols**: HTTP/1.1 and HTTP/2 by default; Can be disabled via [`App::disable_http2`]
    /// - **Concurrent Connections**: Handled asynchronously with Tokio
    ///
    /// ## Error Handling
    ///
    /// If the server fails to start (e.g., port already in use), the error is printed
    /// to stderr and the process continues. You may want to handle this more gracefully:
    ///
    /// ```no_run
    /// # use ripress::app::App;
    /// # #[tokio::main]
    /// # async fn main() {
    /// # let app = App::new();
    /// // The server will print errors but won't panic
    /// app.listen(3000, || println!("Server starting...")).await;
    /// // This line is reached if server fails to start
    /// eprintln!("Server failed to start or has shut down");
    /// # }
    /// ```
    ///
    /// ## Production Considerations
    ///
    /// - Consider using environment variables for port configuration
    /// - Implement graceful shutdown handling
    /// - Use a process manager like systemd or PM2
    /// - Configure reverse proxy (nginx, Apache) for production
    /// - Enable logging middleware to monitor requests
    pub async fn listen<F: FnOnce()>(&self, port: u16, cb: F) {
        let mut router = routerify_ng::Router::<ApiError>::builder();

        #[cfg(feature = "with-wynd")]
        if let Some(middleware) = self.settings.wynd_config.clone() {
            router = router.middleware(routerify_ng::Middleware::pre({
                use crate::helpers::exec_wynd_middleware;

                let middleware = Arc::new(middleware);
                move |req| exec_wynd_middleware(req, Arc::clone(&middleware))
            }));
        }

        for middleware in &self.middlewares {
            match middleware.middleware_type {
                MiddlewareType::Post => {
                    let middleware = Arc::clone(middleware);
                    router = router.middleware(routerify_ng::Middleware::post_with_info(
                        move |res, info| exec_post_middleware(res, Arc::clone(&middleware), info),
                    ));
                }
                _ => {
                    let middleware = Arc::clone(middleware);
                    router = router.middleware(routerify_ng::Middleware::pre(move |req| {
                        exec_pre_middleware(req, Arc::clone(&middleware))
                    }));
                }
            }
        }

        for (path, methods) in &self.routes {
            for (method, handler) in methods {
                let handler = Arc::clone(handler);

                let method = match method {
                    HttpMethods::GET => Method::GET,
                    HttpMethods::POST => Method::POST,
                    HttpMethods::PUT => Method::PUT,
                    HttpMethods::DELETE => Method::DELETE,
                    HttpMethods::PATCH => Method::PATCH,
                    HttpMethods::HEAD => Method::HEAD,
                    HttpMethods::OPTIONS => Method::OPTIONS,
                };

                router = router.add(path, vec![method], move |mut req| {
                    let handler = Arc::clone(&handler);

                    async move {
                        let mut our_req = match HttpRequest::from_hyper_request(&mut req).await {
                            Ok(r) => r,
                            Err(e) => {
                                return Err(ApiError::Generic(
                                    HttpResponse::new().bad_request().text(e.to_string()),
                                ));
                            }
                        };

                        req.params().iter().for_each(|(key, value)| {
                            our_req.set_param(key, value);
                        });

                        let mut response = handler(our_req, HttpResponse::new()).await;

                        let _ = crate::next::PENDING_HEADERS.try_with(|pending| {
                            for (k, v) in pending.borrow_mut().drain(..) {
                                response = std::mem::take(&mut response).set_header(k, v);
                            }
                        });
                        let _ = crate::next::PENDING_COOKIES.try_with(|pending| {
                            for cookie in pending.borrow_mut().drain(..) {
                                response = std::mem::take(&mut response).set_cookie_raw(cookie);
                            }
                        });

                        let hyper_response = response.to_hyper_response().await;
                        Ok(hyper_response.unwrap())
                    }
                });
            }
        }

        for (mount_path, serve_from) in self.settings.static_files.iter() {
            let serve_from = (*serve_from).to_string();
            let mount_root = (*mount_path).to_string();

            let route_pattern_owned = if mount_root == "/" {
                "/*".to_string()
            } else {
                format!("{}/{}", mount_root, "*")
            };

            let serve_from_clone = serve_from.clone();
            let mount_root_clone = mount_root.clone();

            router = router.get(route_pattern_owned, move |req| {
                let serve_from = serve_from_clone.clone();
                let mount_root = mount_root_clone.clone();
                async move {
                    match Self::serve_static_with_headers(req, mount_root, serve_from).await {
                        Ok(res) => Ok(res),
                        Err(e) => Err(ApiError::Generic(
                            HttpResponse::new()
                                .internal_server_error()
                                .text(e.to_string()),
                        )),
                    }
                }
            });
        }

        router = router.err_handler(Self::error_handler);
        let router = router.build().unwrap();
        cb();

        let addr = format!("{}:{}", self.settings.host, port)
            .parse::<SocketAddr>()
            .unwrap();

        let listener = TcpListener::bind(addr).await;

        if let Err(e) = listener {
            eprintln!("Error binding to address {}: {}", addr, e);
            return;
        }

        let listener = listener.unwrap();

        let router_service = Arc::new(RouterService::new(router).unwrap());

        let http2_enabled = self.settings.http2_config.is_enabled;
        let http2_config = self.settings.http2_config.clone();

        let mut shutdown = if self.settings.graceful_shutdown {
            Some(Box::pin(tokio::signal::ctrl_c()))
        } else {
            None
        };

        loop {
            let accept_result = if let Some(ref mut sig) = shutdown {
                tokio::select! {
                    result = listener.accept() => Some(result),
                    _ = sig.as_mut() => None,
                }
            } else {
                Some(listener.accept().await)
            };

            match accept_result {
                Some(Ok((stream, _))) => {
                    let service = Arc::clone(&router_service);
                    let http2_config = http2_config.clone();

                    tokio::task::spawn(async move {
                        crate::next::PENDING_HEADERS.scope(
                            RefCell::new(Vec::new()),
                            crate::next::PENDING_COOKIES.scope(
                                RefCell::new(Vec::new()),
                                Self::handle_connection(stream, service, http2_enabled, http2_config),
                            ),
                        )
                        .await;
                    });
                }
                Some(Err(e)) => {
                    eprintln!("Error accepting connection: {}", e);
                }
                None => {
                    break;
                }
            }
        }
    }

    /// Internal error handler for the router.
    ///
    /// This method processes routing errors and converts them into appropriate HTTP responses.
    /// It handles both generic API errors and unexpected system errors.
    pub(crate) async fn error_handler(
        err: routerify_ng::RouteError,
    ) -> Response<Full<hyper::body::Bytes>> {
        let api_err = err.downcast::<ApiError>().unwrap_or_else(|_| {
            return Box::new(ApiError::Generic(
                HttpResponse::new()
                    .internal_server_error()
                    .text("Unhandled error"),
            ));
        });

        match *api_err {
            ApiError::WebSocketUpgrade(response) => response,
            ApiError::Generic(res) => {
                let hyper_res = <HttpResponse as Clone>::clone(&res)
                    .to_hyper_response()
                    .await
                    .map_err(ApiError::from)
                    .unwrap();

                hyper_res
            }
        }
    }

    /// Internal method for serving static files with proper headers and caching support.
    ///
    /// This method handles the complex logic of serving static files, including:
    /// - URL path rewriting to map mount points to filesystem paths
    /// - ETag-based conditional requests (304 Not Modified responses)
    /// - Proper caching headers
    /// - Error handling for missing files
    ///
    /// ## Arguments
    ///
    /// * `req` - The incoming HTTP request
    /// * `mount_root` - The URL path where static files are mounted
    /// * `fs_root` - The filesystem directory containing the static files
    ///
    /// ## Returns
    ///
    /// * `Ok(Response<Body>)` - Successfully served file or 304 Not Modified
    /// * `Err(std::io::Error)` - File not found or other I/O error
    pub(crate) async fn serve_static_with_headers<B>(
        req: Request<B>,
        mount_root: String,
        fs_root: String,
    ) -> Result<Response<Full<hyper::body::Bytes>>, std::io::Error>
    where
        B: hyper::body::Body<Data = hyper::body::Bytes> + Send + 'static,
        B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
    {
        let (mut parts, body) = req.into_parts();
        let original_uri = parts.uri.clone();
        let original_path = original_uri.path();
        let if_none_match = parts
            .headers
            .get(header::IF_NONE_MATCH)
            .and_then(|v| v.to_str().ok())
            .map(|s| s.to_string());

        let trimmed_path = if mount_root == "/" {
            original_path
        } else if original_path.starts_with(&mount_root) {
            let remaining = &original_path[mount_root.len()..];
            if remaining.is_empty() {
                "/"
            } else {
                remaining
            }
        } else {
            original_path
        };

        let normalized_path = if trimmed_path.is_empty() {
            "/"
        } else {
            trimmed_path
        };

        let new_path_and_query = if let Some(query) = original_uri.query() {
            format!("{}?{}", normalized_path, query)
        } else {
            normalized_path.to_string()
        };

        parts.uri = match new_path_and_query.parse() {
            Ok(uri) => uri,
            Err(e) => {
                eprintln!(
                    "Error parsing URI: {} (original: {}, mount_root: {}, trimmed: {}, normalized: {})",
                    e, original_path, mount_root, trimmed_path, normalized_path
                );
                return Err(std::io::Error::new(
                    std::io::ErrorKind::InvalidInput,
                    format!("Invalid URI after rewriting: {}", e),
                ));
            }
        };

        let rewritten_req = Request::from_parts(parts, body);

        let static_service = Static::new(Path::new(fs_root.as_str()));

        match static_service.serve(rewritten_req).await {
            Ok(mut response) => {
                response
                    .headers_mut()
                    .insert("Cache-Control", "public, max-age=86400".parse().unwrap());
                response
                    .headers_mut()
                    .insert("X-Served-By", "hyper-staticfile".parse().unwrap());
                if let Some(if_none_match_value) = if_none_match {
                    if let Some(etag) = response.headers().get(header::ETAG) {
                        if let Ok(etag_value) = etag.to_str() {
                            if if_none_match_value == etag_value {
                                let mut builder =
                                    Response::builder().status(StatusCode::NOT_MODIFIED);
                                if let Some(h) = builder.headers_mut() {
                                    for (k, v) in response.headers().iter() {
                                        h.insert(k.clone(), v.clone());
                                    }
                                    h.remove(header::CONTENT_LENGTH);
                                }
                                return Ok(builder.body(Full::from(Bytes::new())).unwrap());
                            }
                        }
                    }
                }
                let (parts, body) = response.into_parts();
                let collected = body.collect().await.map_err(|e| {
                    std::io::Error::new(
                        std::io::ErrorKind::Other,
                        format!("Failed to collect body: {}", e),
                    )
                })?;
                let body_bytes = collected.to_bytes();
                let full_body = Full::from(body_bytes);
                Ok(Response::from_parts(parts, full_body))
            }
            Err(e) => Err(e),
        }
    }

    /// Internal method for building a router instance.
    ///
    /// This is used internally for testing and development purposes.
    pub(crate) fn _build_router(&self) -> routerify_ng::Router<ApiError> {
        routerify_ng::Router::builder()
            .err_handler(Self::error_handler)
            .build()
            .unwrap()
    }
}