{"record":{"id":"c262287da08e5657","repo":"can1357/oh-my-pi","slug":"invalid-url-encoding-in-local-path-url-href","errorCode":null,"errorMessage":"Invalid URL encoding in local:// path: ${url.href}","messagePattern":"Invalid URL encoding in local:// path: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/internal-urls/local-protocol.ts","lineNumber":232,"sourceCode":"\tconst pathname = url.rawPathname ?? url.pathname;\n\n\tconst combined = host\n\t\t? pathname && pathname !== \"/\"\n\t\t\t? `${host}${pathname}`\n\t\t\t: host\n\t\t: pathname && pathname !== \"/\"\n\t\t\t? pathname.slice(1)\n\t\t\t: \"\";\n\n\tif (!combined) {\n\t\treturn \"\";\n\t}\n\n\tlet decoded: string;\n\ttry {\n\t\tdecoded = decodeURIComponent(combined.replaceAll(\"\\\\\", \"/\"));\n\t} catch {\n\t\tthrow new Error(`Invalid URL encoding in local:// path: ${url.href}`);\n\t}\n\ttry {\n\t\tvalidateRelativePath(decoded);\n\t} catch (error) {\n\t\tthrow toLocalValidationError(error);\n\t}\n\treturn decoded;\n}\n\n/** Resolve the session-scoped local:// root, shortening long Windows artifact paths before writes hit MAX_PATH. */\nexport function resolveLocalRoot(options: LocalProtocolOptions, platform: NodeJS.Platform = process.platform): string {\n\tconst artifactsDir = options.getArtifactsDir?.();\n\tif (artifactsDir) {\n\t\tconst candidate = path.resolve(artifactsDir, \"local\");\n\t\tif (platform === \"win32\" && candidate.length >= WINDOWS_LOCAL_ROOT_MAX_CHARS) {\n\t\t\treturn shortLocalRoot(options);\n\t\t}\n\t\treturn candidate;","sourceCodeStart":214,"sourceCodeEnd":250,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/internal-urls/local-protocol.ts#L214-L250","documentation":"extractRelativePath percent-decodes the host+pathname of a local:// URL so the resulting path segments match on-disk names. If decodeURIComponent throws (the string contains a stray '%' not followed by two hex digits, or malformed UTF-8 sequences), the handler rethrows with this message including the URL href.","triggerScenarios":"Resolving local://foo%zz.txt or local://100% where the percent escape is invalid; concatenating a pre-encoded path with a raw '%' character; URLs built by naive string templating instead of encodeURIComponent.","commonSituations":"Filenames containing literal '%' (e.g. '50%_off.md') written into the URL unescaped; double-encoding bugs where a component was encoded twice; model-generated local:// URLs with malformed escapes.","solutions":["Percent-encode path segments when building the URL: `local://` + encodeURIComponent(segment) for each segment.","Encode a literal '%' as '%25' in the URL.","If the URL came from upstream code, fix the producer to use proper encoding rather than string concatenation."],"exampleFix":"// before\nconst url = `local://${name}`; // name = '50%_off.md'\n// after\nconst url = `local://${encodeURIComponent(name)}`; // 50%25_off.md","handlingStrategy":"validation","validationCode":"function buildLocalUrl(name) { return 'local://' + encodeURIComponent(name); }\n// validate an existing URL:\ntry { decodeURIComponent(url.replace(/^local:\\/\\//, '')); } catch { throw new Error(`Malformed percent-encoding in ${url}`); }","typeGuard":null,"tryCatchPattern":"try { resource = await handler.resolve(url, ctx); } catch (e) { if (String(e.message).startsWith('Invalid URL encoding')) { /* re-encode segments and retry once */ } else throw e; }","preventionTips":["Always encodeURIComponent() each path segment when constructing local:// URLs.","Encode literal '%' as '%25'.","Avoid double-encoding: encode once, at URL construction only."],"tags":["url-encoding","percent-encoding","validation"],"backgroundTag":"malformed-percent-encoding","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}