Skip to main content

rust_web_server/server_config/
mod.rs

1//! Typed server configuration.
2//!
3//! [`ServerConfig`] holds all per-instance configuration fields as typed Rust
4//! values. It replaces point-of-use `env::var("RWS_CONFIG_*")` calls with a
5//! struct that can be:
6//!
7//! - Built from environment variables once at startup: [`ServerConfig::from_env()`]
8//! - Constructed directly in tests without touching the environment:
9//!   [`ServerConfig::default()`] / struct update syntax
10//! - Passed to [`App::with_config`] to create a fully isolated application
11//!   instance — essential for parallel tests and embedded multi-tenant use
12//!
13//! # Test isolation example
14//!
15//! ```rust,ignore
16//! use rust_web_server::app::App;
17//! use rust_web_server::server_config::ServerConfig;
18//! use rust_web_server::test_client::TestClient;
19//!
20//! // No env writes, no lock needed.
21//! let app = App::with_config(ServerConfig {
22//!     cors_allow_all: false,
23//!     cors_allow_origins: "https://example.com".to_string(),
24//!     ..ServerConfig::default()
25//! });
26//! let client = TestClient::new(app);
27//! let res = client.get("/").send();
28//! ```
29
30#[cfg(test)]
31mod tests;
32
33use std::sync::atomic::{AtomicU64, Ordering};
34use std::sync::{Arc, RwLock};
35
36use crate::entry_point::Config;
37
38/// Bumped whenever `RWS_CONFIG_*` variables change (see [`invalidate`]).
39static GENERATION: AtomicU64 = AtomicU64::new(1);
40
41/// The snapshot [`ServerConfig::current`] hands out, with the generation it
42/// was built at.
43static CURRENT: RwLock<Option<(u64, Arc<ServerConfig>)>> = RwLock::new(None);
44
45/// Mark the cached [`ServerConfig::current`] snapshot stale: the next call
46/// re-reads the environment. Called by everything in the crate that writes
47/// `RWS_CONFIG_*` variables (config file, command line, defaults,
48/// `config_reload::reload`); call it yourself after `std::env::set_var` of
49/// a setting that should take effect immediately.
50pub fn invalidate() {
51    GENERATION.fetch_add(1, Ordering::SeqCst);
52}
53
54/// Default `Content-Security-Policy` header value. Mirrors
55/// `Header::_CONTENT_SECURITY_POLICY_VALUE_DEFAULT` without creating a
56/// circular import (`server_config` ← `header` ← `cors` ← `server_config`).
57const CSP_DEFAULT: &str = "default-src 'self'";
58
59/// All runtime-configurable settings for one server instance.
60///
61/// Fields map 1-to-1 to `RWS_CONFIG_*` environment variable names documented
62/// in [`Config`]. Default values match the environment-variable defaults.
63///
64/// Construct via [`ServerConfig::from_env()`] at startup or
65/// [`ServerConfig::default()`] in tests.
66#[derive(Clone, Debug, PartialEq)]
67pub struct ServerConfig {
68    // ── CORS ─────────────────────────────────────────────────────────────────
69    /// `RWS_CONFIG_CORS_ALLOW_ALL` — when `true`, all cross-origin requests are
70    /// reflected back as allowed (echo the `Origin` header). Overrides all
71    /// other CORS fields. Default: `true`.
72    pub cors_allow_all: bool,
73    /// `RWS_CONFIG_CORS_ALLOW_ORIGINS` — comma-separated list of allowed
74    /// origins when `cors_allow_all` is `false`. Default: `""` (none).
75    pub cors_allow_origins: String,
76    /// `RWS_CONFIG_CORS_ALLOW_CREDENTIALS` — value for the
77    /// `Access-Control-Allow-Credentials` response header. Default: `""`.
78    pub cors_allow_credentials: String,
79    /// `RWS_CONFIG_CORS_ALLOW_METHODS` — value for
80    /// `Access-Control-Allow-Methods`. Default: `""`.
81    pub cors_allow_methods: String,
82    /// `RWS_CONFIG_CORS_ALLOW_HEADERS` — value for
83    /// `Access-Control-Allow-Headers`. Default: `""`.
84    pub cors_allow_headers: String,
85    /// `RWS_CONFIG_CORS_EXPOSE_HEADERS` — value for
86    /// `Access-Control-Expose-Headers`. Default: `""`.
87    pub cors_expose_headers: String,
88    /// `RWS_CONFIG_CORS_MAX_AGE` — value for `Access-Control-Max-Age`.
89    /// Default: `"86400"`.
90    pub cors_max_age: String,
91
92    // ── Security headers ──────────────────────────────────────────────────────
93    /// `RWS_CONFIG_CSP` — `Content-Security-Policy` header value. An empty
94    /// string suppresses the header entirely. Default: the framework default CSP.
95    pub csp: String,
96
97    // ── Server internals ──────────────────────────────────────────────────────
98    /// `RWS_CONFIG_LOG_FORMAT` — `"json"` or `"combined"`. Default: `"json"`.
99    pub log_format: String,
100    /// `RWS_CONFIG_REQUEST_ALLOCATION_SIZE_IN_BYTES` — bytes allocated for
101    /// incoming request parsing. Default: `10000`.
102    pub request_allocation_size: i64,
103}
104
105impl Default for ServerConfig {
106    fn default() -> Self {
107        Self {
108            cors_allow_all: Config::RWS_CONFIG_CORS_ALLOW_ALL_DEFAULT_VALUE
109                .eq_ignore_ascii_case("true"),
110            cors_allow_origins: Config::RWS_CONFIG_CORS_ALLOW_ORIGINS_DEFAULT_VALUE.to_string(),
111            cors_allow_credentials: Config::RWS_CONFIG_CORS_ALLOW_CREDENTIALS_DEFAULT_VALUE
112                .to_string(),
113            cors_allow_methods: Config::RWS_CONFIG_CORS_ALLOW_METHODS_DEFAULT_VALUE.to_string(),
114            cors_allow_headers: Config::RWS_CONFIG_CORS_ALLOW_HEADERS_DEFAULT_VALUE.to_string(),
115            cors_expose_headers: Config::RWS_CONFIG_CORS_EXPOSE_HEADERS_DEFAULT_VALUE.to_string(),
116            cors_max_age: Config::RWS_CONFIG_CORS_MAX_AGE_DEFAULT_VALUE.to_string(),
117            csp: CSP_DEFAULT.to_string(),
118            log_format: Config::RWS_CONFIG_LOG_FORMAT_DEFAULT_VALUE.to_string(),
119            request_allocation_size: *Config::RWS_DEFAULT_REQUEST_ALLOCATION_SIZE_IN_BYTES,
120        }
121    }
122}
123
124impl ServerConfig {
125    /// Build a `ServerConfig` by reading all `RWS_CONFIG_*` environment
126    /// variables. Missing variables fall back to their default values.
127    ///
128    /// Each call reads every variable — the environment lock is taken once
129    /// per variable — so per-request code uses [`ServerConfig::current`].
130    pub fn from_env() -> Self {
131        let read = |key: &str| std::env::var(key).unwrap_or_default();
132        Self {
133            cors_allow_all: read(Config::RWS_CONFIG_CORS_ALLOW_ALL)
134                .eq_ignore_ascii_case("true"),
135            cors_allow_origins: read(Config::RWS_CONFIG_CORS_ALLOW_ORIGINS),
136            cors_allow_credentials: read(Config::RWS_CONFIG_CORS_ALLOW_CREDENTIALS),
137            cors_allow_methods: read(Config::RWS_CONFIG_CORS_ALLOW_METHODS),
138            cors_allow_headers: read(Config::RWS_CONFIG_CORS_ALLOW_HEADERS),
139            cors_expose_headers: read(Config::RWS_CONFIG_CORS_EXPOSE_HEADERS),
140            cors_max_age: read(Config::RWS_CONFIG_CORS_MAX_AGE),
141            csp: std::env::var("RWS_CONFIG_CSP")
142                .unwrap_or_else(|_| CSP_DEFAULT.to_string()),
143            log_format: read(Config::RWS_CONFIG_LOG_FORMAT),
144            request_allocation_size: std::env::var(
145                Config::RWS_CONFIG_REQUEST_ALLOCATION_SIZE_IN_BYTES,
146            )
147            .ok()
148            .and_then(|v| v.parse().ok())
149            .unwrap_or(*Config::RWS_DEFAULT_REQUEST_ALLOCATION_SIZE_IN_BYTES),
150        }
151    }
152
153    /// The configuration from the environment, read once and shared: an
154    /// atomic load and an `Arc` clone per call instead of ten `getenv`s.
155    /// Rebuilt after [`invalidate`] — which `config_reload::reload()` (SIGHUP,
156    /// `POST /admin/config/reload`) and every config writer in the crate call,
157    /// so hot reload still takes effect on the next request.
158    pub fn current() -> Arc<ServerConfig> {
159        let generation = GENERATION.load(Ordering::SeqCst);
160        if let Some((built_at, config)) = CURRENT.read().unwrap_or_else(|e| e.into_inner()).as_ref() {
161            if *built_at == generation {
162                return Arc::clone(config);
163            }
164        }
165        // Built from the generation read *before* the environment: a change
166        // during the rebuild leaves the cache stale, not wrong.
167        let fresh = Arc::new(ServerConfig::from_env());
168        *CURRENT.write().unwrap_or_else(|e| e.into_inner()) = Some((generation, Arc::clone(&fresh)));
169        fresh
170    }
171}