CherryHQ/cherry-studio · error · Error
Brave API error: ${response.status} ${response.statusText}\n
Error message
Brave API error: ${response.status} ${response.statusText}\n${await response.text()} What it means
Thrown by performWebSearch after net.fetch to Brave's /res/v1/web/search endpoint returns a non-OK HTTP status. The message concatenates status code, status text, and the raw response body so the upstream Brave API error is fully surfaced. Brave returns 401 for a bad/missing X-Subscription-Token, 429 for rate limiting, 5xx for upstream outages, and 422/400 for malformed/oversized queries.
Source
Thrown at src/main/ai/mcp/servers/braveSearch.ts:173
}
async function performWebSearch(apiKey: string, query: string, count: number = 10, offset: number = 0) {
checkRateLimit()
const url = new URL('https://api.search.brave.com/res/v1/web/search')
url.searchParams.set('q', query)
url.searchParams.set('count', Math.min(count, 20).toString()) // API limit
url.searchParams.set('offset', offset.toString())
const response = await net.fetch(url.toString(), {
headers: {
Accept: 'application/json',
'Accept-Encoding': 'gzip',
'X-Subscription-Token': apiKey
}
})
if (!response.ok) {
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`)
}
const data = (await response.json()) as BraveWeb
// Extract just web results
const results = (data.web?.results || []).map((result) => ({
title: result.title || '',
description: result.description || '',
url: result.url || ''
}))
return results.map((r) => `Title: ${r.title}\nDescription: ${r.description}\nURL: ${r.url}`).join('\n\n')
}
async function performLocalSearch(apiKey: string, query: string, count: number = 5) {
checkRateLimit()
// Initial search to get location IDs
const webUrl = new URL('https://api.search.brave.com/res/v1/web/search')View on GitHub (pinned to 726446b54c)
Solutions
- Inspect the surfaced status code in the error message — 401 means fix BRAVE_API_KEY, 429 means back off / upgrade plan, 422 means shorten the query.
- Verify the key in the MCP server env config (the same envs.BRAVE_API_KEY passed to BraveSearchServer) is a valid active Brave Search API subscription token.
- For 429/5xx, retry with exponential backoff; checkRateLimit() already gates per-second/per-month counts locally but not server-enforced 429s.
- Trim the query to <=400 chars and <=50 words before calling performWebSearch.
- Check https://status.search.brave.com for ongoing Brave API outages.
Example fix
// before
url.searchParams.set('q', query)
const response = await net.fetch(url.toString(), { headers })
if (!response.ok) {
throw new Error(`Brave API error: ${response.status} ...`)
}
// after — retry transient failures, fail fast on auth errors
if (response.status === 401 || response.status === 403) {
throw new Error(`Brave auth failed (${response.status}); check BRAVE_API_KEY`)
}
if (response.status === 429 || response.status >= 500) {
throw new RetryableError(`Brave transient ${response.status}`) // retry upstream
}
if (!response.ok) {
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`)
} Defensive patterns
Strategy: try-catch
Validate before calling
// Validate inputs before calling brave_web_search
function validateWebSearchArgs(args) {
if (typeof args.query !== 'string' || !args.query.trim()) throw new Error('query required')
if (args.query.length > 400) throw new Error('query must be <= 400 chars')
const c = args.count ?? 10
if (typeof c !== 'number' || c < 1 || c > 20) throw new Error('count must be 1-20')
return { query: args.query.trim(), count: Math.min(c, 20) }
} Type guard
function isWebArgs(a): a is { query: string; count?: number } {
return typeof a === 'object' && a !== null && typeof a.query === 'string' && a.query.length > 0 && a.query.length <= 400
} Try / catch
// Retry transient HTTP statuses; fail fast on auth/validation
async function braveWebSearchWithRetry(apiKey, query, count, tries = 3) {
for (let i = 0; i < tries; i++) {
try {
return await performWebSearch(apiKey, query, count)
} catch (e) {
const msg = String(e.message || e)
const status = parseInt((msg.match(/Brave API error: (\d+)/) || [])[1] || '0', 10)
if (status === 401 || status === 403 || status === 422) throw e // non-retryable
if (i === tries - 1) throw e
await new Promise((r) => setTimeout(r, 2 ** i * 500))
}
}
} Prevention
- Confirm BRAVE_API_KEY is valid before enabling the server (one test call).
- Cap query length client-side at 400 chars and 50 words.
- Retry only on 429/5xx; surface 401/422 immediately.
- Monitor monthly quota to avoid surprise 429s.
When it happens
Trigger: Any MCP CallTool request for brave_web_search where Brave rejects the request: invalid or expired API key (401), exceeded 1 req/s or 15000/month plan quota (429), query longer than 400 chars / 50 words (422), or transient Brave backend failure (5xx).
Common situations: API key typo or copied with whitespace; free-tier key hitting the monthly cap mid-session; an LLM constructing a very long query string; Brave API incident; proxy/corporate network blocking api.search.brave.com.
Related errors
- Brave API error: ${webResponse.status} ${webResponse.statusT
- ModelScope API error: ${response.status} - ${errorText}
- PPIO API error: ${response.status} - ${errorText}
- Discord API error ${url}: HTTP ${response.status} - ${errorT
- Failed to get access token: HTTP ${response.status}
AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12).
Data as JSON: /api/errors/a8c3280ad61acf8b.
Report an issue: GitHub.