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
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.
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.
Defensive patterns
Strategy: retry
Validate before calling
const contentType = response.headers.get('content-type') ?? ''
if (response.ok && !contentType.includes('application/json')) {
// treat as intercepted/proxied response before parsing
} Try / catch
for (let attempt = 0; attempt < 2; attempt++) {
try {
return await parseTavilyResponse(response)
}
catch (e) {
if (attempt === 1 || !errorMessageFrom(e)?.includes('non-JSON'))
throw e
response = await refetch()
}
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: Corporate proxy/VPN TLS interception; cloudflare-style 200 error pages; mobile networks truncating long responses.
Understand the failure class
Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.
Related errors
- web search failed: tavily
- CAP_VITE_CAP_ARGS_JSON must be a JSON string array.
- Expected a JSON object in
- Expected `cap run --list --json` to return a JSON array.
- Extension manifest is not valid JSON
AI-assisted analysis of moeru-ai/airi@b6d0809ecb (2026-08-18).
Data as JSON: /api/errors/8bc26758283a9a4f.
Report an issue: GitHub.
Appendix: source
Thrown at packages/stage-ui/src/tools/web-search.ts:153
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 b6d0809ecb)