sxyazi/yazi · error · io::Error

Not a SFTP URL: {url}

Error message

Not a SFTP URL: {url}

What it means

SftpFs::new requires the URL to be the Unix variant carrying auth (`Url::Unix { loc, auth }`), which is how SFTP locations are represented. Any other URL shape (local OS/Unix-without-auth) is rejected with "Not a SFTP URL". The error mirrors the local engine's guard but in reverse.

Source

Thrown at yazi-vfs/src/engine/sftp/sftp.rs:124

	}

	async fn hard_link<P>(&self, to: P) -> io::Result<()>
	where
		P: DynPath,
	{
		let to = to.dyn_path().as_unix()?;

		Ok(self.op().await?.hardlink(self.path, to).await?)
	}

	async fn metadata(&self) -> io::Result<yazi_fs::cha::Cha> {
		let attrs = self.op().await?.stat(self.path).await?;
		Ok(Cha::try_from((self.path.file_name().unwrap_or_default(), &attrs))?.0)
	}

	async fn new<'b>(url: Url<'b>) -> io::Result<Self::Me<'b>> {
		let Url::Unix { loc, auth } = url else {
			return Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not a SFTP URL: {url}")));
		};

		let config: Arc<ServiceSftp> = Vfs::service(auth)?;
		let pool = Conn::pool(config.clone());
		Ok(Self::Me { url, path: loc.as_inner(), config, pool })
	}

	async fn read_dir(self) -> io::Result<Self::ReadDir> {
		Ok(Self::ReadDir {
			dir:    Arc::new(self.url.to_owned()),
			reader: self.op().await?.read_dir(self.path).await?,
		})
	}

	async fn read_link(&self) -> io::Result<PathBufDyn> {
		Ok(self.op().await?.readlink(self.path).await?.into())
	}

View on GitHub (pinned to 8ebf930f17)

Solutions

  1. Check the URL variant is `Url::Unix` with auth before using the SFTP engine; use the local engine for local URLs.
  2. If the target is remote, construct the URL with its SFTP auth intact (don't strip auth via `physical()`/as_local conversions).
  3. Route by scheme: local -> yazi-fs local engine, ssh/sftp -> yazi-vfs SftpFs.

Example fix

// before
let file = SftpFs::File::new(url).await?; // url is local
// after
match &url {
    Url::Unix { auth, .. } if !auth.is_empty() => SftpFs::File::new(url).await?,
    _ => LocalFs::File::new(url).await?,
}
Defensive patterns

Strategy: type-guard

Validate before calling

let is_sftp = matches!(url, Url::Unix { .. } if url.auth().map_or(false, |a| !a.is_empty()));
if !is_sftp { bail!("SftpFs requires a Unix URL with auth"); }

Type guard

fn as_sftp(url: &Url) -> Option<(&Loc, &AuthArc)> {
    match url { Url::Unix { loc, auth } if !auth.is_empty() => Some((loc, auth)), _ => None }
}

Try / catch

// io::Error with InvalidInput kind
match SftpFs::File::new(url).await {
    Err(e) if e.kind() == io::ErrorKind::InvalidInput => return LocalFs::File::new(url).await.map_err(Into::into),
    other => other.map_err(Into::into),
}

Prevention

When it happens

Trigger: Calling the SFTP engine's `new` with a local URL (Url::Os or Url::Unix without auth), e.g. routing every file through Vfs without checking scheme.

Common situations: Preview/thumbnail or task code that assumes Vfs handles all URLs but passes a purely local path; a missing `auth` component after hand-building a Unix URL; config pointing a remote path at a non-remote scheme.

Understand the failure class

Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.

Related errors


AI-assisted analysis of sxyazi/yazi@8ebf930f17 (2026-09-09). Data as JSON: /api/errors/1b1882d027577058. Report an issue: GitHub.