clockworklabs/SpacetimeDB · error · io::Error
cannot replace without enclosing directory
Error message
cannot replace {} without enclosing directory What it means
crates/paths provides a macro-generated Path wrapper whose write() does atomic replace: it requires an enclosing directory to place the temp file (NamedTempFile::new_in(parent)) and to fsync the directory afterwards. It calls path.parent() and fails with ErrorKind::InvalidInput, 'cannot replace {path} without enclosing directory', when the path has no parent component. Per std::path semantics, only the empty path "" and the root "/" have no parent.
Solutions
- Validate the path is non-empty and has a file name before calling write (assert path.file_name() is Some)
- Fix the source of the empty path: default the config value to a real directory-qualified location like a subdirectory plus file name
- Log the offending path at the configuration boundary so the bad value is visible
Example fix
// before
let p: PathBuf = std::env::var("STATE_FILE").unwrap_or_default().into();
p.write(b"{}")?; // InvalidInput: cannot replace without enclosing directory
// after
let raw = std::env::var("STATE_FILE").unwrap_or_default();
anyhow::ensure!(!raw.is_empty(), "STATE_FILE must be set to a file path");
let p = PathBuf::from(raw);
anyhow::ensure!(p.file_name().is_some(), "STATE_FILE must name a file, got {raw:?}");
p.write(b"{}")?; Defensive patterns
Strategy: validation
Validate before calling
fn writable_target(p: &Path) -> anyhow::Result<()> {
anyhow::ensure!(!p.as_os_str().is_empty(), "path must not be empty");
anyhow::ensure!(p.file_name().is_some(), "path must name a file, got {}", p.display());
Ok(())
} Type guard
fn has_enclosing_dir(p: &Path) -> bool { p.parent().map(|d| !d.as_os_str().is_empty()).unwrap_or(false) } Try / catch
if let Err(e) = path.write(contents) {
if e.kind() == std::io::ErrorKind::InvalidInput
&& e.to_string().contains("without enclosing directory") {
return Err(anyhow::anyhow!("config produced invalid path {}", path.display()));
}
return Err(e.into());
} Prevention
- Validate config-supplied paths (non-empty, has file name) at load time
- Default every config path to a directory-qualified value, never ""
- Fail fast on unset env vars instead of defaulting them to empty strings
When it happens
Trigger: Calling .write(...) on a Path constructed from an empty string; writing to the filesystem root "/"; building a path from unvalidated/unset config (e.g. empty env var) that joins to ""; a component-joining bug that produced an empty path string.
Common situations: Config-driven file locations where a variable is missing and defaults to "" (unset HOME, empty state_dir); CLI tools writing to a user-supplied path that turns out empty; path manipulation that strips all components.
Related errors
- failed to fsync directory
- failed to fsync file
- failed to open directory
- failed to open file for fsync
- Module bindings path must be a relative path
AI-assisted analysis of clockworklabs/SpacetimeDB@6dee26c6ef (2026-08-20).
Data as JSON: /api/errors/b9fde14219e1531b.
Report an issue: GitHub.
Appendix: source
Thrown at crates/paths/src/utils.rs:111
}
};
($(#[$($attr:tt)+])* $name:ident: file) => {
path_type!($(#[$($attr)+])* $name);
impl $name {
pub fn read(&self) -> std::io::Result<Vec<u8>> {
std::fs::read(self)
}
pub fn read_to_string(&self) -> std::io::Result<String> {
std::fs::read_to_string(self)
}
pub fn write(&self, contents: impl AsRef<[u8]>) -> std::io::Result<()> {
use std::io::Write as _;
let path = &self.0;
let parent = path.parent().ok_or_else(||
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("cannot replace {} without enclosing directory", path.display()))
)?;
std::fs::create_dir_all(&parent)?;
let mut tmp = $crate::__tempfile::NamedTempFile::new_in(parent)?;
tmp.write_all(contents.as_ref())?;
tmp.as_file().sync_all()?;
tmp.persist(&path)?;
// On Windows, syncing the directory is not necessary and doesn't even work.
#[cfg(not(target_os = "windows"))]
std::fs::File::open(parent)?.sync_all()?;
Ok(())
}
/// Opens a file at this path with the given options, ensuring its parent directory exists.
#[inline]View on GitHub (pinned to 6dee26c6ef)