{"record":{"id":"e70e871d47d0e32e","repo":"headroomlabs-ai/headroom","slug":"failed-to-read-simulator-config-path-source","errorCode":null,"errorMessage":"failed to read simulator config {path}: {source}","messagePattern":"failed to read simulator config (.+?): (.+?)","errorType":"console","errorClass":"ConfigError","httpStatus":null,"severity":"error","filePath":"crates/headroom-simulators/src/config.rs","lineNumber":57,"sourceCode":"    pub headers: BTreeMap<String, String>,\n    #[serde(default)]\n    pub json: Option<Value>,\n    #[serde(default)]\n    pub body: Option<String>,\n    #[serde(default)]\n    pub sse: Vec<SseFrame>,\n}\n\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct SseFrame {\n    #[serde(default)]\n    pub event: Option<String>,\n    pub data: Value,\n}\n\n#[derive(Debug, Error)]\npub enum ConfigError {\n    #[error(\"failed to read simulator config {path}: {source}\")]\n    Read {\n        path: String,\n        #[source]\n        source: std::io::Error,\n    },\n    #[error(\"simulator config is not valid JSON: {0}\")]\n    Parse(#[from] serde_json::Error),\n}\n\npub fn load_config(path: Option<&Path>) -> Result<SimulatorConfig, ConfigError> {\n    let Some(path) = path else {\n        return Ok(SimulatorConfig::default());\n    };\n    let raw = fs::read_to_string(path).map_err(|source| ConfigError::Read {\n        path: path.display().to_string(),\n        source,\n    })?;\n    Ok(serde_json::from_str(&raw)?)","sourceCodeStart":39,"sourceCodeEnd":75,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/crates/headroom-simulators/src/config.rs#L39-L75","documentation":"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().","triggerScenarios":"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.","commonSituations":"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.","solutions":["Verify the path exists and is a regular file: ls -l <path> from the same working directory the binary runs in.","Check read permission (chmod +r) and, in containers, that the file was actually COPY'd into the image.","Pass an absolute path to load_config to avoid cwd-dependent resolution.","If no custom simulator behavior is needed, call load_config(None) to get the default config instead of pointing at a missing file."],"exampleFix":"// before\nlet cfg = load_config(Some(Path::new(\"sim.json\")))?;\n\n// after\nlet path = std::env::var_os(\"SIM_CONFIG\")\n    .map(PathBuf::from)\n    .filter(|p| p.exists()); // fall back to defaults when absent\nlet cfg = load_config(path.as_deref())?;","handlingStrategy":"validation","validationCode":"use std::path::Path;\n\nfn config_readable(path: &Path) -> bool {\n    path.is_file() && std::fs::metadata(path).map(|m| !m.permissions().readonly()).unwrap_or(false)\n}\n\n// before load_config:\nif !config_readable(&path) { eprintln!(\"missing config {path}, using defaults\"); }","typeGuard":"fn as_valid_config_path(p: Option<&std::path::Path>) -> Option<&std::path::Path> {\n    p.filter(|x| x.is_file())\n}","tryCatchPattern":"match load_config(Some(&path)) {\n    Err(ConfigError::Read { path, source }) => {\n        eprintln!(\"cannot read {path}: {source}; falling back to defaults\");\n        load_config(None)\n    }\n    other => other,\n}","preventionTips":["Resolve config paths to absolute form before passing them in, so behavior does not depend on cwd.","Add a startup doctor check that stats every configured file path and reports missing/unreadable ones.","In containers, assert in the Dockerfile that the config was copied: test -f /etc/headroom/sim.json."],"tags":["rust","config","io","filesystem"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}