{"record":{"id":"d4c69f103cc53731","repo":"can1357/oh-my-pi","slug":"ssh-invalid-host-or-port-in-url-href-use","errorCode":null,"errorMessage":"ssh://: invalid host or port in \"${url.href}\"; use ssh://host[:1-65535]/<absolute-path>","messagePattern":"ssh://: invalid host or port in \"(.+?)\"; use ssh://host\\[:1-65535\\]/<absolute-path>","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/internal-urls/ssh-protocol.ts","lineNumber":144,"sourceCode":"}\n\n/**\n * Resolve the URL authority to an SSH connection target. With no explicit\n * user/port, the full DECODED authority (`url.rawHost`) is matched against a\n * configured host name, so percent-encoded reserved-char aliases (e.g.\n * `alice%40prod` → `alice@prod`) resolve correctly. A literal `user@`/`:port`\n * in the URL is an override: it is rejected on a configured bare name (the\n * ControlMaster/host-info caches key on `name` alone) and otherwise treated as\n * an opaque OpenSSH destination so plain `~/.ssh/config` aliases work.\n */\nasync function resolveTarget(url: InternalUrl, cwd?: string): Promise<SSHConnectionTarget> {\n\t// `parseInternalUrl` falls back to a lenient regex parse when WHATWG `new URL`\n\t// rejects the input. For ssh:// that only happens on a malformed authority — an\n\t// invalid or out-of-range port (`prod:abc`, `host:65536`) or a bad IPv6 literal —\n\t// which would otherwise be mis-read as an opaque host and silently connect to the\n\t// default port. Reject it before resolving.\n\tif (!URL.canParse(url.href)) {\n\t\tthrow new Error(`ssh://: invalid host or port in \"${url.href}\"; use ssh://host[:1-65535]/<absolute-path>`);\n\t}\n\t// WHATWG `hostname` is bracketed only for a *valid* IPv6 literal, so a bracketed\n\t// host is unambiguously IPv6 — hand OpenSSH the bare address. Percent-encoded\n\t// bracketed aliases (e.g. `%5Bprod%3A2222%5D`) keep their literal brackets in the\n\t// decoded `rawHost`, so they are matched and forwarded verbatim, never stripped.\n\tconst bareHost = url.hostname;\n\tconst rawAuthority = url.rawHost || bareHost;\n\tif (!bareHost && !rawAuthority) {\n\t\tthrow new Error(\"ssh:// requires a host: ssh://<host>/<absolute-path>\");\n\t}\n\t// `decodeOr` fails open, so a malformed percent-escape (`%ZZ`) in the authority\n\t// would otherwise pass the canonical check below and reach OpenSSH literally.\n\t// Reject it up front — the path decoder fails closed for the same bad escapes.\n\tfor (const part of [url.username, bareHost]) {\n\t\tif (part.includes(\"%\")) {\n\t\t\ttry {\n\t\t\t\tdecodeURIComponent(part);\n\t\t\t} catch {","sourceCodeStart":126,"sourceCodeEnd":162,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/internal-urls/ssh-protocol.ts#L126-L162","documentation":"When WHATWG `new URL` cannot parse the URL, the internal-URL parser falls back to a lenient regex parse. For ssh:// that only happens with a malformed authority — an invalid or out-of-range port (`prod:abc`, `host:65536`) or a bad IPv6 literal — which would otherwise be mis-read as an opaque host and silently connect on the default port. `resolveTarget` guards with `URL.canParse(url.href)` and rejects such URLs up front.","triggerScenarios":"`SshProtocolHandler.resolve()`/`.write()` (via `resolveTarget`) with a `ssh://` URL whose authority WHATWG URL parsing rejects: non-numeric port (`ssh://prod:abc/path`), port out of range (`ssh://host:65536/path`), or a malformed IPv6 literal such as an unclosed bracket.","commonSituations":"Typos in ports; pasting a `host:port` pair where the port column got corrupted; hand-built URLs like `ssh://host:${portVar}/path` where the variable is empty or non-numeric; IPv6 addresses written without full bracket syntax.","solutions":["Use a numeric port in 1–65535: `ssh://host:2222/path`, or omit the port entirely.","Check the port variable/expression — log or print the final URL before resolving.","Use correct bracketed IPv6: `ssh://[2001:db8::1]/path`.","If the host name contains a colon as part of an alias (e.g. `alice@prod`), percent-encode reserved chars: `ssh://alice%40prod/path`."],"exampleFix":"// before\nawait readResource(\"ssh://prod:abc/etc/hosts\");\n// after\nawait readResource(\"ssh://prod:22/etc/hosts\");","handlingStrategy":"validation","validationCode":"function assertParsableSshUrl(url: string): void {\n  if (url.startsWith(\"ssh://\") && !URL.canParse(url)) {\n    throw new Error(`Malformed ssh:// authority (check host:port): ${url}`);\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  return await sshHandler.resolve(url);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"invalid host or port\")) {\n    // sanitize: strip bad port, retry with default\n    const fixed = url.replace(/:(\\D|6[5-9]\\d{3})/, \"\");\n    return sshHandler.resolve(parseInternalUrl(fixed));\n  }\n  throw err;\n}","preventionTips":["Validate ports are integers in 1–65535 before interpolating into URLs.","Use bracketed IPv6 literals: ssh://[2001:db8::1]/path.","Print/inspect the final URL when built from variables.","Prefer configured host aliases over hand-built host:port strings."],"tags":["url","ssh-protocol","validation","port"],"backgroundTag":"invalid-url-syntax","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}