1use std::net::SocketAddr;
16use std::time::Duration;
17
18#[cfg(feature = "tls")]
19use std::sync::Arc;
20
21#[derive(Debug, Clone)]
40#[must_use]
41pub struct RuntimeConfig {
42 pub bind: SocketAddr,
44 pub max_connections: usize,
46 pub max_file_streams: usize,
48 pub header_read_timeout: Duration,
50 pub connection_total_timeout: Duration,
52 pub handler_timeout: Duration,
54 pub body_read_timeout: Duration,
57 pub graceful_shutdown_timeout: Duration,
59 pub server_header: Option<String>,
62 #[cfg(feature = "tls")]
65 pub tls_config: Option<Arc<rustls::ServerConfig>>,
66 pub max_request_body_bytes: u64,
69}
70
71impl Default for RuntimeConfig {
72 fn default() -> Self {
73 Self {
74 bind: "127.0.0.1:8000".parse().unwrap(),
75 max_connections: 64,
76 max_file_streams: 32,
77 header_read_timeout: Duration::from_secs(10),
78 connection_total_timeout: Duration::from_secs(60),
79 handler_timeout: Duration::from_secs(30),
80 body_read_timeout: Duration::from_secs(30),
81 graceful_shutdown_timeout: Duration::from_secs(10),
82 server_header: None,
83 #[cfg(feature = "tls")]
84 tls_config: None,
85 max_request_body_bytes: 0,
86 }
87 }
88}
89
90impl RuntimeConfig {
91 pub fn builder() -> RuntimeConfigBuilder {
93 RuntimeConfigBuilder {
94 bind: None,
95 max_connections: None,
96 max_file_streams: None,
97 header_read_timeout: None,
98 connection_total_timeout: None,
99 handler_timeout: None,
100 body_read_timeout: None,
101 graceful_shutdown_timeout: None,
102 server_header: None,
103 #[cfg(feature = "tls")]
104 tls_config: None,
105 max_request_body_bytes: None,
106 }
107 }
108}
109
110#[derive(Debug, Default)]
112#[must_use]
113pub struct RuntimeConfigBuilder {
114 bind: Option<SocketAddr>,
115 max_connections: Option<usize>,
116 max_file_streams: Option<usize>,
117 header_read_timeout: Option<Duration>,
118 connection_total_timeout: Option<Duration>,
119 handler_timeout: Option<Duration>,
120 body_read_timeout: Option<Duration>,
121 graceful_shutdown_timeout: Option<Duration>,
122 server_header: Option<String>,
123 #[cfg(feature = "tls")]
124 tls_config: Option<Arc<rustls::ServerConfig>>,
125 max_request_body_bytes: Option<u64>,
126}
127
128impl RuntimeConfigBuilder {
129 pub fn bind(mut self, addr: SocketAddr) -> Self {
131 self.bind = Some(addr);
132 self
133 }
134
135 pub fn max_connections(mut self, max: usize) -> Self {
139 self.max_connections = Some(max);
140 self
141 }
142
143 pub fn max_file_streams(mut self, max: usize) -> Self {
147 self.max_file_streams = Some(max);
148 self
149 }
150
151 pub fn header_read_timeout(mut self, timeout: Duration) -> Self {
153 self.header_read_timeout = Some(timeout);
154 self
155 }
156
157 pub fn connection_total_timeout(mut self, timeout: Duration) -> Self {
159 self.connection_total_timeout = Some(timeout);
160 self
161 }
162
163 pub fn handler_timeout(mut self, timeout: Duration) -> Self {
165 self.handler_timeout = Some(timeout);
166 self
167 }
168
169 pub fn body_read_timeout(mut self, timeout: Duration) -> Self {
173 self.body_read_timeout = Some(timeout);
174 self
175 }
176
177 pub fn graceful_shutdown_timeout(mut self, timeout: Duration) -> Self {
179 self.graceful_shutdown_timeout = Some(timeout);
180 self
181 }
182
183 pub fn server_header(mut self, header: String) -> Self {
187 self.server_header = Some(header);
188 self
189 }
190
191 #[cfg(feature = "tls")]
193 pub fn tls_config(mut self, config: Arc<rustls::ServerConfig>) -> Self {
194 self.tls_config = Some(config);
195 self
196 }
197
198 pub fn max_request_body_bytes(mut self, max: u64) -> Self {
203 self.max_request_body_bytes = Some(max);
204 self
205 }
206
207 pub fn build(self) -> Result<RuntimeConfig, crate::server::errors::ServerError> {
212 let max_connections = self.max_connections.unwrap_or(64);
213 let max_file_streams = self.max_file_streams.unwrap_or(32);
214 let max_semaphore_permits = tokio::sync::Semaphore::MAX_PERMITS;
215 if max_connections == 0 {
216 return Err(crate::server::errors::ServerError::Config(
217 "max_connections must be > 0".into(),
218 ));
219 }
220 if max_connections > max_semaphore_permits {
221 return Err(crate::server::errors::ServerError::Config(format!(
222 "max_connections must be <= {} (Semaphore::MAX_PERMITS): got {}",
223 max_semaphore_permits, max_connections
224 )));
225 }
226 if max_file_streams == 0 {
227 return Err(crate::server::errors::ServerError::Config(
228 "max_file_streams must be > 0".into(),
229 ));
230 }
231 if max_file_streams > max_semaphore_permits {
232 return Err(crate::server::errors::ServerError::Config(format!(
233 "max_file_streams must be <= {} (Semaphore::MAX_PERMITS): got {}",
234 max_semaphore_permits, max_file_streams
235 )));
236 }
237
238 let header_read_timeout = self.header_read_timeout.unwrap_or(Duration::from_secs(10));
239 let connection_total_timeout = self
240 .connection_total_timeout
241 .unwrap_or(Duration::from_secs(60));
242 let handler_timeout = self.handler_timeout.unwrap_or(Duration::from_secs(30));
243 let body_read_timeout = self.body_read_timeout.unwrap_or(Duration::from_secs(30));
244 let graceful_shutdown_timeout = self
245 .graceful_shutdown_timeout
246 .unwrap_or(Duration::from_secs(10));
247
248 if header_read_timeout.is_zero() {
249 return Err(crate::server::errors::ServerError::Config(
250 "header_read_timeout must be > 0".into(),
251 ));
252 }
253 if connection_total_timeout.is_zero() {
254 return Err(crate::server::errors::ServerError::Config(
255 "connection_total_timeout must be > 0".into(),
256 ));
257 }
258 if header_read_timeout > connection_total_timeout {
259 return Err(crate::server::errors::ServerError::Config(
260 "header_read_timeout must be <= connection_total_timeout".into(),
261 ));
262 }
263 if handler_timeout.is_zero() {
264 return Err(crate::server::errors::ServerError::Config(
265 "handler_timeout must be > 0".into(),
266 ));
267 }
268 if body_read_timeout.is_zero() {
269 return Err(crate::server::errors::ServerError::Config(
270 "body_read_timeout must be > 0".into(),
271 ));
272 }
273 if graceful_shutdown_timeout.is_zero() {
274 return Err(crate::server::errors::ServerError::Config(
275 "graceful_shutdown_timeout must be > 0".into(),
276 ));
277 }
278 if let Some(server_header) = &self.server_header {
279 crate::primitives::header_block::HeaderValue::new(server_header.clone()).map_err(
280 |e| {
281 crate::server::errors::ServerError::Config(format!(
282 "invalid server_header: {e}"
283 ))
284 },
285 )?;
286 }
287 Ok(RuntimeConfig {
288 bind: self
289 .bind
290 .unwrap_or_else(|| "127.0.0.1:8000".parse().unwrap()),
291 max_connections,
292 max_file_streams,
293 header_read_timeout,
294 connection_total_timeout,
295 handler_timeout,
296 body_read_timeout,
297 graceful_shutdown_timeout,
298 server_header: self.server_header,
299 #[cfg(feature = "tls")]
300 tls_config: self.tls_config,
301 max_request_body_bytes: self.max_request_body_bytes.unwrap_or(0),
302 })
303 }
304}
305
306pub fn try_from_serve_config(
315 config: &crate::config::ServeConfig,
316) -> Result<RuntimeConfig, crate::server::errors::ServerError> {
317 config.limits.validate().map_err(|errs| {
318 crate::server::errors::ServerError::Config(
319 errs.iter()
320 .map(|e| e.to_string())
321 .collect::<Vec<_>>()
322 .join("; "),
323 )
324 })?;
325 Ok(RuntimeConfig {
326 bind: config.bind,
327 max_connections: config.limits.max_connections,
328 max_file_streams: config.limits.max_file_streams,
329 header_read_timeout: config.limits.header_read_timeout,
330 connection_total_timeout: config.limits.connection_total_timeout,
331 handler_timeout: config.limits.handler_timeout,
332 body_read_timeout: config.limits.body_read_timeout,
333 graceful_shutdown_timeout: config.limits.graceful_shutdown_timeout,
334 server_header: None,
335 #[cfg(feature = "tls")]
336 tls_config: None,
337 max_request_body_bytes: config.limits.max_request_body_bytes,
338 })
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 #[test]
346 fn default_runtime_config() {
347 let config = RuntimeConfig::default();
348 assert!(config.bind.ip().is_loopback());
349 assert_eq!(config.bind.port(), 8000);
350 assert_eq!(config.max_connections, 64);
351 assert_eq!(config.max_file_streams, 32);
352 assert_eq!(config.header_read_timeout, Duration::from_secs(10));
353 assert_eq!(config.connection_total_timeout, Duration::from_secs(60));
354 assert_eq!(config.handler_timeout, Duration::from_secs(30));
355 assert_eq!(config.body_read_timeout, Duration::from_secs(30));
356 assert_eq!(config.graceful_shutdown_timeout, Duration::from_secs(10));
357 assert_eq!(config.server_header, None);
358 assert_eq!(config.max_request_body_bytes, 0);
359 }
360
361 #[test]
362 fn builder_overrides() {
363 let config = RuntimeConfig::builder()
364 .bind("0.0.0.0:9000".parse().unwrap())
365 .max_connections(128)
366 .max_file_streams(64)
367 .header_read_timeout(Duration::from_secs(5))
368 .connection_total_timeout(Duration::from_secs(30))
369 .handler_timeout(Duration::from_secs(15))
370 .body_read_timeout(Duration::from_secs(20))
371 .graceful_shutdown_timeout(Duration::from_secs(5))
372 .server_header("eggserve/0.1".into())
373 .max_request_body_bytes(1024 * 1024)
374 .build()
375 .unwrap();
376 assert_eq!(config.bind.port(), 9000);
377 assert_eq!(config.max_connections, 128);
378 assert_eq!(config.max_file_streams, 64);
379 assert_eq!(config.header_read_timeout, Duration::from_secs(5));
380 assert_eq!(config.connection_total_timeout, Duration::from_secs(30));
381 assert_eq!(config.handler_timeout, Duration::from_secs(15));
382 assert_eq!(config.body_read_timeout, Duration::from_secs(20));
383 assert_eq!(config.graceful_shutdown_timeout, Duration::from_secs(5));
384 assert_eq!(config.server_header.as_deref(), Some("eggserve/0.1"));
385 assert_eq!(config.max_request_body_bytes, 1024 * 1024);
386 }
387
388 #[test]
389 fn invalid_server_header_is_rejected() {
390 let err = RuntimeConfig::builder()
391 .server_header("bad\r\nvalue".into())
392 .build()
393 .unwrap_err();
394 assert!(err.to_string().contains("invalid server_header"));
395 }
396
397 #[test]
398 fn from_serve_config() {
399 let serve_config = crate::config::ServeConfig::default();
400 let runtime = try_from_serve_config(&serve_config).unwrap();
401 assert_eq!(runtime.bind, serve_config.bind);
402 assert_eq!(runtime.max_connections, serve_config.limits.max_connections);
403 assert_eq!(
404 runtime.max_file_streams,
405 serve_config.limits.max_file_streams
406 );
407 assert_eq!(
408 runtime.max_request_body_bytes,
409 serve_config.limits.max_request_body_bytes
410 );
411 }
412
413 #[test]
414 fn zero_connections_returns_error() {
415 let result = RuntimeConfig::builder().max_connections(0).build();
416 assert!(result.is_err());
417 let err = result.unwrap_err();
418 assert!(err.to_string().contains("max_connections must be > 0"));
419 }
420
421 #[test]
422 fn zero_file_streams_returns_error() {
423 let result = RuntimeConfig::builder().max_file_streams(0).build();
424 assert!(result.is_err());
425 let err = result.unwrap_err();
426 assert!(err.to_string().contains("max_file_streams must be > 0"));
427 }
428
429 #[test]
430 fn zero_header_read_timeout_returns_error() {
431 let result = RuntimeConfig::builder()
432 .header_read_timeout(Duration::ZERO)
433 .build();
434 assert!(result.is_err());
435 let err = result.unwrap_err();
436 assert!(err.to_string().contains("header_read_timeout must be > 0"));
437 }
438
439 #[test]
440 fn zero_connection_total_timeout_returns_error() {
441 let result = RuntimeConfig::builder()
442 .connection_total_timeout(Duration::ZERO)
443 .build();
444 assert!(result.is_err());
445 let err = result.unwrap_err();
446 assert!(err
447 .to_string()
448 .contains("connection_total_timeout must be > 0"));
449 }
450
451 #[test]
452 fn header_timeout_cannot_exceed_connection_total_timeout() {
453 let result = RuntimeConfig::builder()
454 .header_read_timeout(Duration::from_secs(2))
455 .connection_total_timeout(Duration::from_secs(1))
456 .build();
457 assert!(result
458 .unwrap_err()
459 .to_string()
460 .contains("header_read_timeout must be <= connection_total_timeout"));
461 }
462
463 #[test]
464 fn zero_handler_timeout_returns_error() {
465 let result = RuntimeConfig::builder()
466 .handler_timeout(Duration::ZERO)
467 .build();
468 assert!(result.is_err());
469 let err = result.unwrap_err();
470 assert!(err.to_string().contains("handler_timeout must be > 0"));
471 }
472
473 #[test]
474 fn zero_body_read_timeout_returns_error() {
475 let result = RuntimeConfig::builder()
476 .body_read_timeout(Duration::ZERO)
477 .build();
478 assert!(result.is_err());
479 let err = result.unwrap_err();
480 assert!(err.to_string().contains("body_read_timeout must be > 0"));
481 }
482
483 #[test]
484 fn zero_graceful_shutdown_timeout_returns_error() {
485 let result = RuntimeConfig::builder()
486 .graceful_shutdown_timeout(Duration::ZERO)
487 .build();
488 assert!(result.is_err());
489 let err = result.unwrap_err();
490 assert!(err
491 .to_string()
492 .contains("graceful_shutdown_timeout must be > 0"));
493 }
494
495 #[test]
496 fn limits_defaults_match_runtime_config_defaults() {
497 let limits = crate::limits::Limits::default();
498 let runtime = RuntimeConfig::default();
499 assert_eq!(limits.max_connections, runtime.max_connections);
500 assert_eq!(limits.max_file_streams, runtime.max_file_streams);
501 assert_eq!(limits.header_read_timeout, runtime.header_read_timeout);
502 assert_eq!(
503 limits.connection_total_timeout,
504 runtime.connection_total_timeout
505 );
506 assert_eq!(limits.handler_timeout, runtime.handler_timeout);
507 assert_eq!(limits.body_read_timeout, runtime.body_read_timeout);
508 assert_eq!(
509 limits.graceful_shutdown_timeout,
510 runtime.graceful_shutdown_timeout
511 );
512 }
513
514 #[test]
515 fn serve_config_to_runtime_preserves_limits() {
516 let limits = crate::limits::Limits {
517 max_connections: 99,
518 max_file_streams: 77,
519 handler_timeout: Duration::from_secs(42),
520 body_read_timeout: Duration::from_secs(99),
521 ..Default::default()
522 };
523 let serve = crate::config::ServeConfig {
524 limits,
525 ..Default::default()
526 };
527 let runtime = try_from_serve_config(&serve).unwrap();
528 assert_eq!(runtime.max_connections, 99);
529 assert_eq!(runtime.max_file_streams, 77);
530 assert_eq!(runtime.handler_timeout, Duration::from_secs(42));
531 assert_eq!(runtime.body_read_timeout, Duration::from_secs(99));
532 }
533
534 #[test]
535 fn try_from_serve_config_rejects_invalid_limits() {
536 let limits = crate::limits::Limits {
537 max_connections: 0,
538 ..Default::default()
539 };
540 let serve = crate::config::ServeConfig {
541 limits,
542 ..Default::default()
543 };
544 let err = try_from_serve_config(&serve).unwrap_err();
545 assert!(err.to_string().contains("max_connections"));
546 }
547
548 #[test]
549 fn limits_validate_rejects_all_zero_fields() {
550 let limits = crate::limits::Limits {
551 max_connections: 0,
552 max_file_streams: 0,
553 header_read_timeout: Duration::ZERO,
554 connection_total_timeout: Duration::ZERO,
555 handler_timeout: Duration::ZERO,
556 body_read_timeout: Duration::ZERO,
557 graceful_shutdown_timeout: Duration::ZERO,
558 ..Default::default()
559 };
560 let errs = limits.validate().unwrap_err();
561 assert_eq!(errs.len(), 7);
562 }
563
564 #[test]
565 fn builder_no_overrides_uses_defaults() {
566 let config = RuntimeConfig::builder().build().unwrap();
567 let default = RuntimeConfig::default();
568 assert_eq!(config.max_connections, default.max_connections);
569 assert_eq!(config.max_file_streams, default.max_file_streams);
570 assert_eq!(config.header_read_timeout, default.header_read_timeout);
571 assert_eq!(
572 config.connection_total_timeout,
573 default.connection_total_timeout
574 );
575 assert_eq!(config.handler_timeout, default.handler_timeout);
576 assert_eq!(config.body_read_timeout, default.body_read_timeout);
577 assert_eq!(
578 config.graceful_shutdown_timeout,
579 default.graceful_shutdown_timeout
580 );
581 }
582
583 #[test]
584 fn builder_is_consumed_by_build() {
585 let builder = RuntimeConfig::builder().max_connections(128);
586 let _config = builder.build().unwrap();
587 }
589
590 #[test]
591 fn try_from_does_not_panic_on_invalid_input() {
592 let limits = crate::limits::Limits {
593 max_connections: 0,
594 max_file_streams: 0,
595 header_read_timeout: Duration::ZERO,
596 connection_total_timeout: Duration::ZERO,
597 handler_timeout: Duration::ZERO,
598 body_read_timeout: Duration::ZERO,
599 graceful_shutdown_timeout: Duration::ZERO,
600 ..Default::default()
601 };
602 let serve = crate::config::ServeConfig {
603 limits,
604 ..Default::default()
605 };
606 let result = try_from_serve_config(&serve);
607 assert!(result.is_err());
608 let err = result.unwrap_err();
609 let msg = err.to_string();
611 assert!(msg.contains("max_connections"));
612 assert!(msg.contains("max_file_streams"));
613 assert!(msg.contains("header_read_timeout"));
614 }
615
616 #[test]
617 fn large_concurrency_valuesaccepted() {
618 let max = tokio::sync::Semaphore::MAX_PERMITS;
619 let config = RuntimeConfig::builder()
620 .max_connections(max)
621 .max_file_streams(max)
622 .build()
623 .unwrap();
624 assert_eq!(config.max_connections, max);
625 assert_eq!(config.max_file_streams, max);
626 }
627
628 #[test]
629 fn exceeding_semaphore_max_permits_rejected() {
630 let result = RuntimeConfig::builder()
631 .max_connections(tokio::sync::Semaphore::MAX_PERMITS + 1)
632 .build();
633 assert!(result.is_err());
634 let err = result.unwrap_err();
635 assert!(err.to_string().contains("Semaphore::MAX_PERMITS"));
636 }
637
638 #[test]
639 fn large_timeout_values_accepted() {
640 let config = RuntimeConfig::builder()
641 .header_read_timeout(Duration::from_secs(u64::MAX))
642 .connection_total_timeout(Duration::from_secs(u64::MAX))
643 .handler_timeout(Duration::from_secs(u64::MAX))
644 .body_read_timeout(Duration::from_secs(u64::MAX))
645 .graceful_shutdown_timeout(Duration::from_secs(u64::MAX))
646 .build()
647 .unwrap();
648 assert_eq!(config.header_read_timeout, Duration::from_secs(u64::MAX));
649 }
650
651 #[test]
652 fn try_from_serve_config_multiple_invalid_fields() {
653 let limits = crate::limits::Limits {
654 max_connections: 0,
655 handler_timeout: Duration::ZERO,
656 ..Default::default()
657 };
658 let serve = crate::config::ServeConfig {
659 limits,
660 ..Default::default()
661 };
662 let err = try_from_serve_config(&serve).unwrap_err();
663 let msg = err.to_string();
664 assert!(msg.contains("max_connections"));
665 assert!(msg.contains("handler_timeout"));
666 }
667
668 #[test]
669 fn try_from_serve_config_preserves_bind_address() {
670 let serve = crate::config::ServeConfig {
671 bind: "0.0.0.0:9000".parse().unwrap(),
672 ..Default::default()
673 };
674 let runtime = try_from_serve_config(&serve).unwrap();
675 assert_eq!(runtime.bind.port(), 9000);
676 assert!(runtime.bind.ip().is_unspecified());
677 }
678
679 #[test]
680 fn try_from_serve_config_sets_safe_defaults() {
681 let serve = crate::config::ServeConfig::default();
682 let runtime = try_from_serve_config(&serve).unwrap();
683 assert_eq!(runtime.max_request_body_bytes, 0);
684 }
685}