CherryHQ/cherry-studio · error · Error

Brave API error: ${webResponse.status} ${webResponse.statusT

Error message

Brave API error: ${webResponse.status} ${webResponse.statusText}\n${await webResponse.text()}

What it means

Thrown in the first leg of performLocalSearch: it queries Brave's web search with result_filter=locations to discover location IDs. If that initial HTTP response is not OK, the error propagates immediately. Note this is distinct from the empty-results fallback (performWebSearch) which only triggers when the call succeeds but yields zero locations.

Source

Thrown at src/main/ai/mcp/servers/braveSearch.ts:206

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')
  webUrl.searchParams.set('q', query)
  webUrl.searchParams.set('search_lang', 'en')
  webUrl.searchParams.set('result_filter', 'locations')
  webUrl.searchParams.set('count', Math.min(count, 20).toString())

  const webResponse = await net.fetch(webUrl.toString(), {
    headers: {
      Accept: 'application/json',
      'Accept-Encoding': 'gzip',
      'X-Subscription-Token': apiKey
    }
  })

  if (!webResponse.ok) {
    throw new Error(`Brave API error: ${webResponse.status} ${webResponse.statusText}\n${await webResponse.text()}`)
  }

  const webData = (await webResponse.json()) as BraveWeb
  const locationIds =
    webData.locations?.results?.filter((r): r is { id: string; title?: string } => r.id != null).map((r) => r.id) || []

  if (locationIds.length === 0) {
    return performWebSearch(apiKey, query, count) // Fallback to web search
  }

  // Get POI details and descriptions in parallel
  const [poisData, descriptionsData] = await Promise.all([
    getPoisData(apiKey, locationIds),
    getDescriptionsData(apiKey, locationIds)
  ])

  return formatLocalResults(poisData, descriptionsData)
}

View on GitHub (pinned to 726446b54c)

Solutions

  1. Read the status code in the message: 401 -> fix BRAVE_API_KEY; 429 -> back off; 5xx -> retry.
  2. Confirm the same Brave key works for a plain brave_web_search call (isolates whether the issue is key/quota vs. the local endpoint).
  3. If the locations endpoint is persistently failing, the tool has no graceful degradation — surface the error to the agent and retry later.
  4. Reduce request frequency; checkRateLimit() caps at 1/sec locally but Brave may 429 sooner on shared keys.

Example fix

// before — HTTP error on the locations probe propagates with no fallback
if (!webResponse.ok) {
  throw new Error(`Brave API error: ${webResponse.status} ...`)
}

// after — fall back to web search on transient location-probe failures
if (!webResponse.ok) {
  if (webResponse.status === 429 || webResponse.status >= 500) {
    return performWebSearch(apiKey, query, count) // degrade gracefully
  }
  throw new Error(`Brave API error: ${webResponse.status} ${webResponse.statusText}\n${await webResponse.text()}`)
}
Defensive patterns

Strategy: try-catch

Validate before calling

function validateLocalArgs(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 ?? 5
  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 isLocalArgs(a): a is { query: string; count?: number } {
  return typeof a === 'object' && a !== null && typeof a.query === 'string' && a.query.trim().length > 0
}

Try / catch

// Local search: fall back to plain web search if the locations probe fails transiently
async function localSearchSafe(apiKey, query, count) {
  try {
    return await performLocalSearch(apiKey, query, count)
  } catch (e) {
    const status = parseInt((String(e.message || '').match(/Brave API error: (\d+)/) || [])[1] || '0', 10)
    if (status === 429 || status >= 500) {
      return await performWebSearch(apiKey, query, count)
    }
    throw e
  }
}

Prevention

When it happens

Trigger: Calling brave_local_search when the locations-filtered web request fails: invalid X-Subscription-Token (401), quota/rate limit (429), or Brave backend error (5xx) on the locations probe specifically.

Common situations: Same key/quota causes as web search, but surfaced through the local-search path; users assume local search is broken when the real cause is the shared subscription token or rate budget. The local-search tool does NOT fall back to web search on HTTP errors — only on empty location lists.

Related errors


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