fix(web_fetch): the protocol gate never accepted a URL — the colon bug

URL.protocol carries a trailing colon ("https:", WHATWG); the colon-less
comparison (a direct port from the py harness, where urlparse().scheme
has none) rejected EVERY valid URL ever fetched — the tool never fetched
a page in its life. Owner-reported via OpenRouter logs: 8 valid URLs in,
8 'invalid URL' results out.

The new pin stubs globalThis.fetch so the happy path runs hermetically —
a valid https URL must REACH the fetch stage (and ftp:// still rejected).

dist rebuilt (desktop:dist) — bundle verified: "http:"!==t.protocol
This commit is contained in:
amosblomqvist
2026-10-07 22:08:29 +03:00
parent 34f498b128
commit d450689a8b
2 changed files with 34 additions and 1 deletions
+6 -1
View File
@@ -160,7 +160,12 @@ export function createWebTools(search: () => SearchProvider | null): Record<stri
execute: serverExecute("web_fetch", webFetchInput, async ({ url }): Promise<string> => { execute: serverExecute("web_fetch", webFetchInput, async ({ url }): Promise<string> => {
try { try {
const parsed = new URL(url); const parsed = new URL(url);
if (parsed.protocol !== "http" && parsed.protocol !== "https") throw new Error("Only http(s) URLs are supported"); // URL.protocol carries a TRAILING COLON ("https:", WHATWG) — the
// colon-less comparison born from the py-harness port (urlparse's
// scheme has NO colon) rejected every valid URL ever fetched
// (owner-reported via OpenRouter logs, 2026-10-07).
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
throw new Error("Only http(s) URLs are supported");
} catch { } catch {
return `web_fetch failed: invalid URL "${url}"`; return `web_fetch failed: invalid URL "${url}"`;
} }
+28
View File
@@ -80,6 +80,34 @@ describe("web_search on the injected port (web.ts)", () => {
const out = await web_fetch!.execute!("c1", { url: "not a url" }); const out = await web_fetch!.execute!("c1", { url: "not a url" });
assert.match(String(out.details), /web_fetch failed: invalid URL/); assert.match(String(out.details), /web_fetch failed: invalid URL/);
}); });
it("web_fetch ACCEPTS a valid http(s) URL — the protocol gate must not reject everything (the colon bug, 2026-10-07)", async () => {
// URL.protocol is \"https:\" WITH the colon; the colon-less comparison
// ported from the py harness (urlparse().scheme has none) made the
// gate throw for every valid URL — the tool never fetched a page in
// its life (owner-reported via OpenRouter logs). This pin stubs fetch
// so the happy path runs hermetically: a VALID url must REACH the
// fetch stage.
const { web_fetch } = createWebTools(() => null);
const realFetch = globalThis.fetch;
globalThis.fetch = (async () =>
new Response("A plain text page with enough content to matter for the test.", {
headers: { "content-type": "text/plain" },
})) as typeof fetch;
try {
const ok = await web_fetch!.execute!("c-ok", { url: "https://example.com/page" });
assert.ok(
!String(ok.details).includes("invalid URL"),
"a valid https URL passes the protocol gate and reaches fetch",
);
assert.match(String(ok.details), /plain text page/);
const ftp = await web_fetch!.execute!("c-ftp", { url: "ftp://example.com/file" });
assert.match(String(ftp.details), /invalid URL/, "non-http(s) schemes still rejected");
} finally {
globalThis.fetch = realFetch;
}
});
}); });
/** Minimal in-memory JobStore — the drop tests never touch the fs. */ /** Minimal in-memory JobStore — the drop tests never touch the fs. */