Skip to main content

hyperi_rustlib/http_server/
config.rs

1// Project:   hyperi-rustlib
2// File:      src/http_server/config.rs
3// Purpose:   HTTP server configuration
4// Language:  Rust
5//
6// License:   BUSL-1.1
7// Copyright: (c) 2026 HYPERI PTY LIMITED
8
9//! HTTP server configuration.
10
11use serde::{Deserialize, Serialize};
12use std::time::Duration;
13
14/// HTTP server configuration.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[allow(clippy::struct_excessive_bools)]
17pub struct HttpServerConfig {
18    /// Address to bind to (e.g., "0.0.0.0:8080").
19    pub bind_address: String,
20
21    /// Request timeout in milliseconds.
22    /// Defaults to 30 seconds.
23    #[serde(default = "default_request_timeout_ms")]
24    pub request_timeout_ms: u64,
25
26    /// Keep-alive timeout in ms. Default 75 s.
27    ///
28    /// **Not wired.** axum 0.8 `serve()` doesn't surface keep-alive;
29    /// needs hyper builder. Follow-up.
30    #[serde(default = "default_keep_alive_timeout_ms")]
31    pub keep_alive_timeout_ms: u64,
32
33    /// In-flight request cap. Default 10,000. Enforced via
34    /// `tower::limit::ConcurrencyLimitLayer`. Excess requests queue.
35    #[serde(default = "default_max_connections")]
36    pub max_connections: usize,
37
38    /// Mount /health/live + /health/ready. Default true.
39    #[serde(default = "default_true")]
40    pub enable_health_endpoints: bool,
41
42    /// Mount /metrics.
43    ///
44    /// **Not wired here.** MetricsManager owns its own /metrics
45    /// listener (often a separate admin port). Forward-compat for
46    /// framework-managed wiring.
47    #[serde(default)]
48    pub enable_metrics_endpoint: bool,
49
50    /// Mount /config (redacted effective config). Default false.
51    #[serde(default)]
52    pub enable_config_endpoint: bool,
53
54    /// HTTP/2 (also required for gRPC).
55    ///
56    /// **Not wired.** axum 0.8 `serve()` always negotiates HTTP/1
57    /// and HTTP/2 on cleartext. Disabling needs hyper builder.
58    #[serde(default = "default_true")]
59    pub enable_http2: bool,
60
61    /// TLS cert path (PEM).
62    ///
63    /// **Not wired.** Needs `axum_server::tls_rustls` (not a dep).
64    /// K8s pattern: terminate TLS at Ingress / Service Mesh,
65    /// cleartext in-pod.
66    #[serde(default)]
67    pub tls_cert_path: Option<String>,
68
69    /// TLS key path (PEM). See `tls_cert_path` -- not wired.
70    #[serde(default)]
71    pub tls_key_path: Option<String>,
72
73    /// Graceful drain budget in ms. Default 30 s. Caps drain time
74    /// to fit under K8s `terminationGracePeriodSeconds`.
75    #[serde(default = "default_shutdown_timeout_ms")]
76    pub shutdown_timeout_ms: u64,
77}
78
79fn default_request_timeout_ms() -> u64 {
80    30_000
81}
82
83fn default_keep_alive_timeout_ms() -> u64 {
84    75_000
85}
86
87fn default_max_connections() -> usize {
88    10_000
89}
90
91fn default_shutdown_timeout_ms() -> u64 {
92    30_000
93}
94
95fn default_true() -> bool {
96    true
97}
98
99impl Default for HttpServerConfig {
100    fn default() -> Self {
101        Self {
102            bind_address: "0.0.0.0:8080".to_string(),
103            request_timeout_ms: default_request_timeout_ms(),
104            keep_alive_timeout_ms: default_keep_alive_timeout_ms(),
105            max_connections: default_max_connections(),
106            enable_health_endpoints: true,
107            enable_metrics_endpoint: false,
108            enable_config_endpoint: false,
109            enable_http2: true,
110            tls_cert_path: None,
111            tls_key_path: None,
112            shutdown_timeout_ms: default_shutdown_timeout_ms(),
113        }
114    }
115}
116
117impl HttpServerConfig {
118    /// Create a new config with the given bind address.
119    #[must_use]
120    pub fn new(bind_address: impl Into<String>) -> Self {
121        Self {
122            bind_address: bind_address.into(),
123            ..Default::default()
124        }
125    }
126
127    /// Get request timeout as Duration.
128    #[must_use]
129    pub fn request_timeout(&self) -> Duration {
130        Duration::from_millis(self.request_timeout_ms)
131    }
132
133    /// Get keep-alive timeout as Duration.
134    #[must_use]
135    pub fn keep_alive_timeout(&self) -> Duration {
136        Duration::from_millis(self.keep_alive_timeout_ms)
137    }
138
139    /// Get shutdown timeout as Duration.
140    #[must_use]
141    pub fn shutdown_timeout(&self) -> Duration {
142        Duration::from_millis(self.shutdown_timeout_ms)
143    }
144
145    /// Check if TLS is configured.
146    #[must_use]
147    pub fn is_tls_enabled(&self) -> bool {
148        self.tls_cert_path.is_some() && self.tls_key_path.is_some()
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn test_default_config() {
158        let config = HttpServerConfig::default();
159        assert_eq!(config.bind_address, "0.0.0.0:8080");
160        assert_eq!(config.request_timeout_ms, 30_000);
161        assert_eq!(config.keep_alive_timeout_ms, 75_000);
162        assert_eq!(config.max_connections, 10_000);
163        assert!(config.enable_health_endpoints);
164        assert!(!config.enable_metrics_endpoint);
165        assert!(config.enable_http2);
166        assert!(!config.is_tls_enabled());
167    }
168
169    #[test]
170    fn test_new_with_address() {
171        let config = HttpServerConfig::new("127.0.0.1:3000");
172        assert_eq!(config.bind_address, "127.0.0.1:3000");
173    }
174
175    #[test]
176    fn test_tls_enabled() {
177        let mut config = HttpServerConfig::default();
178        assert!(!config.is_tls_enabled());
179
180        config.tls_cert_path = Some("/path/to/cert.pem".to_string());
181        assert!(!config.is_tls_enabled());
182
183        config.tls_key_path = Some("/path/to/key.pem".to_string());
184        assert!(config.is_tls_enabled());
185    }
186
187    #[test]
188    fn test_duration_conversions() {
189        let config = HttpServerConfig::default();
190        assert_eq!(config.request_timeout(), Duration::from_secs(30));
191        assert_eq!(config.keep_alive_timeout(), Duration::from_secs(75));
192        assert_eq!(config.shutdown_timeout(), Duration::from_secs(30));
193    }
194}