Skip to main content

foxtive_ntex/http/server/
config.rs

1use crate::http::kernel::Route;
2use crate::http::shutdown::{ShutdownConfig, ShutdownRegistry};
3use crate::http::Method;
4use crate::FoxtiveNtexState;
5use foxtive::prelude::AppResult;
6use foxtive::setup::trace::Tracing;
7use foxtive::setup::FoxtiveSetup;
8use ntex::time::Seconds;
9use std::any::Any;
10use std::collections::HashMap;
11use std::future::Future;
12use std::pin::Pin;
13use std::sync::Arc;
14
15pub type ShutdownSignalHandler = Pin<Box<dyn Future<Output = ()> + Send + 'static>>;
16
17/// Custom state builder function type
18type CustomStateBuilderFn = Box<dyn FnOnce() -> HashMap<String, Box<dyn Any + Send + Sync>> + Send>;
19
20/// Configuration for serving static files.
21///
22/// This struct defines the mapping between URL paths and filesystem directories
23/// for static file serving. Only available when the `static` feature is enabled.
24///
25/// # Example
26/// ```rust
27/// use foxtive_ntex::http::server::StaticFileConfig;
28///
29/// let config = StaticFileConfig {
30///     path: "/assets".to_string(),
31///     dir: "./public".to_string(),
32/// };
33///
34/// // This would serve files from "./public" directory at "/assets/*" URL path
35/// // e.g., "./public/style.css" would be accessible at "/assets/style.css"
36/// ```
37///
38/// # Security Notes
39/// - The `dir` path should be carefully validated to prevent directory traversal attacks
40/// - Consider using absolute paths or canonicalized paths for the `dir` field
41/// - Ensure proper file permissions are set on the served directory
42#[cfg(feature = "static")]
43pub struct StaticFileConfig {
44    /// The URL path prefix where static files will be served.
45    ///
46    /// This defines the base route under which static files are accessible.
47    /// Should start with "/" (e.g., "/static", "/assets", "/public").
48    pub path: String,
49
50    /// The filesystem directory path containing the static files to serve.
51    ///
52    /// This can be either a relative path (relative to the application's working directory)
53    /// or an absolute path. All files within this directory and its subdirectories
54    /// will be served under the configured URL path.
55    pub dir: String,
56}
57
58/// Configuration for HTTP request body parsing.
59///
60/// This struct controls how different body types (JSON, string, bytes) are processed,
61/// including size limits for each type.
62///
63/// # Default Settings
64/// - JSON limit: 51,000 bytes (50 KB)
65/// - String limit: 51,000 bytes (50 KB)
66/// - Byte limit: 51,000 bytes (50 KB)
67///
68/// # Example
69/// ```rust
70/// use foxtive_ntex::http::server::BodyConfig;
71///
72/// // Use default configuration
73/// let config = BodyConfig::default();
74///
75/// // Custom limits for different body types
76/// let config = BodyConfig::default()
77///     .json_limit(1024 * 1024)      // 1 MB for JSON
78///     .string_limit(512 * 1024)     // 512 KB for strings
79///     .byte_limit(2 * 1024 * 1024); // 2 MB for bytes
80/// ```
81#[derive(Clone, Debug)]
82pub struct BodyConfig {
83    pub(crate) json_limit: usize,
84    pub(crate) string_limit: usize,
85    pub(crate) byte_limit: usize,
86}
87
88impl BodyConfig {
89    pub fn json_limit(mut self, limit: usize) -> Self {
90        self.json_limit = limit;
91        self
92    }
93
94    pub fn string_limit(mut self, limit: usize) -> Self {
95        self.string_limit = limit;
96        self
97    }
98
99    pub fn byte_limit(mut self, limit: usize) -> Self {
100        self.byte_limit = limit;
101        self
102    }
103}
104
105impl Default for BodyConfig {
106    fn default() -> Self {
107        Self {
108            json_limit: 51_000,
109            string_limit: 51_000,
110            byte_limit: 51_000,
111        }
112    }
113}
114
115#[deprecated(since = "0.31.0", note = "Use BodyConfig instead")]
116pub type JsonConfig = BodyConfig;
117
118pub struct ServerBuilder {
119    pub(crate) host: String,
120    pub(crate) port: u16,
121    pub(crate) workers: usize,
122
123    pub(crate) max_connections: usize,
124
125    pub(crate) max_connections_rate: usize,
126
127    pub(crate) client_timeout: Seconds,
128
129    pub(crate) client_disconnect: Seconds,
130
131    pub(crate) keep_alive: Seconds,
132
133    pub(crate) backlog: i32,
134
135    pub(crate) body_config: Option<BodyConfig>,
136
137    pub(crate) app: String,
138    pub(crate) foxtive_setup: FoxtiveSetup,
139
140    pub(crate) tracing: Option<Tracing>,
141
142    #[cfg(feature = "static")]
143    pub(crate) static_config: StaticFileConfig,
144
145    /// whether the app bootstrap has started
146    pub(crate) has_started_bootstrap: bool,
147
148    pub(crate) allowed_origins: Vec<String>,
149
150    pub(crate) allowed_methods: Vec<Method>,
151
152    pub(crate) route_factory: Arc<dyn Fn() -> Vec<Route> + Send + Sync>,
153
154    pub(crate) on_shutdown: Option<ShutdownSignalHandler>,
155
156    pub(crate) shutdown_signal: Option<Pin<Box<dyn Future<Output = ()> + Send>>>,
157
158    pub(crate) shutdown_config: Option<ShutdownConfig>,
159
160    pub(crate) shutdown_registry: ShutdownRegistry,
161
162    pub(crate) custom_state_builder: Option<CustomStateBuilderFn>,
163}
164
165impl ServerBuilder {
166    pub fn create(host: &str, port: u16, setup: FoxtiveSetup) -> ServerBuilder {
167        ServerBuilder {
168            host: host.to_string(),
169            port,
170            workers: 2,
171            max_connections: 25_000,
172            max_connections_rate: 256,
173            client_timeout: Seconds(3),
174            client_disconnect: Seconds(5),
175            keep_alive: Seconds(5),
176            backlog: 2048,
177            app: "foxtive".to_string(),
178            foxtive_setup: setup,
179            #[cfg(feature = "static")]
180            static_config: StaticFileConfig::default(),
181            has_started_bootstrap: false,
182            allowed_origins: vec![],
183            allowed_methods: vec![],
184            route_factory: Arc::new(Vec::new),
185            tracing: None,
186            body_config: None,
187            on_shutdown: None,
188            shutdown_signal: None,
189            shutdown_config: None,
190            shutdown_registry: ShutdownRegistry::new(),
191            custom_state_builder: None,
192        }
193    }
194
195    #[cfg(feature = "static")]
196    pub fn create_with_static(
197        host: &str,
198        port: u16,
199        setup: FoxtiveSetup,
200        config: StaticFileConfig,
201    ) -> ServerBuilder {
202        Self::create(host, port, setup).static_config(config)
203    }
204
205    pub fn app(mut self, app: &str) -> Self {
206        self.app = app.to_string();
207        self
208    }
209
210    pub fn tracing(mut self, config: Tracing) -> Self {
211        self.tracing = Some(config);
212        self
213    }
214
215    /// Set number of workers to start.
216    ///
217    /// By default http server uses 2
218    pub fn workers(mut self, workers: usize) -> Self {
219        self.workers = workers;
220        self
221    }
222
223    /// Set the maximum number of pending connections.
224    ///
225    /// This refers to the number of clients that can be waiting to be served.
226    /// Exceeding this number results in the client getting an error when
227    /// attempting to connect. It should only affect servers under significant
228    /// load.
229    ///
230    /// Generally set in the 64-2048 range. Default value is 2048.
231    ///
232    /// This method should be called before `bind()` method call.
233    pub fn backlog(mut self, backlog: i32) -> Self {
234        self.backlog = backlog;
235        self
236    }
237
238    /// Set server keep-alive setting.
239    ///
240    /// By default keep alive is set to a 5 seconds.
241    pub fn keep_alive(mut self, keep_alive: Seconds) -> Self {
242        self.keep_alive = keep_alive;
243        self
244    }
245
246    /// Set request read timeout in seconds.
247    ///
248    /// Defines a timeout for reading client request headers. If a client does not transmit
249    /// the entire set headers within this time, the request is terminated with
250    /// the 408 (Request Time-out) error.
251    ///
252    /// To disable timeout set value to 0.
253    ///
254    /// By default client timeout is set to 3 seconds.
255    pub fn client_timeout(mut self, timeout: u16) -> Self {
256        self.client_timeout = Seconds(timeout);
257        self
258    }
259
260    /// Set server connection disconnect timeout in seconds.
261    ///
262    /// Defines a timeout for shutdown connection. If a shutdown procedure does not complete
263    /// within this time, the request is dropped.
264    ///
265    /// To disable timeout set value to 0.
266    ///
267    /// By default client timeout is set to 5 seconds.
268    pub fn client_disconnect(mut self, timeout: u16) -> Self {
269        self.client_disconnect = Seconds(timeout);
270        self
271    }
272
273    /// Sets the maximum per-worker number of concurrent connections.
274    ///
275    /// All socket listeners will stop accepting connections when this limit is reached
276    /// for each worker.
277    ///
278    /// By default max connections is set to a 25k.
279    pub fn max_conn(mut self, max: usize) -> Self {
280        self.max_connections = max;
281        self
282    }
283
284    /// Sets the maximum per-worker concurrent connection establish process.
285    ///
286    /// All listeners will stop accepting connections when this limit is reached. It
287    /// can be used to limit the global SSL CPU usage.
288    ///
289    /// By default max connections is set to a 256.
290    pub fn max_conn_rate(mut self, max: usize) -> Self {
291        self.max_connections_rate = max;
292        self
293    }
294
295    pub fn allowed_origins(mut self, allowed_origins: Vec<String>) -> Self {
296        self.allowed_origins = allowed_origins;
297        self
298    }
299
300    pub fn allowed_methods(mut self, allowed_methods: Vec<Method>) -> Self {
301        self.allowed_methods = allowed_methods;
302        self
303    }
304
305    #[cfg(feature = "static")]
306    pub fn static_config(mut self, static_config: StaticFileConfig) -> Self {
307        self.static_config = static_config;
308        self
309    }
310
311    /// Set the route factory function.
312    ///
313    /// This function is called once per worker to create route definitions.
314    pub fn route_factory<F: Fn() -> Vec<Route> + Send + Sync + 'static>(mut self, factory: F) -> Self {
315        self.route_factory = Arc::new(factory);
316        self
317    }
318
319    /// Set the route factory using an existing `Arc`.
320    ///
321    /// Useful for sharing the same factory across multiple server configurations.
322    pub fn route_factory_arc(mut self, factory: Arc<dyn Fn() -> Vec<Route> + Send + Sync>) -> Self {
323        self.route_factory = factory;
324        self
325    }
326
327    #[deprecated(since = "0.32.0", note = "Use route_factory instead")]
328    pub fn boot_thread<F: Fn() -> Vec<Route> + Send + Sync + 'static>(self, factory: F) -> Self {
329        self.route_factory(factory)
330    }
331
332    pub fn has_started_bootstrap(mut self, has_started_bootstrap: bool) -> Self {
333        self.has_started_bootstrap = has_started_bootstrap;
334        self
335    }
336
337    pub fn body_config(mut self, body_config: BodyConfig) -> Self {
338        self.body_config = Some(body_config);
339        self
340    }
341
342    #[deprecated(since = "0.31.0", note = "Use body_config instead")]
343    pub fn json_config(self, config: BodyConfig) -> Self {
344        self.body_config(config)
345    }
346
347    pub fn custom_state_builder(
348        mut self,
349        builder: CustomStateBuilderFn,
350    ) -> Self {
351        self.custom_state_builder = Some(builder);
352        self
353    }
354
355    /// Sets a custom shutdown handler to be called when the application is shutting down.
356    ///
357    /// This method allows you to provide a future that will be awaited during shutdown.
358    /// It is typically used to perform cleanup tasks like closing database connections,
359    /// flushing logs, or other async teardown operations.
360    ///
361    /// **Note:** If a custom `shutdown_signal` is also provided using [`shutdown_signal`],
362    /// that will take precedence over this handler, and this `on_shutdown` handler will
363    /// **not** be executed.
364    ///
365    pub fn on_shutdown<F>(mut self, func: F) -> Self
366    where
367        F: Future<Output = ()> + Send + 'static,
368    {
369        self.on_shutdown = Some(Box::pin(func));
370        self
371    }
372
373    /// Sets a custom shutdown signal handler that determines when the application should begin shutting down.
374    ///
375    /// This method allows you to provide a future that, when resolved, triggers the application shutdown.
376    /// It is typically used to listen for signals like `Ctrl+C` or system termination requests (`SIGTERM`).
377    ///
378    /// If this shutdown signal is provided, it will override any handler set using [`on_shutdown`].
379    pub fn shutdown_signal<F>(mut self, func: F) -> Self
380    where
381        F: Future<Output = ()> + Send + 'static,
382    {
383        self.shutdown_signal = Some(Box::pin(func));
384        self
385    }
386
387    /// Validate the server configuration before startup.
388    ///
389    /// This method checks for common configuration errors and warns about potentially
390    /// problematic settings. It returns an error if critical issues are found.
391    ///
392    /// # Validation Rules
393    /// - Port must not be 0
394    /// - Workers must be at least 1
395    /// - Backlog must not be negative
396    /// - Timeout values are checked for reasonable ranges (warnings only)
397    ///
398    /// # Example
399    /// ```rust,ignore
400    /// use foxtive_ntex::http::server::ServerConfig;
401    /// // FoxtiveSetup must be created with your application's setup logic
402    /// // let config = ServerConfig::validate_example();
403    /// ```
404    pub fn validate(&self) -> foxtive::results::AppResult<()> {
405        use foxtive::internal_server_error;
406
407        // Critical validations
408        if self.port == 0 {
409            return Err(internal_server_error!("Port cannot be 0"));
410        }
411
412        if self.workers == 0 {
413            return Err(internal_server_error!("Workers must be at least 1"));
414        }
415
416        if self.backlog < 0 {
417            return Err(internal_server_error!("Backlog cannot be negative"));
418        }
419
420        if self.max_connections == 0 {
421            return Err(internal_server_error!("Max connections must be at least 1"));
422        }
423
424        if self.max_connections_rate == 0 {
425            return Err(internal_server_error!(
426                "Max connection rate must be at least 1"
427            ));
428        }
429
430        // Warnings for potentially problematic settings
431        if self.client_timeout.0 > 300 {
432            tracing::warn!(
433                "Client timeout is very high: {} seconds. Consider reducing for better resource management.",
434                self.client_timeout.0
435            );
436        }
437
438        if self.keep_alive.0 > 300 {
439            tracing::warn!(
440                "Keep-alive timeout is very high: {} seconds. This may cause resource exhaustion under load.",
441                self.keep_alive.0
442            );
443        }
444
445        if self.workers > num_cpus::get() * 2 {
446            tracing::warn!(
447                "Worker count ({}) is more than 2x the available CPU cores ({}). This may degrade performance.",
448                self.workers,
449                num_cpus::get()
450            );
451        }
452
453        if self.backlog > 10000 {
454            tracing::warn!(
455                "Backlog ({}) is very high. This may cause memory issues under extreme load.",
456                self.backlog
457            );
458        }
459
460        Ok(())
461    }
462
463    /// Create a server configuration with smart defaults for development.
464    ///
465    /// This is a convenience method that creates a configuration optimized for
466    /// local development with relaxed timeouts and single worker.
467    ///
468    /// # Defaults
469    /// - Workers: 1 (easier debugging)
470    /// - Client timeout: 60 seconds
471    /// - Keep-alive: 60 seconds
472    /// - Max connections: 1000
473    /// - Backlog: 256
474    ///
475    /// # Example
476    /// ```rust,ignore
477    /// use foxtive_ntex::http::server::ServerConfig;
478    /// // FoxtiveSetup must be created with your application's setup logic
479    /// // let config = ServerConfig::dev_mode("127.0.0.1", 3000, setup);
480    /// ```
481    pub fn dev_mode(host: &str, port: u16, setup: FoxtiveSetup) -> Self {
482        Self::create(host, port, setup)
483            .workers(1)
484            .client_timeout(60)
485            .keep_alive(Seconds(60))
486            .max_conn(1000)
487            .backlog(256)
488    }
489
490    /// Create a server configuration optimized for production deployment.
491    ///
492    /// This configuration uses conservative settings suitable for most production
493    /// workloads with good performance and resource management.
494    ///
495    /// # Defaults
496    /// - Workers: Auto-detected (number of CPU cores)
497    /// - Client timeout: 15 seconds
498    /// - Keep-alive: 30 seconds
499    /// - Max connections: 25,000
500    /// - Backlog: 2048
501    ///
502    /// # Example
503    /// ```rust,ignore
504    /// use foxtive_ntex::http::server::ServerConfig;
505    /// // FoxtiveSetup must be created with your application's setup logic
506    /// // let config = ServerConfig::production_mode("0.0.0.0", 8080, setup);
507    /// ```
508    pub fn production_mode(host: &str, port: u16, setup: FoxtiveSetup) -> Self {
509        let workers = num_cpus::get();
510        Self::create(host, port, setup)
511            .workers(workers)
512            .client_timeout(15)
513            .keep_alive(ntex::time::Seconds(30))
514            .max_conn(25_000)
515            .backlog(2048)
516    }
517
518    /// Create a server configuration optimized for high-performance scenarios.
519    ///
520    /// This configuration maximizes throughput and concurrent connections,
521    /// suitable for high-traffic APIs or microservices.
522    ///
523    /// # Defaults
524    /// - Workers: 2x CPU cores (for I/O-bound workloads)
525    /// - Client timeout: 5 seconds
526    /// - Keep-alive: 10 seconds
527    /// - Max connections: 50,000
528    /// - Max connection rate: 512
529    /// - Backlog: 4096
530    ///
531    /// # Warning
532    /// This configuration uses more resources. Monitor your system to ensure
533    /// it can handle the increased load.
534    ///
535    /// # Example
536    /// ```rust,ignore
537    /// use foxtive_ntex::http::server::ServerConfig;
538    /// // FoxtiveSetup must be created with your application's setup logic
539    /// // let config = ServerConfig::high_performance_mode("0.0.0.0", 8080, setup);
540    /// ```
541    pub fn high_performance_mode(host: &str, port: u16, setup: FoxtiveSetup) -> Self {
542        let workers = num_cpus::get() * 2;
543        Self::create(host, port, setup)
544            .workers(workers)
545            .client_timeout(5)
546            .keep_alive(Seconds(10))
547            .max_conn(50_000)
548            .max_conn_rate(512)
549            .backlog(4096)
550    }
551
552    /// Configure shutdown behavior with timeout and cleanup coordination.
553    ///
554    /// This method sets up coordinated shutdown for all registered services.
555    /// Services are shut down in priority order with per-service timeouts.
556    ///
557    /// # Arguments
558    /// * `config` - Shutdown configuration with timeout settings
559    ///
560    /// # Example
561    /// ```rust,ignore
562    /// use foxtive_ntex::http::server::{ServerConfig, ShutdownConfig};
563    /// // FoxtiveSetup must be created with your application's setup logic
564    /// // let config = ServerConfig::create("127.0.0.1", 8080, setup)
565    /// //     .shutdown_config(ShutdownConfig::new(30));
566    /// ```
567    pub fn shutdown_config(mut self, config: ShutdownConfig) -> Self {
568        self.shutdown_config = Some(config);
569        self
570    }
571
572    /// Register a service for graceful shutdown cleanup.
573    ///
574    /// Services are shut down in priority order (lower priority number first).
575    /// Each service has a timeout to prevent one slow service from blocking others.
576    ///
577    /// # Arguments
578    /// * `name` - Name of the service (for logging)
579    /// * `priority` - Shutdown priority (lower = shutdown first)
580    ///   - 0-10: Critical infrastructure (databases, message queues)
581    ///   - 11-50: Application services (caches, connection pools)
582    ///   - 51-100: Auxiliary services (loggers, metrics)
583    /// * `cleanup` - Async cleanup function to execute
584    ///
585    /// # Example
586    /// ```rust,ignore
587    /// use foxtive_ntex::http::server::ServerBuilder;
588    /// // FoxtiveSetup must be created with your application's setup logic
589    /// // let config = ServerBuilder::create("127.0.0.1", 8080, setup)
590    /// //     .register_shutdown_service("database", 1, || async {
591    /// //         println!("Database closed");
592    /// //     });
593    /// ```
594    pub fn register_shutdown_service<F, Fut>(mut self, name: &str, priority: u8, cleanup: F) -> Self
595    where
596        F: FnOnce() -> Fut + Send + 'static,
597        Fut: Future<Output = ()> + Send + 'static,
598    {
599        self.shutdown_registry.register(name, priority, cleanup);
600        self
601    }
602
603    /// Start the HTTP server with an optional bootstrap callback.
604    ///
605    /// This method validates the configuration, sets up the ntex server,
606    /// and starts listening for incoming requests.
607    ///
608    /// # Arguments
609    /// * `callback` - Optional async function that runs after state creation but before server starts.
610    ///   Useful for database migrations, cache warming, etc.
611    ///
612    /// # Example
613    /// ```rust,ignore
614    /// use foxtive_ntex::http::server::ServerBuilder;
615    /// use foxtive::setup::FoxtiveSetup;
616    ///
617    /// let foxtive = FoxtiveSetup::default();
618    ///
619    /// ServerBuilder::dev_mode("127.0.0.1", 3000, foxtive)
620    ///     .on_shutdown(async {
621    ///         println!("Server shutting down gracefully");
622    ///     })
623    ///     .start(|state| async move {
624    ///         // Bootstrap code here (e.g., database migrations)
625    ///         println!("Server starting...");
626    ///         Ok(())
627    ///     })
628    ///     .await?;
629    /// ```
630    pub async fn start<Callback, Fut>(self, callback: Callback) -> AppResult<()>
631    where
632        Callback: FnOnce(FoxtiveNtexState) -> Fut + Copy + Send + 'static,
633        Fut: Future<Output = AppResult<()>> + Send + 'static,
634    {
635        super::start_ntex_server(self, callback).await
636    }
637
638    /// Start the HTTP server without a bootstrap callback.
639    ///
640    /// This is a convenience method for simple servers that don't need
641    /// initialization logic before starting.
642    ///
643    /// # Example
644    /// ```rust,ignore
645    /// use foxtive_ntex::http::server::ServerBuilder;
646    /// use foxtive::setup::FoxtiveSetup;
647    ///
648    /// let foxtive = FoxtiveSetup::default();
649    ///
650    /// ServerBuilder::production_mode("0.0.0.0", 8080, foxtive)
651    ///     .run()
652    ///     .await?;
653    /// ```
654    pub async fn run(self) -> AppResult<()> {
655        self.start(|_state| async { Ok(()) }).await
656    }
657}
658
659#[cfg(feature = "static")]
660impl Default for StaticFileConfig {
661    fn default() -> Self {
662        Self {
663            path: "static".to_string(),
664            dir: "./static".to_string(),
665        }
666    }
667}
668
669#[cfg(test)]
670mod tests {
671    use super::*;
672
673    #[test]
674    fn test_body_config_limits() {
675        let config = BodyConfig::default()
676            .json_limit(1024 * 1024)
677            .string_limit(512 * 1024)
678            .byte_limit(2 * 1024 * 1024);
679        
680        assert_eq!(config.json_limit, 1_048_576);
681        assert_eq!(config.string_limit, 524_288);
682        assert_eq!(config.byte_limit, 2_097_152);
683    }
684}