Skip to content

fetch(url: URL) rejects with 'Invalid URL' (cc's MCP HTTP transport passes a URL instance); a DNS failure is a plain Error without node's 'fetch failed' + cause.code #9536

Description

@proggeramlug

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:

  1. 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.
  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions