can1357/oh-my-pi · error · ToolError
Invalid URL encoding in ${scheme}:// path: ${rawPath}
Error message
Invalid URL encoding in ${scheme}:// path: ${rawPath} What it means
When resolving an internal-URL path (e.g. `local://a%20b.md`) under its registered root, `resolveUnderRoot` decodes percent-escapes via decodeURIComponent. Malformed escapes (a bare `%` or truncated `%zz` sequence) make decoding throw, and the resolver surfaces this ToolError instead of a raw URIError.
Source
Thrown at packages/coding-agent/src/eval/js/shared/helpers.ts:128
function resolveHelperPath(ctx: HelperContext, rawPath: string, op: "read" | "write"): string {
const match = INTERNAL_URL_RE.exec(rawPath);
if (!match) return resolvePath(ctx, rawPath);
const scheme = match[1].toLowerCase();
const root = ctx.localRoots()[scheme];
if (!root) {
throw new ToolError(`Protocol paths are not supported by ${op}(): ${rawPath}`);
}
return resolveUnderRoot(scheme, root, match[2], rawPath);
}
/** Resolve an internal-URL relative path under its root, mirroring the host
* local-protocol handler: decode, reject absolute/traversal, confine to root. */
function resolveUnderRoot(scheme: string, root: string, rawRelative: string, rawPath: string): string {
let relative: string;
try {
relative = decodeURIComponent(rawRelative.replaceAll("\\", "/"));
} catch {
throw new ToolError(`Invalid URL encoding in ${scheme}:// path: ${rawPath}`);
}
const rootPath = path.resolve(root);
if (relative === "") return rootPath;
if (path.isAbsolute(relative)) {
throw new ToolError(`Absolute paths are not allowed in ${scheme}:// URLs: ${rawPath}`);
}
const normalized = path.normalize(relative);
if (normalized.startsWith("..") || normalized.includes("/../") || normalized.includes("/..")) {
throw new ToolError(`Path traversal (..) is not allowed in ${scheme}:// URLs: ${rawPath}`);
}
const resolved = path.resolve(rootPath, normalized);
if (resolved !== rootPath && !resolved.startsWith(`${rootPath}${path.sep}`)) {
throw new ToolError(`${scheme}:// path escapes its root: ${rawPath}`);
}
return resolved;
}
async function resolveRegularFile(View on GitHub (pinned to 9690622007)
Solutions
- Percent-encode the path segment: `write(`local://${encodeURIComponent(name)}`, data)`.
- Remove or escape stray `%` characters in the path.
- If building URLs from file names, encodeURIComponent each segment rather than the whole string with unsafe characters.
Example fix
// before
await write(`local://${name}.md`, data); // name = "100% done"
// after
await write(`local://${encodeURIComponent(name)}.md`, data); Defensive patterns
Strategy: validation
Validate before calling
function safeSegment(name: string): string {
const enc = encodeURIComponent(name);
decodeURIComponent(enc); // throws on impossible cases; encodeURIComponent output is always valid
return enc;
} Try / catch
try {
return await read(url);
} catch (err) {
if (String(err?.message).includes("Invalid URL encoding")) {
const [scheme, rest] = url.split("://");
return await read(`${scheme}://${encodeURIComponent(decodeURI(rest))}`);
}
throw err;
} Prevention
- encodeURIComponent path segments interpolated into scheme:// URLs.
- Never interpolate raw filenames containing `%` unescaped.
- Encode per segment, not the whole URL (keep `://` intact).
When it happens
Trigger: Calling read()/write() with an internal-URL path containing invalid percent-encoding, such as `local://100%.md` or `local://a%2.md`.
Common situations: Interpolating raw user/filename content into a scheme:// URL without encodeURIComponent; copy-pasting URLs that were truncated; template strings containing literal `%` characters.
Related errors
- Protocol paths are not supported by ${op}(): ${rawPath}
- write() expects string, Blob, ArrayBuffer, or TypedArray dat
- Absolute paths are not allowed in ${scheme}:// URLs: ${rawPa
- Path traversal (..) is not allowed in ${scheme}:// URLs: ${r
- Directory paths are not supported by read(): ${filePath}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/9ba1596d0bd120df.
Report an issue: GitHub.