{"record":{"id":"fbb61a04264e9b30","repo":"can1357/oh-my-pi","slug":"ssh-unsupported-or-malformed-authority-in-u","errorCode":null,"errorMessage":"ssh://: unsupported or malformed authority in \"${url.href}\"; use ssh://[user@]host[:1-65535]/<absolute-path>","messagePattern":"ssh://: unsupported or malformed authority in \"(.+?)\"; use ssh://\\[user@\\]host\\[:1-65535\\]/<absolute-path>","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/internal-urls/ssh-protocol.ts","lineNumber":212,"sourceCode":"\tif (port === undefined && url.rawHost === `${username ? `${decodeOr(username)}@` : \"\"}${decodeOr(bareHost)}:`) {\n\t\tthrow new Error(`ssh://: empty port in \"${url.href}\"; use ssh://host:<1-65535>/<path> or drop the colon`);\n\t}\n\t// A literal but empty userinfo (`ssh://@host`) sets username to \"\" — WHATWG drops\n\t// the `@` from hostname, but rawHost keeps the leading `@`. A percent-encoded\n\t// alias like `%40prod` decodes to `@prod` in rawHost too, but its hostname keeps\n\t// `%40`, so the reconstruction is `@@prod` and is left alone.\n\tif (username === undefined && url.rawHost === `@${decodeOr(bareHost)}${port !== undefined ? `:${port}` : \"\"}`) {\n\t\tthrow new Error(`ssh://: empty username in \"${url.href}\"; drop the leading '@' or provide a username before it`);\n\t}\n\t// Backstop for any remaining stray/empty authority marker the explicit checks\n\t// above do not name — notably an empty password (`ssh://user:@host`, `ssh://:@host`,\n\t// where `url.password === \"\"`). `rawHost` keeps the literal marker, so it differs\n\t// from the canonical decoded `[user@]host[:port]` WHATWG actually parsed. Every\n\t// valid authority — including percent-encoded reserved-char aliases — reconstructs\n\t// to exactly `rawHost`, so only malformed userinfo trips this.\n\tconst canonicalAuthority = `${url.username ? `${decodeOr(url.username)}@` : \"\"}${decodeOr(bareHost)}${port !== undefined ? `:${port}` : \"\"}`;\n\tif (url.rawHost !== canonicalAuthority) {\n\t\tthrow new Error(\n\t\t\t`ssh://: unsupported or malformed authority in \"${url.href}\"; use ssh://[user@]host[:1-65535]/<absolute-path>`,\n\t\t);\n\t}\n\tconst items = await loadConfiguredHosts(cwd);\n\n\t// A literal user/port in the URL is an authority override. A configured alias\n\t// is addressed only by its (percent-encoded) name, never with a separate\n\t// user/port — so reject an override on a configured bare name, else opaque.\n\tif (username || port !== undefined) {\n\t\tconst decodedBareHost = decodeOr(bareHost);\n\t\tif (items.some(entry => entry.name === bareHost || entry.name === decodedBareHost)) {\n\t\t\tthrow new Error(\n\t\t\t\t`ssh://: user/port overrides are not allowed for the configured host \"${decodedBareHost}\"; use ssh://${bareHost}/<path> or an unconfigured hostname`,\n\t\t\t);\n\t\t}\n\t\tconst sshUser = username ? decodeOr(username) : undefined;\n\t\tconst sshTargetHost = decodeOr(sshHost);\n\t\tconst name = `${sshUser ? `${sshUser}@` : \"\"}${sshTargetHost}${port !== undefined ? `:${port}` : \"\"}`;","sourceCodeStart":194,"sourceCodeEnd":230,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/internal-urls/ssh-protocol.ts#L194-L230","documentation":"Generic backstop for any ssh:// authority whose decoded literal text does not reconstruct to the canonical `[user@]host[:port]` that WHATWG actually parsed — most notably an empty password (`ssh://user:@host`, `ssh://:@host`). Every valid authority, including percent-encoded aliases, reconstructs exactly, so only genuinely malformed userinfo trips this.","triggerScenarios":"Resolving URLs like `ssh://user:@host/path` or `ssh://:@host/path` (empty `url.password`), or any other stray/empty authority marker where `url.rawHost !== canonicalAuthority` after the explicit empty-port/empty-username checks.","commonSituations":"Deleting a password but leaving the colon; templates emitting `ssh://${user}:${pass}@${host}` with an empty pass; hand-edited URLs with leftover separators.","solutions":["Fix the authority to the shape ssh://[user@]host[:port]/path","Remove the leftover `:`/`@` markers: `ssh://user@host/path`","Percent-encode any reserved characters actually intended in user/host (e.g. `%40` for `@`)"],"exampleFix":"// before\nresolve('ssh://user:@prod.example.com/etc/hosts')\n// after\nresolve('ssh://user@prod.example.com/etc/hosts')","handlingStrategy":"validation","validationCode":"const u = new URL(candidate);\nconst decode = (s: string) => { try { return decodeURIComponent(s); } catch { return s; } };\nconst host = u.hostname, port = u.port ? Number(u.port) : undefined;\nconst canonical = `${u.username ? `${decode(u.username)}@` : ''}${decode(host)}${port !== undefined ? `:${port}` : ''}`;\nif (u.protocol === 'ssh:' && (u.rawHost || host) !== canonical) throw new Error(`malformed authority in ${candidate}`);","typeGuard":"function hasCanonicalSshAuthority(u: URL): boolean { return (u.rawHost ?? u.hostname) === canonicalSshAuthority(u); }","tryCatchPattern":"try {\n  const res = await handler.resolve(url, ctx);\n} catch (e) {\n  if (e instanceof Error && e.message.includes('unsupported or malformed authority')) {\n    // rebuild the authority as [user@]host[:port], then retry\n  } else throw e;\n}","preventionTips":["Build ssh URLs from structured parts (user, host, port) instead of string editing","Percent-encode reserved characters in usernames/hosts deliberately (e.g. %40 for @)","Validate the URL with new URL/URL.canParse before handing it to the handler"],"tags":["ssh","url-parsing","malformed-url"],"backgroundTag":"malformed-url-authority","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}