headroomlabs-ai/headroom · error · ConfigError

failed to read simulator config {path}: {source}

Error message

failed to read simulator config {path}: {source}

What it means

Raised by load_config in headroom-simulators when fs::read_to_string fails on the explicitly provided simulator config path. The std::io::Error source is preserved via #[source], so the underlying reason (NotFound, PermissionDenied, etc.) appears in the chain. It never fires when path is None — the function then returns SimulatorConfig::default().

Source

Thrown at crates/headroom-simulators/src/config.rs:57

    pub headers: BTreeMap<String, String>,
    #[serde(default)]
    pub json: Option<Value>,
    #[serde(default)]
    pub body: Option<String>,
    #[serde(default)]
    pub sse: Vec<SseFrame>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SseFrame {
    #[serde(default)]
    pub event: Option<String>,
    pub data: Value,
}

#[derive(Debug, Error)]
pub enum ConfigError {
    #[error("failed to read simulator config {path}: {source}")]
    Read {
        path: String,
        #[source]
        source: std::io::Error,
    },
    #[error("simulator config is not valid JSON: {0}")]
    Parse(#[from] serde_json::Error),
}

pub fn load_config(path: Option<&Path>) -> Result<SimulatorConfig, ConfigError> {
    let Some(path) = path else {
        return Ok(SimulatorConfig::default());
    };
    let raw = fs::read_to_string(path).map_err(|source| ConfigError::Read {
        path: path.display().to_string(),
        source,
    })?;
    Ok(serde_json::from_str(&raw)?)

View on GitHub (pinned to 322425c43b)

Solutions

  1. Verify the path exists and is a regular file: ls -l <path> from the same working directory the binary runs in.
  2. Check read permission (chmod +r) and, in containers, that the file was actually COPY'd into the image.
  3. Pass an absolute path to load_config to avoid cwd-dependent resolution.
  4. If no custom simulator behavior is needed, call load_config(None) to get the default config instead of pointing at a missing file.

Example fix

// before
let cfg = load_config(Some(Path::new("sim.json")))?;

// after
let path = std::env::var_os("SIM_CONFIG")
    .map(PathBuf::from)
    .filter(|p| p.exists()); // fall back to defaults when absent
let cfg = load_config(path.as_deref())?;
Defensive patterns

Strategy: validation

Validate before calling

use std::path::Path;

fn config_readable(path: &Path) -> bool {
    path.is_file() && std::fs::metadata(path).map(|m| !m.permissions().readonly()).unwrap_or(false)
}

// before load_config:
if !config_readable(&path) { eprintln!("missing config {path}, using defaults"); }

Type guard

fn as_valid_config_path(p: Option<&std::path::Path>) -> Option<&std::path::Path> {
    p.filter(|x| x.is_file())
}

Try / catch

match load_config(Some(&path)) {
    Err(ConfigError::Read { path, source }) => {
        eprintln!("cannot read {path}: {source}; falling back to defaults");
        load_config(None)
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling load_config(Some(path)) where path does not exist, is a directory, has no read permission, or is otherwise unreadable at the OS level.

Common situations: Typos in the --simulator-config flag, pointing at a config file that was never committed to the container image, restrictive file modes in a slim Docker image, or a relative path resolved against an unexpected working directory.

Related errors


AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15). Data as JSON: /api/errors/e70e871d47d0e32e. Report an issue: GitHub.