Automattic/harper · error · anyhow::Error

Unable to convert URI to file path.

Error message

Unable to convert URI to file path.

What it means

harper-ls's `fileify_path` converts an LSP document URI into a filesystem-friendly file name (Neovim undo-file style) by calling `UriExt::to_file_path()` on the URI. That conversion only succeeds for URIs that actually denote local file paths (e.g. `file:///home/user/doc.md`). When the URI cannot be mapped to a file path — it is `None` — the function short-circuits with this anyhow error, since per-client persisted state (ignored lints, per-file dictionaries) would otherwise have nowhere well-defined to live.

Source

Thrown at harper-ls/src/io_utils.rs:14

use anyhow::anyhow;
use std::path::{Component, PathBuf};

use tower_lsp_server::{UriExt, lsp_types::Uri};

/// Rewrites a path to a filename using the same conventions as
/// [Neovim's undo-files](https://neovim.io/doc/user/options.html#'undodir').
pub fn fileify_path(uri: &Uri) -> anyhow::Result<PathBuf> {
    let mut rewritten = String::new();

    // We assume all URLs are local files and have a base.
    for seg in uri
        .to_file_path()
        .ok_or_else(|| anyhow!("Unable to convert URI to file path."))?
        .components()
    {
        if !matches!(seg, Component::RootDir) {
            rewritten.push_str(&seg.as_os_str().to_string_lossy());
            rewritten.push('%');
        }
    }

    Ok(rewritten.into())
}

View on GitHub (pinned to 5fe7d5ab76)

Solutions

  1. Identify the URI in the failing editor notification; it is almost certainly non-`file:` (untitled/virtual/remote document).
  2. Guard in the caller: skip persistence for non-file URIs by checking the scheme (e.g. only proceed when the URI starts with `file:` or when `to_file_path()` succeeds) instead of propagating the error.
  3. If the document is `untitled:`, save it to disk first or configure the editor to assign a real path; harper-ls cannot derive an on-disk path otherwise.
  4. For remote/SSH setups, ensure the language server runs on the same host as the files so the client sends real `file:` URIs.
  5. If you control the code, fall back to a hash of the URI string when a file path cannot be derived, so state can still be persisted per-URI.

Example fix

// before
let path = fileify_path(&uri)?;

// after: skip non-file URIs instead of failing
let path = match uri.to_file_path() {
    Some(_) => fileify_path(&uri)?,
    None => return Ok(None), // untitled/virtual document: no on-disk state
};
Defensive patterns

Strategy: validation

Validate before calling

// Rust: validate the URI before calling fileify_path
fn is_local_file(uri: &lsp_types::Uri) -> bool {
    uri.as_str().starts_with("file:") || uri.to_file_path().is_some()
}

if !is_local_file(&uri) {
    // skip persistence / return early instead of erroring
}

Type guard

// Rust: narrow an arbitrary Uri to a convertible file URI
fn as_file_path(uri: &Uri) -> Option<PathBuf> {
    uri.to_file_path()
}

Try / catch

// Rust (anyhow): match on the error and degrade gracefully
match fileify_path(&uri) {
    Ok(p) => get_ignored_lints_path(&p),
    Err(e) if e.to_string().contains("Unable to convert URI to file path") => {
        // non-file URI: treat as no persisted state
        eprintln!("skipping state for non-file URI: {uri}");
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling `fileify_path` (directly or indirectly via `get_ignored_lints_path` / `get_file_dict_path`) with an LSP `Uri` whose scheme is not `file:` — e.g. `untitled:` buffers, `http(s)://` remote documents, `jdt://`/`zipfile://` virtual schemes, or a malformed URI that `to_file_path()` cannot parse into a local path.

Common situations: A client sends `didOpen`/`didChange`/`codeAction` notifications for an unsaved scratch buffer (`untitled:Untitled-1`) or a remotely-hosted/virtual document (remote SSH, browser-based editors, container schemes). Editors like Neovim and VS Code emit such URIs for new or virtual files, and harper-ls then fails while resolving the per-file ignored-lints or dictionary path.

Understand the failure class

Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.

Related errors


AI-assisted analysis of Automattic/harper@5fe7d5ab76 (2026-09-06). Data as JSON: /api/errors/8f3510fd843a01bd. Report an issue: GitHub.