CherryHQ/cherry-studio · error · Error

Failed to fetch ${url}: Unknown error

Error message

Failed to fetch ${url}: Unknown error

What it means

The defensive fallback branch of _fetchText's catch: fetchRemoteText rejected with a non-Error value (a string, number, null, undefined, or a plain object without Error.prototype). It exists because a bare `throw 'string'` in a dependency would otherwise produce an unhelpful '[object Object]' or empty message. Rare in practice — almost every Node.js and undici rejection is an Error instance — but it guarantees the tool result always carries a human-readable reason.

Source

Thrown at src/main/ai/mcp/servers/fetch.ts:39

  if (!resolvedHeaders.has('User-Agent')) {
    resolvedHeaders.set('User-Agent', DEFAULT_USER_AGENT)
  }

  return resolvedHeaders
}

export class Fetcher {
  private static async _fetchText({ url, headers }: RequestPayload): Promise<string> {
    try {
      // The URL is model-supplied and this tool is auto-callable, so direct
      // main-process fetches must bind the connection to validated DNS results.
      return await fetchRemoteText(url, { headers: buildHeaders(headers), maxRedirects: 5 })
    } catch (e: unknown) {
      if (e instanceof Error) {
        throw new Error(`Failed to fetch ${url}: ${e.message}`)
      } else {
        throw new Error(`Failed to fetch ${url}: Unknown error`)
      }
    }
  }

  static async html(requestPayload: RequestPayload) {
    try {
      const html = await this._fetchText(requestPayload)
      return { content: [{ type: 'text', text: html }], isError: false }
    } catch (error) {
      return {
        content: [{ type: 'text', text: (error as Error).message }],
        isError: true
      }
    }
  }

  static async json(requestPayload: RequestPayload) {
    try {

View on GitHub (pinned to 726446b54c)

Solutions

  1. Inspect the full stack/context around the call — 'Unknown error' means the cause was not an Error, so the original value was swallowed; add logging in _fetchText to capture the raw `e` value before it is lost.
  2. Search the dependency chain (fetchRemoteText and anything it calls) for a non-Error throw or reject and convert it to an Error.
  3. If reproducing in tests, ensure mocks reject with `new Error(...)` rather than bare strings.

Example fix

// before
} else {
  throw new Error(`Failed to fetch ${url}: Unknown error`)
}

// after — preserve the raw value so it is diagnosable
} else {
  logger.error('fetchRemoteText rejected with non-Error', { url, value: e })
  throw new Error(`Failed to fetch ${url}: ${typeof e === 'string' ? e : 'Unknown error'}`)
}
Defensive patterns

Strategy: try-catch

Type guard

// Distinguish Error from non-Error rejections so the 'Unknown error' branch is reachable.
function isErrorLike(e: unknown): e is Error {
  return e instanceof Error || (typeof e === 'object' && e !== null && 'message' in e && typeof (e as any).message === 'string')
}

Try / catch

// Catch non-Error rejections and preserve their value for diagnosis.
try {
  return await fetchRemoteText(url, opts)
} catch (e: unknown) {
  if (e instanceof Error) throw new Error(`Failed to fetch ${url}: ${e.message}`)
  console.error('non-Error rejection from fetchRemoteText', e)
  throw new Error(`Failed to fetch ${url}: ${typeof e === 'string' ? e : 'Unknown error'}`)
}

Prevention

When it happens

Trigger: A dependency or interceptor along the fetch path throws a non-Error value, e.g. a third-party middleware doing `throw 'network down'`, a Promise reject with `null`, or a custom agent that rejects with a status code number. Also reachable if fetchRemoteText itself has a code path that does `throw someString`.

Common situations: Patched or monkey-patched fetch implementations in test harnesses; older libraries that reject with strings rather than Error objects; bugs in interceptors (e.g. a proxy module) that reject with the HTTP status number.

Related errors


AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12). Data as JSON: /api/errors/816c93031c5dfb06. Report an issue: GitHub.