{"record":{"id":"7346c66321ced44c","repo":"can1357/oh-my-pi","slug":"invalid-url-encoding-in-vault-path-url-href","errorCode":null,"errorMessage":"Invalid URL encoding in vault:// path: ${url.href}","messagePattern":"Invalid URL encoding in vault:// path: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/internal-urls/vault-protocol.ts","lineNumber":159,"sourceCode":"\nfunction decodeVaultPath(url: InternalUrl): {\n\trawPathname: string;\n\trelativePath: string;\n\thasPath: boolean;\n\tisDirectory: boolean;\n} {\n\tconst rawPathname = url.rawPathname ?? url.pathname;\n\tconst hasPath = rawPathname !== undefined && rawPathname !== \"\" && rawPathname !== \"/\";\n\tconst isDirectory = rawPathname === \"/\" || rawPathname.endsWith(\"/\");\n\tif (!hasPath) {\n\t\treturn { rawPathname, relativePath: \"\", hasPath: false, isDirectory };\n\t}\n\n\tlet decoded: string;\n\ttry {\n\t\tdecoded = decodeURIComponent(rawPathname.slice(1).replaceAll(\"\\\\\", \"/\"));\n\t} catch {\n\t\tthrow new Error(`Invalid URL encoding in vault:// path: ${url.href}`);\n\t}\n\n\ttry {\n\t\tvalidateRelativePath(decoded);\n\t} catch (error) {\n\t\tthrow toVaultValidationError(error);\n\t}\n\n\treturn { rawPathname, relativePath: decoded.replace(/\\/+$/, \"\"), hasPath: true, isDirectory };\n}\n\nfunction paramsFromUrl(url: InternalUrl): VaultParams {\n\tconst params: VaultParams = {};\n\tfor (const [key, value] of url.searchParams) {\n\t\tparams[key] = value === \"\" ? true : value;\n\t}\n\treturn params;\n}","sourceCodeStart":141,"sourceCodeEnd":177,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/internal-urls/vault-protocol.ts#L141-L177","documentation":"`decodeVaultPath` decodes the vault:// URL pathname with `decodeURIComponent` after normalizing backslashes to slashes. If the percent-encoding is malformed (e.g. a stray `%` not followed by two hex digits, or a truncated escape), `decodeURIComponent` throws a URIError which is rethrown as this descriptive error including the full URL href. This happens before relative-path validation, so it is purely about URL syntax.","triggerScenarios":"parseVaultUrl -> decodeVaultPath is called with a URL like `vault://_/notes/100%.md`, `vault://_/a%2b%zz.txt`, or a truncated copy-pasted URL ending in `%e`. Any `%` that is not part of a valid `%XX` escape triggers it.","commonSituations":"Pasting Obsidian URIs or file names containing literal `%` characters unencoded into a vault:// URL; hand-building URLs with string concatenation instead of encodeURIComponent; truncating a URL mid-escape when editing it.","solutions":["Percent-encode all `%` characters in the path as `%25` (use encodeURIComponent per path segment).","Regenerate the URL instead of hand-editing it; use the encodeRelativePath-style scheme (encodeURIComponent each segment joined by `/`).","Rename the file so its name contains no raw `%` characters, then use the encoded form."],"exampleFix":"// before\nconst url = `vault://_/notes/${name}.md`; // name = \"100% done\"\n// after\nconst url = `vault://_/notes/${encodeURIComponent(name)}.md`; // \"100%25 done\"","handlingStrategy":"validation","validationCode":"function isWellFormedPercentEncoding(s: string): boolean {\n  // every '%' must be followed by two hex digits\n  return !/(^|[^%])%([^0-9A-Fa-f]{2}|$)|%$/.test(s) || !/%(?![0-9A-Fa-f]{2})/.test(s);\n}\n// equivalently: decodeURIComponent(test) inside try/catch before building the URL","typeGuard":"function hasValidEncoding(rawPathname: string): boolean {\n  try { decodeURIComponent(rawPathname.replace(/\\\\/g, \"/\").slice(1)); return true; }\n  catch { return false; }\n}","tryCatchPattern":"try {\n  const res = await handler.resolve(parseInternalUrl(url));\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith(\"Invalid URL encoding in vault://\")) {\n    url = reEncodeUrl(url); // encodeURIComponent each path segment\n  } else throw err;\n}","preventionTips":["Always encodeURIComponent each path segment; never interpolate raw file names","Escape literal `%` as `%25` when hand-writing URLs","Avoid copy-paste truncation of URLs — verify no dangling `%` at the end","Round-trip test: decodeURIComponent(encode(x)) === x before using a generated URL"],"tags":["url","encoding","percent-encoding","parsing"],"backgroundTag":"malformed-percent-encoding","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}