{"record":{"id":"8bc26758283a9a4f","repo":"moeru-ai/airi","slug":"web-search-failed-tavily-returned-a-non-json-resp","errorCode":null,"errorMessage":"web search failed: tavily returned a non-JSON response","messagePattern":"web search failed: tavily returned a non-JSON response","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/tools/web-search.ts","lineNumber":153,"sourceCode":"    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 ?? '',\n    snippet: (result.content ?? '').slice(0, DEFAULT_RESULT_CHARS),\n    ...(typeof result.score === 'number' ? { score: result.score } : {}),\n    ...(result.published_date ? { ageHint: result.published_date } : {}),\n  }))\n}\n\n/**\n * Renders results as a numbered list the model can read and cite. Each snippet\n * is wrapped as untrusted content; the leading `[N] url` citations survive even\n * if the model ignores the rest.","sourceCodeStart":135,"sourceCodeEnd":171,"githubUrl":"https://github.com/moeru-ai/airi/blob/b6d0809ecb6421d492c4ce38aa7a76146c83269d/packages/stage-ui/src/tools/web-search.ts#L135-L171","documentation":"Tavily returned 2xx but response.json() failed — the body was not valid JSON, typically an HTML error/proxy page or a truncated response. The tool keeps the 'web search failed: tavily' taxonomy so callers handle it like other search failures instead of seeing an opaque SyntaxError.","triggerScenarios":"An intercepting proxy or captive portal returning an HTML page with status 200; a CDN edge error page; a truncated body on a flaky connection.","commonSituations":"Corporate proxy/VPN TLS interception; cloudflare-style 200 error pages; mobile networks truncating long responses.","solutions":["Retry once — truncated or corrupted responses are frequently transient.","Log the response content-type; text/html confirms proxy/CDN interception.","Bypass the proxy for api.tavily.com or add an exception rule.","If persistent, capture the first bytes of the body to identify the interceptor."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"const contentType = response.headers.get('content-type') ?? ''\nif (response.ok && !contentType.includes('application/json')) {\n  // treat as intercepted/proxied response before parsing\n}","typeGuard":null,"tryCatchPattern":"for (let attempt = 0; attempt < 2; attempt++) {\n  try {\n    return await parseTavilyResponse(response)\n  }\n  catch (e) {\n    if (attempt === 1 || !errorMessageFrom(e)?.includes('non-JSON'))\n      throw e\n    response = await refetch()\n  }\n}","preventionTips":["Check the content-type header before parsing 2xx bodies.","Add a one-shot retry for parse failures distinct from HTTP failures.","Alert when non-JSON 2xx repeats — it indicates interception, not flakiness."],"tags":["web-search","tavily","json","proxy","response-parsing"],"backgroundTag":"invalid-json-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"}