Found through the MCP debug logs that #9491 unlocked (#9500 / #9531): cc's HTTP MCP transport logs
node : HTTP Connection failed after 4ms: fetch failed (code: none, errno: none) → TypeError: fetch failed
perry: HTTP Connection failed after 1ms: Invalid URL (code: none, errno: none) → Error: Invalid URL
for https://example.invalid/mcp (the harness's unreachable HTTP server). The SDK's StreamableHTTPClientTransport calls fetch(this._url, init) where _url is the URL instance cc constructs (client/streamableHttp.js:94/311/449).
Minimal repro
const u = new URL("https://example.invalid/mcp");
for (const [label, go] of [
["string", () => fetch("https://example.invalid/mcp", { method: "POST", body: "{}" })],
["URL object", () => fetch(u, { method: "POST", body: "{}" })],
["URL object + Headers", () => fetch(u, { method: "POST", headers: new Headers({ "X-A": "1" }), body: "{}" })],
["Request object", () => fetch(new Request(u, { method: "POST", body: "{}" }))],
["URL object, GET, signal", () => fetch(u, { signal: AbortSignal.timeout(5000) })],
] as const) {
try { const r = await go(); console.log(label, "-> status", r.status); }
catch (e: any) { console.log(label, "->", e?.name, JSON.stringify(e?.message), "cause:", e?.cause?.code ?? "none"); }
}
node 26.5.1: every shape -> TypeError "fetch failed" cause: ENOTFOUND
perry main : string -> Error "Fetch error: error sending request for url (https://example.invalid/mcp)" cause: none
URL object -> Error "Invalid URL" cause: none <- the cc shape
URL + Headers -> Error "Invalid URL" cause: none
Request -> Error "Fetch error: error sending request for url (...)" cause: none
URL, signal -> Error "Invalid URL" cause: none
Two divergences, in order of weight:
fetch(URL) is rejected as Invalid URL — the URL instance is not stringified (the spec's RequestInfo is USVString | Request, and a URL converts through toString(); node, Bun and browsers all accept it). Only the string and Request forms reach the network. This is what cc hits.
- The DNS-failure error shape: node rejects with
TypeError whose message is exactly fetch failed and whose cause carries the node-shaped error (code: 'ENOTFOUND', syscall, hostname); perry rejects with a plain Error("Fetch error: error sending request for url (...)") and no cause. Code that branches on e.cause.code (cc does for its "(code: ..., errno: ...)" log line, retry classifiers do for ENOTFOUND/ECONNREFUSED) sees none.
Byte-compare bar: the fixture above prints node's five lines.
Found through the MCP debug logs that #9491 unlocked (#9500 / #9531): cc's HTTP MCP transport logs
for
https://example.invalid/mcp(the harness's unreachable HTTP server). The SDK'sStreamableHTTPClientTransportcallsfetch(this._url, init)where_urlis theURLinstance cc constructs (client/streamableHttp.js:94/311/449).Minimal repro
Two divergences, in order of weight:
fetch(URL)is rejected asInvalid URL— theURLinstance is not stringified (the spec'sRequestInfoisUSVString | Request, and aURLconverts throughtoString(); node, Bun and browsers all accept it). Only the string andRequestforms reach the network. This is what cc hits.TypeErrorwhosemessageis exactlyfetch failedand whosecausecarries the node-shaped error (code: 'ENOTFOUND',syscall,hostname); perry rejects with a plainError("Fetch error: error sending request for url (...)")and nocause. Code that branches one.cause.code(cc does for its "(code: ..., errno: ...)" log line, retry classifiers do forENOTFOUND/ECONNREFUSED) seesnone.Byte-compare bar: the fixture above prints node's five lines.