rust-lang/mdBook · error

expected a file, `{}` appears to be {:?}

Error message

expected a file, `{}` appears to be {:?}

What it means

mdbook's copy helper (utils::fs::copy / copy_inner) opens the source with File::open and requires it to be a regular file. If the source path is a directory, fifo, socket, or other non-file, it bails with this message. It exists because the copy implementation streams file contents and assumes a regular file.

Source

Thrown at crates/mdbook-core/src/utils/fs.rs:179

    // This is a workaround for an issue with the macOS file watcher.
    // Rust's `std::fs::copy` function uses `fclonefileat`, which creates
    // clones on APFS. Unfortunately fs events seem to trigger on both
    // sides of the clone, and there doesn't seem to be a way to differentiate
    // which side it is.
    // https://github.com/notify-rs/notify/issues/465#issuecomment-1657261035
    // contains more information.
    //
    // This is essentially a copy of the simple copy code path in Rust's
    // standard library.
    #[cfg(target_os = "macos")]
    fn copy_inner(from: &Path, to: &Path) -> Result<()> {
        use std::fs::OpenOptions;
        use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};

        let mut reader = std::fs::File::open(from)?;
        let metadata = reader.metadata()?;
        if !metadata.is_file() {
            anyhow::bail!(
                "expected a file, `{}` appears to be {:?}",
                from.display(),
                metadata.file_type()
            );
        }
        let perm = metadata.permissions();
        let mut writer = OpenOptions::new()
            .mode(perm.mode())
            .write(true)
            .create(true)
            .truncate(true)
            .open(to)?;
        let writer_metadata = writer.metadata()?;
        if writer_metadata.is_file() {
            // Set the correct file permissions, in case the file already existed.
            // Don't set the permissions on already existing non-files like
            // pipes/FIFOs or device nodes.
            writer.set_permissions(perm)?;

View on GitHub (pinned to dc21064fc2)

Solutions

  1. Ensure the source path points to a regular file
  2. Use utils::fs::copy_files (recursive) instead of copy() when copying directories
  3. Remove or rename directories inside the src/theme directories that shouldn't be copied

Example fix

// before
utils::fs::copy(&src_path, &dest_path)?; // src_path is a directory
// after
if src_path.is_dir() {
    utils::fs::copy_files(&src_path, &dest_path)?;
} else {
    utils::fs::copy(&src_path, &dest_path)?;
}
Defensive patterns

Strategy: validation

Validate before calling

fn ensure_regular_file(p: &Path) -> std::io::Result<()> {
    let md = std::fs::metadata(p)?;
    if md.is_file() { Ok(()) } else {
        Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{} is not a file", p.display())))
    }
}

Type guard

fn is_regular_file(p: &Path) -> bool {
    std::fs::metadata(p).map(|m| m.is_file()).unwrap_or(false)
}

Try / catch

match utils::fs::copy(from, to) {
    Ok(()) => (),
    Err(e) if e.to_string().contains("expected a file") =>
        // fall back to recursive copy for directories
        utils::fs::copy_files(from, to)?,
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Passing a directory (or special file) as the source path to utils::fs::copy; mdbook internals copying a src item that is actually a directory (often from a bad `src` layout or symlink target).

Common situations: Custom renderers/preprocessors that place directories under the src or theme directory and then trigger a copy; users pointing `book.src` or theme paths at directories.

Related errors


AI-assisted analysis of rust-lang/mdBook@dc21064fc2 (2026-09-01). Data as JSON: /api/errors/2424040208a51ce3. Report an issue: GitHub.