can1357/oh-my-pi · error

Invalid xd:// URL: ${url.href}. Use xd:// or xd://<tool>.

Error message

Invalid xd:// URL: ${url.href}. Use xd:// or xd://<tool>.

What it means

XdProtocolHandler.resolve() validates the incoming URL with parseXdUrl(), which accepts only "xd://" (root) or "xd://<tool>" with no path separators, query, or fragment characters. Anything else (wrong scheme, subpaths like xd://a/b, trailing /?# characters) makes the parser return null and the handler throws this error. It is pure URL-shape validation before any device access.

Source

Thrown at packages/coding-agent/src/internal-urls/xd-protocol.ts:34

	return { name };
}

/** Whether a streaming path prefix could still become an `xd://` URL. */
export function couldBecomeXdUrl(partialPath: string): boolean {
	if (partialPath.length <= XD_URL_PREFIX.length) {
		return XD_URL_PREFIX.startsWith(partialPath.toLowerCase());
	}
	return partialPath.toLowerCase().startsWith(XD_URL_PREFIX);
}

/** Routes session-bound virtual tool devices through `xd://` URLs. */
export class XdProtocolHandler implements ProtocolHandler {
	readonly scheme = "xd";
	readonly immutable = true;

	async resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
		const target = parseXdUrl(url.href);
		if (!target) throw new Error(`Invalid xd:// URL: ${url.href}. Use xd:// or xd://<tool>.`);
		if (!context?.xd) throw new Error("xd:// is not mounted in this session.");
		const content = await context.xd.read(target.name);
		return { url: url.href, content, contentType: "text/plain", size: Buffer.byteLength(content) };
	}

	async write(url: InternalUrl, content: string, context?: WriteContext): Promise<void> {
		const target = parseXdUrl(url.href);
		if (!target) throw new Error(`Invalid xd:// URL: ${url.href}. Use xd://<tool>.`);
		if (!context?.xd) throw new Error("xd:// is not mounted in this session.");
		await context.xd.write(target.name, content);
	}
}

View on GitHub (pinned to 9690622007)

Solutions

  1. Use "xd://" for the device root or "xd://<tool>" for a single tool device — no subpaths, query, or fragments.
  2. Pre-validate with parseXdUrl(url.href) and handle null before calling resolve.
  3. If you need hierarchical addressing, encode it in the tool name or use a different protocol.

Example fix

// before
await resolveInternalUrl("xd://browser/tab/1"); // path -> throws
// after
await resolveInternalUrl("xd://browser"); // valid device URL
Defensive patterns

Strategy: validation

Validate before calling

import { parseXdUrl } from "./internal-urls/xd-protocol";
if (!parseXdUrl(url.href)) {
  throw new Error(`Not a valid xd:// device URL: ${url.href}. Use xd:// or xd://<tool>.`);
}

Type guard

function isXdDeviceUrl(href: string): boolean {
  return /^xd:\/\/[^/?#]*$/.test(href.trim());
}

Try / catch

try {
  return await resolveInternalUrl(url);
} catch (err) {
  if (err instanceof Error && err.message.startsWith("Invalid xd:// URL:")) {
    // normalize: strip subpath/query, or route to the correct protocol handler
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: Resolving an InternalUrl whose href is not exactly xd:// or xd://<name> — e.g. "xd://browser/tab/1" (contains "/"), "xd://tool?x=1" (contains "?"), or a non-xd URL routed to this handler.

Common situations: Appending paths or query strings to xd:// URLs as if they were HTTP URLs; agents constructing hierarchical device addresses; routing a foreign URL into the xd handler.

Understand the failure class

Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.

Related errors


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