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
- 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.
- Search the dependency chain (fetchRemoteText and anything it calls) for a non-Error throw or reject and convert it to an Error.
- 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
- Ensure all dependencies reject with Error instances, not bare strings/numbers.
- In test mocks, always reject with `new Error(...)` rather than `throw 'msg'`.
- Log the raw rejection value before rethrowing so 'Unknown error' is diagnosable.
- Avoid interceptors that convert errors to plain objects.
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
- OpenAI-compatible reranking model requires baseURL
- Failed to fetch ${url}: ${e.message}
- Failed to delete: ${error.message}
- DashScope API request timeout after ${timeout / 1000}s
AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12).
Data as JSON: /api/errors/816c93031c5dfb06.
Report an issue: GitHub.