moeru-ai/airi · error · Error

web search failed: tavily ${response.status}${detail ? `: ${

Error message

web search failed: tavily ${response.status}${detail ? `: ${detail}` : ''}

What it means

Thrown by searchTavily() (web-search.ts:147) when the Tavily POST to https://api.tavily.com/search returns a non-2xx status. The body is read and sliced to 200 characters so a failing endpoint cannot flood the model context or logs; the HTTP status and that detail slice are concatenated into the message. This is the single error taxonomy for transport-level Tavily failures (auth, quota, bad request, upstream outage).

Source

Thrown at packages/stage-ui/src/tools/web-search.ts:147

    body.include_domains = input.include_domains
  if (input.exclude_domains?.length)
    body.exclude_domains = input.exclude_domains

  const response = await fetch(TAVILY_SEARCH_URL, {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      'authorization': `Bearer ${apiKey}`,
    },
    body: JSON.stringify(body),
    signal,
  })

  if (!response.ok) {
    // Slice the body so a failing endpoint never dumps a full payload into the
    // model context or logs.
    const detail = (await response.text().catch(() => '')).slice(0, 200)
    throw new Error(`web search failed: tavily ${response.status}${detail ? `: ${detail}` : ''}`)
  }

  // A 2xx with a non-JSON body (an HTML proxy/error page, a truncated response)
  // would otherwise throw an opaque SyntaxError; surface it in the same taxonomy.
  let json: { results?: Array<{ title?: string, url?: string, content?: string, score?: number, published_date?: string }> }
  try {
    json = await response.json()
  }
  catch {
    throw new Error('web search failed: tavily returned a non-JSON response')
  }

  // Guard the shape before mapping: a 2xx whose `results` is missing or not an
  // array is treated as "no results" rather than throwing on `.map`.
  const results = Array.isArray(json.results) ? json.results : []
  return results.map(result => ({
    title: result.title ?? '',
    url: result.url ?? '',

View on GitHub (pinned to 27111382b4)

Solutions

  1. Read the HTTP status in the message: 401/403 → regenerate and re-enter the Tavily API key in the web-search settings; 429 → wait and/or upgrade the Tavily plan; 5xx → retry after a short backoff.
  2. Confirm the key is being passed into createWebSearchTools({ apiKey }) and that no leading/trailing whitespace or 'Bearer ' prefix was accidentally included (the code already adds the Bearer prefix at web-search.ts:137).
  3. Verify network egress to https://api.tavily.com/search is not blocked or rewritten by a proxy.
  4. Surface the error to the user (via the tool result) so they know the web-search provider rejected the request, and offer to update the key in settings.

Example fix

// before: stale or empty key passed through
const tools = await createWebSearchTools({ apiKey: storedKey })
// -> web search failed: tavily 401: Unauthorized

// after: gate mounting on a configured, trimmed key
const apiKey = storedKey?.trim()
if (!apiKey) return [] // omit web_search entirely
return await createWebSearchTools({ apiKey })
Defensive patterns

Strategy: try-catch

Validate before calling

// Validate the key shape/length before mounting the tool.
const apiKey = options.apiKey?.trim()
if (!apiKey || apiKey.length < 20) {
  // Tavily keys are long; a short/empty value will 401 — do not mount.
  return []
}
return await createWebSearchTools({ apiKey, timeoutMs: options.timeoutMs })

Try / catch

// Keep tool execution resilient: catch Tavily failures and return a model-facing
// error string instead of letting the tool reject the whole turn.
try {
  const results = await searchTavily(apiKey, input, maxResults, signal)
  return formatResults(input.query, results)
}
catch (error) {
  const msg = errorMessageFromValue(error)
  if (msg.startsWith('web search failed: tavily ')) {
    // provider failure — surface to the model, do not retry automatically.
    return `Web search is unavailable right now (${msg}). Answer from existing knowledge or tell the user to check the Tavily key/quota.`
  }
  throw error
}

Prevention

When it happens

Trigger: Calling the web_search tool with an invalid/expired/revoked Tavily API key (401/403), exceeding the Tavily plan's rate limit or monthly quota (429), sending a malformed request body (400, though the schema normally prevents this), or hitting a Tavily/CDN outage (5xx). Also reachable if a corporate proxy returns its own non-2xx status for api.tavily.com.

Common situations: The Tavily key was rotated in the dashboard but the new value was not saved into the web-search module settings; the free-tier quota was exhausted mid-session; a stale key leaked into production env; a proxy/firewall blocks or rewrites the request to api.tavily.com.

Related errors


AI-assisted analysis of moeru-ai/airi@27111382b4 (2026-08-12). Data as JSON: /api/errors/f492f64635d18937. Report an issue: GitHub.