moeru-ai/airi · error · Error

web search failed: tavily returned a non-JSON response

Error message

web search failed: tavily returned a non-JSON response

What it means

Thrown by searchTavily() (web-search.ts:157) when Tavily returns a 2xx status but the body cannot be parsed as JSON. The explicit catch converts what would otherwise be an opaque SyntaxError from response.json() into the same 'web search failed: tavily ...' taxonomy so callers see a consistent failure shape. A 2xx with HTML/text typically means an intermediary returned its own page instead of the API payload.

Source

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

    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 ?? '',
    snippet: (result.content ?? '').slice(0, DEFAULT_RESULT_CHARS),
    ...(typeof result.score === 'number' ? { score: result.score } : {}),
    ...(result.published_date ? { ageHint: result.published_date } : {}),
  }))
}

/**
 * Renders results as a numbered list the model can read and cite. Each snippet
 * is wrapped as untrusted content; the leading `[N] url` citations survive even
 * if the model ignores the rest.

View on GitHub (pinned to 27111382b4)

Solutions

  1. Inspect the actual response body: capture it (e.g. via a network log or by temporarily logging response.text()) to identify the proxy/maintenance page responsible.
  2. Bypass or whitelist api.tavily.com in any corporate proxy / SSL-inspection / captive-portal layer so the request reaches the real API.
  3. If intermittent, retry once with a short backoff — a truncated body is often transient.
  4. Verify the request Content-Type/Accept headers and that no proxy is altering the response encoding.

Example fix

// before: opaque SyntaxError swallowed into the taxonomy
json = await response.json()
// -> web search failed: tavily returned a non-JSON response

// after (diagnostic): capture the body to find the culprit proxy page
const text = await response.text()
console.warn('[web_search] non-JSON 2xx body (first 300 chars):', text.slice(0, 300))
json = JSON.parse(text)
Defensive patterns

Strategy: validation

Validate before calling

// After reading the body, validate it is JSON-shaped before relying on it.
const text = await response.text()
if (!text || text[0] !== '{') {
  // A 2xx body that does not start with '{' is a proxy/maintenance page.
  throw new Error('web search failed: tavily returned a non-JSON response')
}
const json = JSON.parse(text)

Try / catch

// Treat a non-JSON 2xx as a transient/infra failure: log once and retry once.
async function readTavilyJson(response: Response) {
  try {
    return await response.json()
  }
  catch {
    const body = (await response.text().catch(() => '')).slice(0, 200)
    console.warn('[web_search] non-JSON 2xx body:', body)
    throw new Error('web search failed: tavily returned a non-JSON response')
  }
}

Prevention

When it happens

Trigger: A corporate proxy, captive portal, or transparent CDN returns an HTML error/login page with a 200 status for api.tavily.com; a truncated response body from a flaky upstream; a misconfigured reverse proxy or gateway in front of the Tavily endpoint serving a maintenance page; response body gzip/encoding mismatch producing non-JSON bytes.

Common situations: Running on a network with mandatory proxy auth that serves an HTML login page for unauthenticated egress; deploying behind a corporate firewall whose SSL-inspection appliance rewrites responses; intermittent upstream issues that close the connection mid-body.

Related errors


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