stamparm/maltrail · error

settings::init() must run before statics()

Error message

settings::init() must run before statics()

What it means

`settings::statics()` reads a lazily-initialized global (`STATICS`, typically a `OnceLock`) and `.expect()`s that `settings::init()` was called first. Calling `statics()` before initialization panics with 'settings::init() must run before statics()'. This is an initialization-order contract: compiled settings/static tables can only be built after user configuration is loaded.

Solutions

  1. Call `settings::init()` (with the required config) at the start of the entry point or test before anything touches `statics()`.
  2. In tests, add an init call or a shared `once` fixture (e.g. a `#[ctor]`/`OnceCell` helper) so every test that transitively uses `statics()` is covered.
  3. Make `statics()` resilient if appropriate: return `Option<&'static Statics>` or fall back to defaults, converting the panic into a clear init error.
  4. Audit all binaries/threads: ensure init happens before spawning anything that reads settings, and consider asserting initialization in a startup self-check.

Example fix

// before
let statics = settings::statics(); // panics if init not yet run
// after
settings::init(&config).expect("settings init failed");
let statics = settings::statics();
Defensive patterns

Strategy: type-guard

Validate before calling

// caller-side pre-check
fn statics_ready() -> bool { settings::initialized() } // or STATICS.get().is_some()

Type guard

fn try_statics() -> Option<&'static Statics> { settings::try_statics() }

Try / catch

// std::sync::OnceLock based
let statics = STATICS.get().unwrap_or_else(||
    panic!("settings::init() must run before statics(); call it in main()/test setup"));

Prevention

When it happens

Trigger: Any code path — a unit test, a binary entry point, a background thread or lazy static — dereferences `settings::statics()` (or helpers that call it, like `ac()` pattern builders) before `settings::init()` has stored the value. Typical: a test that touches a module using `statics()` without calling `settings::init()` first, or a second binary/entry point missing the init call.

Common situations: New integration tests that import sensor modules but skip the init fixture; reordering startup so a consumer runs before init; spawning threads that read statics before the main thread initializes; adding a new binary target without copying the init sequence from main.

Related errors


AI-assisted analysis of stamparm/maltrail@77cfb06d76 (2026-09-13). Data as JSON: /api/errors/ae30570d26fc0e9f. Report an issue: GitHub.

Appendix: source

Thrown at sensor/src/settings.rs:239

    // It stays out of the trailing label above, where no real TLD has one.
    b[..i - 1].iter().all(|c| c.is_ascii_alphanumeric() || *c == b'.' || *c == b'-' || *c == b'_')
}

/// `\A\d+\-\d+\-\d+\-\d+\Z`, hand-coded — the dashed-quad first label check.
#[inline]
pub fn is_dashed_quad(label: &str) -> bool {
    let mut groups = 0;
    for part in label.split('-') {
        if part.is_empty() || !part.bytes().all(|c| c.is_ascii_digit()) {
            return false;
        }
        groups += 1;
    }
    groups == 4
}

pub fn statics() -> &'static Statics {
    STATICS.get().expect("settings::init() must run before statics()")
}

fn ac(patterns: &[&str]) -> AhoCorasick {
    AhoCorasick::new(patterns).expect("aho-corasick build")
}

/// `ac()` for patterns that have to match regardless of case, which is how HTTP header names
/// arrive on the wire.
fn ac_nocase(patterns: &[&str]) -> AhoCorasick {
    aho_corasick::AhoCorasickBuilder::new().ascii_case_insensitive(true).build(patterns).expect("aho-corasick build")
}

impl Statics {
    pub fn build(root: PathBuf) -> Statics {
        let ua_src = build_suspicious_ua_regex(&root);
        let suspicious_ua = match pyre::build(&ua_src) {
            Ok(re) => Some(re),
            Err(e) => {

View on GitHub (pinned to 77cfb06d76)