{"record":{"id":"a1601475361f285e","repo":"CherryHQ/cherry-studio","slug":"failed-to-fetch-url-e-message","errorCode":null,"errorMessage":"Failed to fetch ${url}: ${e.message}","messagePattern":"Failed to fetch (.+?): (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/mcp/servers/fetch.ts","lineNumber":37,"sourceCode":"function buildHeaders(headers: RequestPayload['headers']): Headers {\n  const resolvedHeaders = new Headers(headers)\n\n  if (!resolvedHeaders.has('User-Agent')) {\n    resolvedHeaders.set('User-Agent', DEFAULT_USER_AGENT)\n  }\n\n  return resolvedHeaders\n}\n\nexport class Fetcher {\n  private static async _fetchText({ url, headers }: RequestPayload): Promise<string> {\n    try {\n      // The URL is model-supplied and this tool is auto-callable, so direct\n      // main-process fetches must bind the connection to validated DNS results.\n      return await fetchRemoteText(url, { headers: buildHeaders(headers), maxRedirects: 5 })\n    } catch (e: unknown) {\n      if (e instanceof Error) {\n        throw new Error(`Failed to fetch ${url}: ${e.message}`)\n      } else {\n        throw new Error(`Failed to fetch ${url}: Unknown error`)\n      }\n    }\n  }\n\n  static async html(requestPayload: RequestPayload) {\n    try {\n      const html = await this._fetchText(requestPayload)\n      return { content: [{ type: 'text', text: html }], isError: false }\n    } catch (error) {\n      return {\n        content: [{ type: 'text', text: (error as Error).message }],\n        isError: true\n      }\n    }\n  }\n","sourceCodeStart":19,"sourceCodeEnd":55,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/mcp/servers/fetch.ts#L19-L55","documentation":"Thrown by Fetcher._fetchText when fetchRemoteText rejects with a standard Error. It re-wraps the underlying cause (network failure, DNS binding mismatch, redirect loop beyond maxRedirects=5, non-2xx HTTP status, or invalid URL) into a uniform 'Failed to fetch <url>: <reason>' message. The wrapper preserves the original message so the caller can diagnose the specific transport failure. This error is caught one frame up by Fetcher.html/json/txt/markdown and returned to the MCP client as a tool result with isError:true rather than propagating as an uncaught exception.","triggerScenarios":"An MCP client invokes fetch_html/fetch_json/fetch_txt/fetch_markdown with a URL that fetchRemoteText cannot honor: the host does not resolve through the validated DNS path, the server returns 4xx/5xx, the redirect chain exceeds 5 hops, the connection times out, or the URL fails z.url() parsing in RequestPayloadSchema before reaching _fetchText (in which case parse throws first). The catch only fires for Error instances; the non-Error branch is error 301.","commonSituations":"Corporate or sandboxed environments where DNS does not resolve public hosts; endpoints behind auth that return 401/403; redirect loops on URL shorteners; TLS certificate problems on self-signed servers; offline development machines; mis-typed URLs that pass z.url() but point at nonexistent hosts.","solutions":["Verify the URL is reachable from the main process's network context using a direct curl/fetch outside the tool, since fetchRemoteText binds to validated DNS results.","Check whether the host is in any allowlist or DNS-resolution path the tool requires; model-supplied URLs are only honored when DNS validates.","If the endpoint redirects, confirm the chain length is <= 5 or raise maxRedirects in the fetchRemoteText call.","Inspect the trailing message after the colon — it is the underlying cause (e.g. 'ENOTFOUND', 'ETIMEDOUT', 'max redirect') and points at the layer to fix."],"exampleFix":"// before\nreturn await fetchRemoteText(url, { headers: buildHeaders(headers), maxRedirects: 5 })\n\n// after — surface the specific failure class for the caller\ntry {\n  return await fetchRemoteText(url, { headers: buildHeaders(headers), maxRedirects: 5 })\n} catch (e) {\n  const reason = e instanceof Error ? e.message : 'Unknown error'\n  if (reason.includes('ENOTFOUND')) throw new Error(`DNS resolution failed for ${url}: ${reason}`)\n  if (reason.includes('redirect')) throw new Error(`Redirect limit exceeded for ${url}`)\n  throw new Error(`Failed to fetch ${url}: ${reason}`)\n}","handlingStrategy":"retry","validationCode":"// Validate reachability + redirect budget before dispatching the fetch tool.\nasync function preflightUrl(url: string, maxRedirects = 5): Promise<void> {\n  const parsed = z.url().safeParse(url)\n  if (!parsed.success) throw new Error(`Bad URL: ${url}`)\n  // Optional: HEAD request to confirm host resolves and responds\n  const res = await fetch(url, { method: 'HEAD', redirect: 'manual' })\n  if (res.status >= 400) throw new Error(`Preflight status ${res.status}`)\n}","typeGuard":"// Narrow an unknown fetch failure to a network/DNS error worth retrying.\nfunction isTransientNetworkError(e: unknown): boolean {\n  if (!(e instanceof Error)) return false\n  return /ENOTFOUND|ECONNRESET|ETIMEDOUT|EAI_AGAIN|socket hang up|redirect/i.test(e.message)\n}","tryCatchPattern":"// Retry transient fetch failures with backoff; surface the final error.\nasync function fetchWithRetry(url: string, attempts = 3): Promise<string> {\n  let lastErr: unknown\n  for (let i = 0; i < attempts; i++) {\n    try {\n      return await fetchRemoteText(url, { maxRedirects: 5 })\n    } catch (e) {\n      lastErr = e\n      if (!isTransientNetworkError(e) || i === attempts - 1) break\n      await new Promise(r => setTimeout(r, 2 ** i * 200))\n    }\n  }\n  throw lastErr\n}","preventionTips":["Validate the URL with z.url() on the client before dispatching the fetch tool.","Confirm the host resolves in the main process's DNS context, not just the browser's.","Keep redirect chains under 5 hops or raise maxRedirects explicitly.","Log the underlying error code (ENOTFOUND, ETIMEDOUT) so transient vs permanent failures are distinguishable."],"tags":["network","fetch","mcp-tool","dns"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}