{"record":{"id":"a2c10d04f39cf281","repo":"can1357/oh-my-pi","slug":"invalid-scheme-url-empty-or-unsafe-path-seg","errorCode":null,"errorMessage":"Invalid ${scheme}:// URL: empty or unsafe path segment","messagePattern":"Invalid (.+?):// URL: empty or unsafe path segment","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/internal-urls/issue-pr-protocol.ts","lineNumber":121,"sourceCode":"\t\tauthor: url.searchParams.get(\"author\") ?? undefined,\n\t\tlabel: url.searchParams.get(\"label\") ?? undefined,\n\t};\n}\n\nfunction parseUrl(url: InternalUrl, scheme: Scheme): Parsed {\n\tlet host = url.rawHost || url.hostname;\n\tconst rawPath = url.rawPathname ?? url.pathname;\n\t// Strip a single leading slash so we can detect empty internal segments\n\t// (e.g. `pr://owner//77` → pathname `//77` → stripped `/77` → [\"\", \"77\"]).\n\tconst stripped = rawPath.startsWith(\"/\") ? rawPath.slice(1) : rawPath;\n\tlet parts: string[] = [];\n\tif (stripped !== \"\") {\n\t\tfor (const seg of stripped.split(\"/\")) {\n\t\t\tlet decoded: string;\n\t\t\ttry {\n\t\t\t\tdecoded = decodeURIComponent(seg);\n\t\t\t} catch {\n\t\t\t\tthrow new Error(`Invalid ${scheme}:// URL: empty or unsafe path segment`);\n\t\t\t}\n\t\t\tif (decoded === \"\" || decoded === \".\" || decoded === \"..\") {\n\t\t\t\tthrow new Error(`Invalid ${scheme}:// URL: empty or unsafe path segment`);\n\t\t\t}\n\t\t\tparts.push(seg);\n\t\t}\n\t}\n\n\t// Detect a leading `<host>/` prefix. A dotted first segment can only be a\n\t// host, because GitHub owner names are alphanumeric-plus-hyphen, so dotted\n\t// hosts work with every shape below. A single-label host (`ghe`,\n\t// `localhost`) is only recognizable from the item number's position, so it\n\t// is accepted in the numbered form alone — `<host>/<owner>/<repo>` with no\n\t// number is indistinguishable from `<owner>/<repo>/<bad-number>`, and\n\t// keeping the latter's error beats guessing.\n\tlet repoHost: string | undefined;\n\tconst dottedHost = host.includes(\".\");\n\tif (dottedHost && parts.length < 2) {","sourceCodeStart":103,"sourceCodeEnd":139,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/internal-urls/issue-pr-protocol.ts#L103-L139","documentation":"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.","triggerScenarios":"Calling parseUrl with URLs like pr://org/repo%/3, issue://org//123, issue://org/../123, or issue://org/./repo.","commonSituations":"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.","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"],"exampleFix":"// before\nresolve(`issue://${org}/${repo}/comments`)\n// after\nresolve(`issue://${encodeURIComponent(org)}/${encodeURIComponent(repo)}/comments`)","handlingStrategy":"validation","validationCode":"const segs = [org, repo, num].filter(s => s !== '' && s !== '.' && s !== '..')\nif (segs.length < 2) throw new Error('need non-empty org/repo segments')\nconst url = `issue://${segs.map(encodeURIComponent).join('/')}`","typeGuard":"const isSafeSegment = (s: string): boolean => {\n  let d: string\n  try { d = decodeURIComponent(s) } catch { return false }\n  return d !== '' && d !== '.' && d !== '..'\n}","tryCatchPattern":"try {\n  return parseUrl(input)\n} catch (err) {\n  if (String(err).includes('empty or unsafe path segment')) {\n    throw new Error(`malformed internal URL ${input}; encode segments and remove empty/'.'/'..' parts`)\n  }\n  throw err\n}","preventionTips":["encodeURIComponent every dynamic path segment","Never interpolate raw user/repo names containing '%' or '/'","Validate interpolated values are non-empty before building URLs"],"tags":["validation","url","path-traversal"],"backgroundTag":"invalid-url-path","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}