sinelaw/fresh · error
invalid ssh:// URL : . To open a local file with this name…
Error message
invalid ssh:// URL {:?}: {}. To open a local file with this name, prefix it with ./ What it means
parse_location accepts user input for a location to open. When the input starts with ssh:// but the rest fails to parse, it converts the parse reason into this error and helpfully suggests that if the user actually meant a local file literally named like a URL, they should prefix the path with ./ to force local interpretation.
Solutions
- Fix the URL syntax: ssh://[user@]host[:port]/path with a valid host and port
- If you meant a local file that literally starts with ssh://, open it as ./ssh://... per the error hint
- Drop scp-style colon paths; use the slash-separated ssh URL form instead
- Verify user/host characters are URL-safe (escape or quote special characters)
Example fix
// before fresh ssh://server:22:~/project // after fresh ssh://user@server:22/home/user/project
Defensive patterns
Strategy: validation
Validate before calling
fn parse_or_local(input: &str) -> Result<ParsedLocation, String> {
if input.starts_with("ssh://") && !input.contains("@") && !input.contains(":") {
return Err(format!("invalid ssh URL '{input}'; use ./{} for a local file", input));
}
Ok(ParsedLocation::Local(input.into()))
} Try / catch
match parse_location(input, default_user) {
Err(e) if e.to_string().contains("invalid ssh:// URL") => {
eprintln!("{e:#}"); // hint already suggests ./ prefix for local files
}
other => other?,
} Prevention
- Use full, well-formed ssh URLs: ssh://[user@]host[:port]/path, no scp-style colons
- Prefix local files whose names resemble URLs with ./ to force local parsing
- Avoid trailing colons (ssh://host:) — they produce empty ports and fail parsing
- Quote inputs with special characters so the shell doesn't split the URL
When it happens
Trigger: A user passes an argument like ssh://bad host form on the command line or in the open prompt: missing host, bad port, or malformed user@host — anything parse_ssh_url_rest rejects.
Common situations: Typing ssh://host: (trailing colon, empty port); copying an scp-style shorthand (ssh://host:path with colon-separated path) the parser doesn't accept; intentionally opening a file named 'ssh://...' in the working directory without ./ prefix.
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
- --ssh-url is not a valid ssh:// URL
- Cannot open files from multiple remote hosts. First
- Cannot mix local and remote files. Use either local paths…
- --ssh-url expects an ssh:// URL, got
- Too many '+ ' arguments (at most one is allowed)
AI-assisted analysis of sinelaw/fresh@67894ca546 (2026-09-13).
Data as JSON: /api/errors/a4a5921100a09a98.
Report an issue: GitHub.
Appendix: source
Thrown at crates/fresh-editor/src/main.rs:1497
/// argument is a hard error, never a local-path fallback. Silently
/// opening a local buffer named `ssh://host/...` hid the failure and
/// invited saving to a bogus local path (#2221). A genuine local
/// file whose name starts with `ssh://` can still be opened as
/// `./ssh://...`.
fn parse_location(input: &str) -> AnyhowResult<ParsedLocation> {
parse_location_with_default_user(input, default_ssh_user().as_deref())
}
/// `parse_location` with the `$USER`/`$USERNAME` fallback injected,
/// so tests can pin it without mutating the process environment.
fn parse_location_with_default_user(
input: &str,
default_user: Option<&str>,
) -> AnyhowResult<ParsedLocation> {
if let Some(rest) = input.strip_prefix("ssh://") {
return match parse_ssh_url_rest(rest, default_user) {
Ok(loc) => Ok(ParsedLocation::Remote(loc)),
Err(reason) => Err(anyhow::anyhow!(
"invalid ssh:// URL {:?}: {}. \
To open a local file with this name, prefix it with ./",
input,
reason
)),
};
}
// scp-style: user@host:path. Must have @ before the first : to
// count as remote (skips Windows drive letters like `C:\...`).
if let Some(at_pos) = input.find('@') {
let user = &input[..at_pos];
let after_at = &input[at_pos + 1..];
if let Some(colon_pos) = after_at.find(':') {
let host = &after_at[..colon_pos];
let path_and_rest = &after_at[colon_pos + 1..];
View on GitHub (pinned to 67894ca546)