sigoden/aichat · error · anyhow::Error

Invalid document loader

Error message

Invalid document loader '{extension}': `{loader_command}`

What it means

`run_loader_command` builds the loader command line with `shell_words::split(loader_command)`; when that parser rejects the string (unbalanced quotes, trailing backslash, unterminated escape), it wraps the parse error as "Invalid document loader '{extension}': `{loader_command}`". The extension identifies which configured loader was at fault, and the loader_command is echoed so the misconfiguration is visible.

Solutions

  1. Inspect the loader command for the given extension and fix unbalanced quotes, smart quotes, or dangling backslashes.
  2. Test the command string with `shell_words::split` semantics: plain space-separated tokens with single/double quoting.
  3. Validate loader commands at config-load time instead of at fetch time.
  4. Replace problematic quoting with simpler argument lists (no embedded quotes) if possible.

Example fix

// before
loaders.insert("pdf", "pdftotext - \"$1\""); // unbalanced quote -> Invalid document loader 'pdf'
// after
loaders.insert("pdf", "pdftotext - $1");
Defensive patterns

Strategy: validation

Validate before calling

fn validate_loader_command(cmd: &str) -> Result<(), String> {
    match shell_words::split(cmd) {
        Ok(args) if !args.is_empty() => Ok(()),
        Ok(_) => Err("loader command is empty".into()),
        Err(e) => Err(format!("cannot tokenize loader command {cmd:?}: {e}")),
    }
}

Try / catch

match run_loader_command(path, ext, &cmd) {
    Err(e) if e.to_string().starts_with("Invalid document loader") => {
        eprintln!("Check the loader definition for '{ext}': {e}");
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling `load_with_command`/`load_recursive_url`/`load_protocol_path`/`fetch_with_loaders` with a loader command string that `shell_words::split` cannot tokenize, e.g. `pandoc -f "markdown` (unterminated quote) or a dangling `\`.

Common situations: Typo in a loader definition in config (unbalanced quotes, stray backslash); copy-pasting commands with smart quotes or Windows-style quoting; environment-specific quoting that shell_words cannot parse.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of sigoden/aichat@82976d349a (2026-09-09). Data as JSON: /api/errors/448b2b57c81545d9. Report an issue: GitHub.

Appendix: source

Thrown at src/utils/command.rs:110

pub fn run_command_with_output<T: AsRef<OsStr>>(
    cmd: &str,
    args: &[T],
    envs: Option<HashMap<String, String>>,
) -> Result<(bool, String, String)> {
    let output = Command::new(cmd)
        .args(args.iter())
        .envs(envs.unwrap_or_default())
        .output()?;
    let status = output.status;
    let stdout = std::str::from_utf8(&output.stdout).context("Invalid UTF-8 in stdout")?;
    let stderr = std::str::from_utf8(&output.stderr).context("Invalid UTF-8 in stderr")?;
    Ok((status.success(), stdout.to_string(), stderr.to_string()))
}

pub fn run_loader_command(path: &str, extension: &str, loader_command: &str) -> Result<String> {
    let cmd_args = shell_words::split(loader_command)
        .with_context(|| anyhow!("Invalid document loader '{extension}': `{loader_command}`"))?;
    let mut use_stdout = true;
    let outpath = temp_file("-output-", "").display().to_string();
    let cmd_args: Vec<_> = cmd_args
        .into_iter()
        .map(|mut v| {
            if v.contains("$1") {
                v = v.replace("$1", path);
            }
            if v.contains("$2") {
                use_stdout = false;
                v = v.replace("$2", &outpath);
            }
            v
        })
        .collect();
    let cmd_eval = shell_words::join(&cmd_args);
    debug!("run `{cmd_eval}`");
    let (cmd, args) = cmd_args.split_at(1);

View on GitHub (pinned to 82976d349a)