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

  1. 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.
  2. 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.
  3. For 429/5xx, retry with exponential backoff; checkRateLimit() already gates per-second/per-month counts locally but not server-enforced 429s.
  4. Trim the query to <=400 chars and <=50 words before calling performWebSearch.
  5. 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

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


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