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}