can1357/oh-my-pi · error
ssh://: empty port in "${url.href}"; use ssh://host:<1-65535
Error message
ssh://: empty port in "${url.href}"; use ssh://host:<1-65535>/<path> or drop the colon What it means
WHATWG URL parsing accepts `ssh://prod:/path` with `url.port === ""` — an empty port after a trailing colon. That would silently be treated as 'no port' and connect to the default/configured target, so the handler detects the dangling colon via `rawHost` and rejects it.
Source
Thrown at packages/coding-agent/src/internal-urls/ssh-protocol.ts:195
throw new Error("ssh://: port 0 is not a valid SSH port; use ssh://host:<1-65535>/<path> or omit the port");
}
// An empty port (`ssh://prod:/path`, `ssh://user@host:/path`, including
// percent-encoded authority parts) parses cleanly with `url.port === ""`, so it
// slips past the malformed-authority guard and would be read as "no port" —
// silently using the default/configured target. `url.rawHost` is the decoded
// authority and uniquely retains the trailing `:`; comparing it to the decoded
// host (+ user) catches the empty port, while a percent-encoded alias like
// `prod%3A` (whose decoded host already ends in `:`) reconstructs to `prod::`
// and is left alone.
const decodeOr = (s: string): string => {
try {
return decodeURIComponent(s);
} catch {
return s;
}
};
if (port === undefined && url.rawHost === `${username ? `${decodeOr(username)}@` : ""}${decodeOr(bareHost)}:`) {
throw new Error(`ssh://: empty port in "${url.href}"; use ssh://host:<1-65535>/<path> or drop the colon`);
}
// A literal but empty userinfo (`ssh://@host`) sets username to "" — WHATWG drops
// the `@` from hostname, but rawHost keeps the leading `@`. A percent-encoded
// alias like `%40prod` decodes to `@prod` in rawHost too, but its hostname keeps
// `%40`, so the reconstruction is `@@prod` and is left alone.
if (username === undefined && url.rawHost === `@${decodeOr(bareHost)}${port !== undefined ? `:${port}` : ""}`) {
throw new Error(`ssh://: empty username in "${url.href}"; drop the leading '@' or provide a username before it`);
}
// Backstop for any remaining stray/empty authority marker the explicit checks
// above do not name — notably an empty password (`ssh://user:@host`, `ssh://:@host`,
// where `url.password === ""`). `rawHost` keeps the literal marker, so it differs
// from the canonical decoded `[user@]host[:port]` WHATWG actually parsed. Every
// valid authority — including percent-encoded reserved-char aliases — reconstructs
// to exactly `rawHost`, so only malformed userinfo trips this.
const canonicalAuthority = `${url.username ? `${decodeOr(url.username)}@` : ""}${decodeOr(bareHost)}${port !== undefined ? `:${port}` : ""}`;
if (url.rawHost !== canonicalAuthority) {
throw new Error(
`ssh://: unsupported or malformed authority in "${url.href}"; use ssh://[user@]host[:1-65535]/<absolute-path>`,View on GitHub (pinned to 9690622007)
Solutions
- Drop the trailing colon: `ssh://prod/path`
- Supply an actual port: `ssh://prod:2222/path`
- Fix the template/config that left the port variable empty
Example fix
// before
resolve('ssh://prod:/etc/hosts')
// after
resolve('ssh://prod/etc/hosts') Defensive patterns
Strategy: validation
Validate before calling
const u = new URL(candidate);
if (u.protocol === 'ssh:' && u.port === '' && /:$/.test(u.rawHost ?? '')) throw new Error(`drop the trailing colon: ${candidate}`); Type guard
function hasNoEmptySshPort(u: URL): boolean { return !(u.port === '' && (u.rawHost ?? '').endsWith(':')); } Try / catch
try {
const res = await handler.resolve(url, ctx);
} catch (e) {
if (e instanceof Error && e.message.includes('empty port in')) {
// remove the stray ':' or supply a port, then retry
} else throw e;
} Prevention
- Don't convert scp-style `host:/path` strings verbatim to ssh:// URLs — drop the colon
- Guard template interpolation: emit `:${port}` only when port is non-empty
- Trim stray separators when hand-editing URLs
When it happens
Trigger: Resolving `ssh://prod:/path` or `ssh://user@host:/path`, including with percent-encoded authority parts, where `port === undefined` but `rawHost` still ends with a literal `:`.
Common situations: scp-style strings (`scp file prod:/tmp`) copy-pasted into an ssh:// URL; a template producing `ssh://${host}:${port}/` with an empty port variable; hand-edited URLs leaving a stray colon.
Related errors
- ssh://: empty username in "${url.href}"; drop the leading '@
- ssh://: unsupported or malformed authority in "${url.href}";
- ssh:// requires a host before the path: ssh://<host>${rawPat
- imageUrls exposure "ssh" requires imageUrls.publicBaseUrl
- imageUrls exposure "ssh" requires imageUrls.sshTarget
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/0e647bc4e0dfe05f.
Report an issue: GitHub.