Skip to main content

eggserve_core/server/
config.rs

1//! Runtime configuration for the HTTP server.
2//!
3//! [`RuntimeConfig`] controls transport-level concerns (connection limits,
4//! timeouts, keep-alive) independently of service-level concerns (filesystem
5//! policy, root directory). The CLI and Python frontends translate their
6//! respective configurations into a shared [`RuntimeConfig`] plus service
7//! configuration.
8//!
9//! # Separation from service configuration
10//!
11//! Filesystem policy ([`StaticPolicy`]) and root directory belong to the
12//! static service, not the runtime. This separation ensures the runtime
13//! remains transport-agnostic and reusable for custom services.
14
15use std::net::SocketAddr;
16use std::time::Duration;
17
18#[cfg(feature = "tls")]
19use std::sync::Arc;
20
21/// Transport-level runtime configuration.
22///
23/// All fields have safe defaults that match or strengthen the CLI defaults.
24/// Configuration validation occurs at construction time via the builder.
25///
26/// # Examples
27///
28/// ```no_run
29/// use eggserve_core::server::RuntimeConfig;
30/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
31///
32/// let config = RuntimeConfig::builder()
33///     .bind("127.0.0.1:8000".parse().unwrap())
34///     .max_connections(128)
35///     .build()?;
36/// # Ok(())
37/// # }
38/// ```
39#[derive(Debug, Clone)]
40#[must_use]
41pub struct RuntimeConfig {
42    /// Address to bind the listener to.
43    pub bind: SocketAddr,
44    /// Maximum concurrent connections. Default: 64.
45    pub max_connections: usize,
46    /// Maximum concurrent file-stream responses. Default: 32.
47    pub max_file_streams: usize,
48    /// Timeout for reading request headers. Default: 10s.
49    pub header_read_timeout: Duration,
50    /// Timeout wrapping the entire Hyper connection future. Default: 60s.
51    pub connection_total_timeout: Duration,
52    /// Timeout for a single handler invocation. Default: 30s.
53    pub handler_timeout: Duration,
54    /// Timeout for reading the request body. Default: 30s.
55    /// This is a total deadline for body consumption, not an idle timeout.
56    pub body_read_timeout: Duration,
57    /// Graceful shutdown grace period. Default: 10s.
58    pub graceful_shutdown_timeout: Duration,
59    /// Server identification header value. If `Some`, added as `Server`
60    /// header on responses. Default: `None`.
61    pub server_header: Option<String>,
62    /// TLS server configuration. If `Some`, connections are upgraded to TLS.
63    /// Only available with the `tls` feature. Default: `None`.
64    #[cfg(feature = "tls")]
65    pub tls_config: Option<Arc<rustls::ServerConfig>>,
66    /// Maximum allowed request body size in bytes. This is the hard ceiling
67    /// that no service can exceed. Default: 0 (bodies rejected).
68    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    /// Create a new builder with default values.
92    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/// Builder for [`RuntimeConfig`].
111#[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    /// Set the bind address.
130    pub fn bind(mut self, addr: SocketAddr) -> Self {
131        self.bind = Some(addr);
132        self
133    }
134
135    /// Set the maximum number of concurrent connections.
136    ///
137    /// Must be > 0. Default: 64.
138    pub fn max_connections(mut self, max: usize) -> Self {
139        self.max_connections = Some(max);
140        self
141    }
142
143    /// Set the maximum number of concurrent file-stream responses.
144    ///
145    /// Must be > 0. Default: 32.
146    pub fn max_file_streams(mut self, max: usize) -> Self {
147        self.max_file_streams = Some(max);
148        self
149    }
150
151    /// Set the header-read timeout.
152    pub fn header_read_timeout(mut self, timeout: Duration) -> Self {
153        self.header_read_timeout = Some(timeout);
154        self
155    }
156
157    /// Set the connection total timeout.
158    pub fn connection_total_timeout(mut self, timeout: Duration) -> Self {
159        self.connection_total_timeout = Some(timeout);
160        self
161    }
162
163    /// Set the handler invocation timeout.
164    pub fn handler_timeout(mut self, timeout: Duration) -> Self {
165        self.handler_timeout = Some(timeout);
166        self
167    }
168
169    /// Set the body read timeout.
170    ///
171    /// This is a total deadline for body consumption, not an idle timeout.
172    pub fn body_read_timeout(mut self, timeout: Duration) -> Self {
173        self.body_read_timeout = Some(timeout);
174        self
175    }
176
177    /// Set the graceful shutdown grace period.
178    pub fn graceful_shutdown_timeout(mut self, timeout: Duration) -> Self {
179        self.graceful_shutdown_timeout = Some(timeout);
180        self
181    }
182
183    /// Set the server identification header value.
184    ///
185    /// If set, added as `Server` header on all responses.
186    pub fn server_header(mut self, header: String) -> Self {
187        self.server_header = Some(header);
188        self
189    }
190
191    /// Set the TLS server configuration.
192    #[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    /// Set the maximum request body size in bytes.
199    ///
200    /// This is the hard ceiling that no service can exceed. Default: 0
201    /// (bodies rejected). Set to a positive value to allow request bodies.
202    pub fn max_request_body_bytes(mut self, max: u64) -> Self {
203        self.max_request_body_bytes = Some(max);
204        self
205    }
206
207    /// Build the runtime configuration.
208    ///
209    /// Returns an error if `max_connections`, `max_file_streams`, or any
210    /// timeout duration is 0.
211    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
306/// Try to convert a [`crate::config::ServeConfig`] into a [`RuntimeConfig`].
307///
308/// This bridges the CLI/Python configuration model into the runtime model.
309/// Filesystem policy and root directory are NOT transferred — they belong
310/// to the service, not the runtime.
311///
312/// Returns an error if the `Limits` contain invalid values (zero concurrency,
313/// zero timeouts).
314pub 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        // builder is moved, cannot use again
588    }
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        // Error message contains all invalid field names
610        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}