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

  1. Percent-encode segments with encodeURIComponent before building the URL
  2. Remove empty segments (collapse '//') and avoid '.' or '..' in paths
  3. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/a2c10d04f39cf281. Report an issue: GitHub.