sinelaw/fresh · error

--ssh-url is not a valid ssh:// URL

Error message

--ssh-url is not a valid ssh:// URL ({}): {:?}

What it means

After the ssh:// prefix check passes, parse_ssh_url_arg delegates to parse_ssh_url_rest to split user@host:port/path into a RemoteLocation. If that parse fails (no host, bad port, empty path where a path is required), the reason is wrapped into this error. The doc notes a missing path is fatal because the URL comes from spawn_server_detached and malformed input means internal corruption.

Solutions

  1. Read the wrapped reason in the message and correct the URL: it must be ssh://[user@]host[:port]/path
  2. Ensure the path component is present — the caller requires it (ssh://host alone fails)
  3. If produced internally, inspect spawn_server_detached's URL building for dropped or truncated components
  4. Test parse_ssh_url_arg directly with the exact string to isolate which part fails

Example fix

// before
fresh --ssh-url ssh://deploy@build-box
// after
fresh --ssh-url ssh://deploy@build-box/home/deploy/project
Defensive patterns

Strategy: validation

Validate before calling

fn looks_like_complete_ssh_url(url: &str) -> bool {
    let rest = url.strip_prefix("ssh://").unwrap_or("");
    let after_authority = rest.split('/').next().unwrap_or("");
    !after_authority.split('@').last().unwrap_or("").is_empty()
        && url.contains('/')
}

Try / catch

match parse_ssh_url_arg(url) {
    Err(e) if e.to_string().starts_with("--ssh-url is not a valid") => {
        eprintln!("{e:#}; expected ssh://[user@]host[:port]/path");
        std::process::exit(2);
    }
    other => other?,
}

Prevention

When it happens

Trigger: --ssh-url carries an ssh:// prefix but the remainder is unparseable: missing host (ssh:///path), invalid port (ssh://host:notaport/), or missing path when the caller requires one (ssh://host only).

Common situations: spawn_server_detached generating a URL with the path stripped; hand-testing the binary with ssh://host and no path; IPv6 hosts or usernames with characters the parser rejects.

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 sinelaw/fresh@67894ca546 (2026-09-13). Data as JSON: /api/errors/36096df094c12aa9. Report an issue: GitHub.

Appendix: source

Thrown at crates/fresh-editor/src/main.rs:1458

        anyhow::bail!(
            "Cannot mix local and remote files. Use either local paths or remote paths (ssh:// or user@host:path)."
        );
    }

    Ok(Some(remote_location_to_ssh_url(first)))
}

/// Parse a standalone `ssh://…` URL passed via the internal
/// `--ssh-url` flag.  Accepts only the URL form (not scp-style) and
/// the URL must carry a path; anything else is a hard error because
/// this input comes from our own `spawn_server_detached` and a
/// malformed URL there means we corrupted it on the way over.
fn parse_ssh_url_arg(url: &str) -> AnyhowResult<RemoteLocation> {
    let rest = url
        .strip_prefix("ssh://")
        .ok_or_else(|| anyhow::anyhow!("--ssh-url expects an ssh:// URL, got {:?}", url))?;
    parse_ssh_url_rest(rest, default_ssh_user().as_deref()).map_err(|reason| {
        anyhow::anyhow!(
            "--ssh-url is not a valid ssh:// URL ({}): {:?}",
            reason,
            url
        )
    })
}

/// Parse a location that may be local, scp-style remote, or an
/// `ssh://` URL.
///
/// Accepted forms:
/// - local: `file`, `file:line`, `file:line:col`
/// - scp-style remote: `user@host:path[:line[:col]]`
/// - URL-style remote: `ssh://[user@]host[:port]/path[:line[:col]]`
///
/// When `ssh://` omits the user, the current login name (`$USER` /
/// `$USERNAME`) is used.  The URL form is the only way to pass a
/// port.  The path must be non-empty in both remote forms.

View on GitHub (pinned to 67894ca546)