can1357/oh-my-pi · error
Invalid ${scheme}:// URL: empty or unsafe path segment
Error message
Invalid ${scheme}:// URL: empty or unsafe path segment What it means
parseUrl decodes each path segment of an issue:// or pr:// URL with decodeURIComponent and validates the result. It throws when decoding fails (bad percent-encoding) or when the decoded segment is empty, '.', or '..' — path-traversal/empty-segment protection. The same message covers both branches.
Source
Thrown at packages/coding-agent/src/internal-urls/issue-pr-protocol.ts:121
author: url.searchParams.get("author") ?? undefined,
label: url.searchParams.get("label") ?? undefined,
};
}
function parseUrl(url: InternalUrl, scheme: Scheme): Parsed {
let host = url.rawHost || url.hostname;
const rawPath = url.rawPathname ?? url.pathname;
// Strip a single leading slash so we can detect empty internal segments
// (e.g. `pr://owner//77` → pathname `//77` → stripped `/77` → ["", "77"]).
const stripped = rawPath.startsWith("/") ? rawPath.slice(1) : rawPath;
let parts: string[] = [];
if (stripped !== "") {
for (const seg of stripped.split("/")) {
let decoded: string;
try {
decoded = decodeURIComponent(seg);
} catch {
throw new Error(`Invalid ${scheme}:// URL: empty or unsafe path segment`);
}
if (decoded === "" || decoded === "." || decoded === "..") {
throw new Error(`Invalid ${scheme}:// URL: empty or unsafe path segment`);
}
parts.push(seg);
}
}
// Detect a leading `<host>/` prefix. A dotted first segment can only be a
// host, because GitHub owner names are alphanumeric-plus-hyphen, so dotted
// hosts work with every shape below. A single-label host (`ghe`,
// `localhost`) is only recognizable from the item number's position, so it
// is accepted in the numbered form alone — `<host>/<owner>/<repo>` with no
// number is indistinguishable from `<owner>/<repo>/<bad-number>`, and
// keeping the latter's error beats guessing.
let repoHost: string | undefined;
const dottedHost = host.includes(".");
if (dottedHost && parts.length < 2) {View on GitHub (pinned to 9690622007)
Solutions
- Percent-encode segments with encodeURIComponent before building the URL
- Remove empty segments (collapse '//') and avoid '.' or '..' in paths
- Ensure interpolated variables are non-empty before constructing the URL
Example fix
// before
resolve(`issue://${org}/${repo}/comments`)
// after
resolve(`issue://${encodeURIComponent(org)}/${encodeURIComponent(repo)}/comments`) Defensive patterns
Strategy: validation
Validate before calling
const segs = [org, repo, num].filter(s => s !== '' && s !== '.' && s !== '..')
if (segs.length < 2) throw new Error('need non-empty org/repo segments')
const url = `issue://${segs.map(encodeURIComponent).join('/')}` Type guard
const isSafeSegment = (s: string): boolean => {
let d: string
try { d = decodeURIComponent(s) } catch { return false }
return d !== '' && d !== '.' && d !== '..'
} Try / catch
try {
return parseUrl(input)
} catch (err) {
if (String(err).includes('empty or unsafe path segment')) {
throw new Error(`malformed internal URL ${input}; encode segments and remove empty/'.'/'..' parts`)
}
throw err
} Prevention
- encodeURIComponent every dynamic path segment
- Never interpolate raw user/repo names containing '%' or '/'
- Validate interpolated values are non-empty before building URLs
When it happens
Trigger: Calling parseUrl with URLs like pr://org/repo%/3, issue://org//123, issue://org/../123, or issue://org/./repo.
Common situations: Hand-built URLs with raw '%' characters (unencoded '%' in repo or org names); joining path fragments naively producing '..' or empty segments; template strings with missing interpolation values.
Related errors
- Provider delete URL must not embed an account credential
- ${destination} returned an invalid upload URL
- Destination option endpoint must be an absolute URL
- Destination option endpoint must use HTTP or HTTPS
- Destination paths cannot contain parent traversal or NUL byt
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/a2c10d04f39cf281.
Report an issue: GitHub.