feat(config): optional TOML config loading behind config-file feature
A file-based way to set tuning knobs without recompiling. urus is a library, so it never presumes a config path or reads the environment — the binary hands the text in: - Config::with_toml_str(&str) -> Result<Config, ConfigError>: sparse overlay onto an existing Config (built with the addr the binary chose). Only keys present are applied; durations are integer seconds; unknown keys are a hard error (deny_unknown_fields) so a typo is loud, not a silent no-op. - Scope: the four slowloris knobs (head_timeout_secs, body_timeout_secs, body_burst_bytes, body_stall_timeout_secs). Migrating the rest of Config into the file is a separate, additive job — TomlOverrides just grows. - Feature `config-file = ["dep:serde", "dep:toml"]`; the optional serde dep gains the derive feature. The default build is unchanged (deps + code are all gated). - examples/serve_toml.rs (required-features = ["config-file"]): a `--config PATH` demo with no presumed default location. plain_serve and its env vars are left untouched. Tests (feature-gated): empty keeps defaults, partial overrides only named, full overrides all, unknown key errors, malformed errors. 84 lib with the feature / 79 without; clippy --lib clean both ways; e2e smoke serves 200 from a file and rejects an unknown key loudly.
This commit is contained in:
+131
@@ -135,6 +135,79 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// config-file: TOML overlay for tuning knobs
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// urus is a library, so it never presumes a config-file path or reads the
|
||||
// environment — the embedding binary decides where a file lives and hands
|
||||
// the text here. This overlays a sparse TOML document onto an existing
|
||||
// `Config` (built with an addr the binary chose): only the keys present are
|
||||
// applied, everything else keeps the compiled default. Durations are
|
||||
// integer seconds. Unknown keys are a hard error so a typo is loud, not a
|
||||
// silent no-op.
|
||||
//
|
||||
// Scope for now: the slowloris-tuning knobs only. Migrating the rest of the
|
||||
// Config surface into the file is a separate, additive job (the loader
|
||||
// mechanism is general — it just extends `TomlOverrides`).
|
||||
|
||||
/// Error from [`Config::with_toml_str`]: the TOML failed to parse or carried
|
||||
/// an unknown/mistyped key.
|
||||
#[cfg(feature = "config-file")]
|
||||
#[derive(Debug)]
|
||||
pub enum ConfigError {
|
||||
Toml(String),
|
||||
}
|
||||
|
||||
#[cfg(feature = "config-file")]
|
||||
impl std::fmt::Display for ConfigError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ConfigError::Toml(m) => write!(f, "config TOML error: {m}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "config-file")]
|
||||
impl std::error::Error for ConfigError {}
|
||||
|
||||
#[cfg(feature = "config-file")]
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct TomlOverrides {
|
||||
head_timeout_secs: Option<u64>,
|
||||
body_timeout_secs: Option<u64>,
|
||||
body_burst_bytes: Option<usize>,
|
||||
body_stall_timeout_secs: Option<u64>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "config-file")]
|
||||
impl Config {
|
||||
/// Overlay a TOML document of tuning knobs onto this config (sparse:
|
||||
/// only the keys present are applied). Durations are integer seconds.
|
||||
///
|
||||
/// Recognized keys: `head_timeout_secs`, `body_timeout_secs`,
|
||||
/// `body_burst_bytes`, `body_stall_timeout_secs`. Unknown keys error.
|
||||
/// Other `Config` knobs are not yet file-configurable.
|
||||
pub fn with_toml_str(mut self, s: &str) -> Result<Self, ConfigError> {
|
||||
let o: TomlOverrides =
|
||||
toml::from_str(s).map_err(|e| ConfigError::Toml(e.to_string()))?;
|
||||
if let Some(v) = o.head_timeout_secs {
|
||||
self.head_timeout = Duration::from_secs(v);
|
||||
}
|
||||
if let Some(v) = o.body_timeout_secs {
|
||||
self.body_timeout = Duration::from_secs(v);
|
||||
}
|
||||
if let Some(v) = o.body_burst_bytes {
|
||||
self.body_burst_bytes = v;
|
||||
}
|
||||
if let Some(v) = o.body_stall_timeout_secs {
|
||||
self.body_stall_timeout = Duration::from_secs(v);
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// dup helper
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -470,3 +543,61 @@ pub fn serve(addr: impl ToSocketAddrs, pipeline: Pipeline) -> io::Result<()> {
|
||||
.ok_or_else(|| io::Error::new(ErrorKind::InvalidInput, "no addresses resolved"))?;
|
||||
serve_with(Config::new(addr), pipeline)
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "config-file"))]
|
||||
mod config_file_tests {
|
||||
use super::*;
|
||||
|
||||
fn base() -> Config {
|
||||
Config::new("127.0.0.1:0".parse().unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toml_empty_keeps_defaults() {
|
||||
let d = base();
|
||||
let c = base().with_toml_str("").unwrap();
|
||||
assert_eq!(c.head_timeout, d.head_timeout);
|
||||
assert_eq!(c.body_timeout, d.body_timeout);
|
||||
assert_eq!(c.body_burst_bytes, d.body_burst_bytes);
|
||||
assert_eq!(c.body_stall_timeout, d.body_stall_timeout);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toml_partial_overrides_only_named() {
|
||||
let d = base();
|
||||
let c = base().with_toml_str("head_timeout_secs = 5").unwrap();
|
||||
assert_eq!(c.head_timeout, Duration::from_secs(5)); // overridden
|
||||
assert_eq!(c.body_timeout, d.body_timeout); // default kept
|
||||
assert_eq!(c.body_burst_bytes, d.body_burst_bytes); // default kept
|
||||
assert_eq!(c.body_stall_timeout, d.body_stall_timeout);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toml_full_overrides_all() {
|
||||
let c = base()
|
||||
.with_toml_str(
|
||||
"head_timeout_secs = 10\n\
|
||||
body_timeout_secs = 120\n\
|
||||
body_burst_bytes = 8192\n\
|
||||
body_stall_timeout_secs = 15\n",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(c.head_timeout, Duration::from_secs(10));
|
||||
assert_eq!(c.body_timeout, Duration::from_secs(120));
|
||||
assert_eq!(c.body_burst_bytes, 8192);
|
||||
assert_eq!(c.body_stall_timeout, Duration::from_secs(15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toml_unknown_key_errors() {
|
||||
// A mistyped/unknown key is a hard error, not a silent no-op.
|
||||
let e = base().with_toml_str("body_timeout_sec = 120"); // typo: missing 's'
|
||||
assert!(e.is_err(), "unknown key should error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toml_malformed_errors() {
|
||||
let e = base().with_toml_str("this is not = valid = toml");
|
||||
assert!(e.is_err(), "malformed TOML should error");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user