sigoden/aichat · error · anyhow::Error

No document loader for

Error message

No document loader for '{}'

What it means

`load_protocol_path` parses a path of the form `protocol:rest`, looks the protocol up in the configured `loaders` map, and returns `anyhow!("No document loader for '{}'", path)` when the protocol is missing or the path has no `protocol:` prefix at all (`split_once(':')` yields None). It means the given URI scheme has no loader registered, so the document cannot be fetched.

Solutions

  1. Register a loader for the protocol: add an entry to the `loaders` map keyed by the scheme in the path.
  2. Fix the path to use a protocol that has a configured loader.
  3. Check for typos between the path prefix and the loader key (e.g. `https:` vs `http:`).
  4. Validate all document paths against the loader keys before starting a sync run.

Example fix

// before
let mut loaders = HashMap::new();
loaders.insert("http", "curl -L $1");
load_protocol_path("s3:manual.md", &loaders)?; // No document loader for 's3:manual.md'
// after
loaders.insert("s3", "aws s3 cp $1 -");
load_protocol_path("s3:manual.md", &loaders)?;
Defensive patterns

Strategy: validation

Validate before calling

fn loader_registered(path: &str, loaders: &HashMap<String, String>) -> Result<(), String> {
    match path.split_once(':') {
        Some((protocol, _)) if loaders.contains_key(protocol) => Ok(()),
        Some((protocol, _)) => Err(format!("no loader registered for protocol '{protocol}'")),
        None => Err(format!("path {path:?} has no 'protocol:' prefix")),
    }
}

Try / catch

match load_protocol_path(p, &loaders) {
    Err(e) if e.to_string().starts_with("No document loader for") => {
        eprintln!("Skipping {p}: no loader configured");
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling `load_protocol_path` (directly or via `load_documents`/`sync_documents`) with a path whose prefix is absent from the `loaders` map, e.g. `s3:docs/report.md` when only `http` is configured, or a plain `README.md` with no colon at all.

Common situations: Typo'd protocol in a document list; loader removed or renamed in config while old references remain; forgetting to register a loader for a scheme used in a docs index.

Understand the failure class

Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.

Related errors


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

Appendix: source

Thrown at src/utils/loader.rs:103

pub fn is_loader_protocol(loaders: &HashMap<String, String>, path: &str) -> bool {
    match path.split_once(':') {
        Some((protocol, _)) => loaders.contains_key(protocol),
        None => false,
    }
}

pub fn load_protocol_path(
    loaders: &HashMap<String, String>,
    path: &str,
) -> Result<Vec<LoadedDocument>> {
    let (protocol, loader_command, new_path) = path
        .split_once(':')
        .and_then(|(protocol, path)| {
            let loader_command = loaders.get(protocol)?;
            Some((protocol, loader_command, path))
        })
        .ok_or_else(|| anyhow!("No document loader for '{}'", path))?;
    let contents = run_loader_command(new_path, protocol, loader_command)?;
    let output = if let Ok(list) = serde_json::from_str::<Vec<LoadedDocument>>(&contents) {
        list.into_iter()
            .map(|mut v| {
                if v.path.starts_with(path) {
                } else if v.path.starts_with(new_path) {
                    v.path = format!("{}:{}", protocol, v.path);
                } else {
                    v.path = format!("{}/{}", path, v.path);
                }
                v
            })
            .collect()
    } else {
        vec![LoadedDocument::new(
            path.into(),
            contents,
            Default::default(),

View on GitHub (pinned to 82976d349a)