{"record":{"id":"f492f64635d18937","repo":"moeru-ai/airi","slug":"web-search-failed-tavily-response-status-deta","errorCode":null,"errorMessage":"web search failed: tavily ${response.status}${detail ? `: ${detail}` : ''}","messagePattern":"web search failed: tavily (.+?)(.+?)` : ''\\}","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/tools/web-search.ts","lineNumber":143,"sourceCode":"    body.include_domains = input.include_domains\n  if (input.exclude_domains?.length)\n    body.exclude_domains = input.exclude_domains\n\n  const response = await fetch(TAVILY_SEARCH_URL, {\n    method: 'POST',\n    headers: {\n      'content-type': 'application/json',\n      'authorization': `Bearer ${apiKey}`,\n    },\n    body: JSON.stringify(body),\n    signal,\n  })\n\n  if (!response.ok) {\n    // Slice the body so a failing endpoint never dumps a full payload into the\n    // model context or logs.\n    const detail = (await response.text().catch(() => '')).slice(0, 200)\n    throw new Error(`web search failed: tavily ${response.status}${detail ? `: ${detail}` : ''}`)\n  }\n\n  // A 2xx with a non-JSON body (an HTML proxy/error page, a truncated response)\n  // would otherwise throw an opaque SyntaxError; surface it in the same taxonomy.\n  let json: { results?: Array<{ title?: string, url?: string, content?: string, score?: number, published_date?: string }> }\n  try {\n    json = await response.json()\n  }\n  catch {\n    throw new Error('web search failed: tavily returned a non-JSON response')\n  }\n\n  // Guard the shape before mapping: a 2xx whose `results` is missing or not an\n  // array is treated as \"no results\" rather than throwing on `.map`.\n  const results = Array.isArray(json.results) ? json.results : []\n  return results.map(result => ({\n    title: result.title ?? '',\n    url: result.url ?? '',","sourceCodeStart":125,"sourceCodeEnd":161,"githubUrl":"https://github.com/moeru-ai/airi/blob/b6d0809ecb6421d492c4ce38aa7a76146c83269d/packages/stage-ui/src/tools/web-search.ts#L125-L161","documentation":"The web-search tool called Tavily's REST endpoint with the configured bearer key and got a non-2xx; the message embeds the status plus up to 200 chars of body so the model and logs see the upstream reason without payload dumps.","triggerScenarios":"401/403 invalid or revoked Tavily API key; 429 (or 432) quota/rate limit exhausted; 400 malformed query arguments; region-blocked access.","commonSituations":"Key pasted with trailing whitespace/newline; expired free-tier quota; one key shared across environments hitting rate limits; a proxy in front of api.tavily.com rejecting the request.","solutions":["Check the Tavily key in provider settings and re-save it cleanly (no whitespace).","Verify quota/billing on the Tavily dashboard.","Classify by embedded status: 401/403 = key, 429/432 = quota, 400 = arguments.","Retry with backoff only for 429/5xx."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"const apiKey = providerConfig.apiKey?.trim()\nif (!apiKey) {\n  return { results: [], error: 'Tavily API key not configured' }\n}","typeGuard":null,"tryCatchPattern":"try {\n  const results = await webSearchTavily(query, { apiKey, signal })\n}\ncatch (e) {\n  const msg = errorMessageFrom(e) ?? ''\n  if (msg.includes('tavily 401'))\n    configureProvider()\n  else if (msg.includes('tavily 429') || msg.includes('tavily 432'))\n    await delay(60_000) // then retry once\n  else\n    throw e\n}","preventionTips":["Trim and validate the API key before first use.","Run a cheap warm-up call at settings-save time to fail fast on bad keys.","Log the status code separately from the body for alerting."],"tags":["web-search","tavily","http","api-key","quota"],"backgroundTag":"http-error-response","analyzedSha":"b6d0809ecb6421d492c4ce38aa7a76146c83269d","analyzedAt":"2026-08-18T17:29:58.153Z","contentChangedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}