can1357/oh-my-pi · error · SearchProviderError
Exa API error (${response.status}): ${errorText}
Error message
Exa API error (${response.status}): ${errorText} What it means
The Exa REST API returned a non-2xx response. The provider reads the response body as text, attempts classification into a specific error, and if none applies throws SearchProviderError with the raw status and the response body text embedded.
Source
Thrown at packages/coding-agent/src/web/search/providers/exa.ts:329
const body = buildExaRequestBody(params);
const fetchImpl = params.fetch ?? fetch;
await waitForExaSearchSlot(params.signal);
const response = await fetchImpl(EXA_API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify(body),
signal: withHardTimeout(params.signal, params.timeoutMs),
});
if (!response.ok) {
const errorText = await response.text();
const classified = classifyProviderHttpError("exa", response.status, errorText);
if (classified) throw classified;
throw new SearchProviderError("exa", `Exa API error (${response.status}): ${errorText}`, response.status);
}
return response.json() as Promise<ExaSearchResponse>;
}
function buildExaMcpArgs(params: ExaSearchParams): Record<string, unknown> {
const queryParts = [params.query];
for (const domain of params.include_domains ?? []) {
const trimmed = domain.trim();
if (trimmed) queryParts.push(`site:${trimmed}`);
}
for (const domain of params.exclude_domains ?? []) {
const trimmed = domain.trim();
if (trimmed) queryParts.push(`-site:${trimmed}`);
}
if (params.start_published_date) queryParts.push(`after:${params.start_published_date}`);
if (params.end_published_date) queryParts.push(`before:${params.end_published_date}`);
const args: Record<string, unknown> = { query: queryParts.join(" ") };View on GitHub (pinned to 9690622007)
Solutions
- Read the embedded errorText — Exa's JSON error body usually states the exact problem.
- Verify EXA_API_KEY is valid and active at dashboard.exa.ai (401/403).
- Check request parameters (query, numResults, category) against Exa's API docs (400).
- Retry with backoff for 429/5xx; check rate limits on your Exa plan.
- Fall back to the keyless Exa MCP path or another provider.
Example fix
// before
const res = await searchExa({ query, apiKey });
// after
try {
const res = await searchExa({ query, apiKey });
} catch (e) {
console.error(e.message); // e.g. "Exa API error (401): {\"error\":\"invalid api key\"}"
} Defensive patterns
Strategy: try-catch
Validate before calling
if (!process.env.EXA_API_KEY) throw new Error("EXA_API_KEY not set; Exa REST calls will fail"); Try / catch
try {
const res = await searchExa({ query, apiKey });
} catch (e) {
if (e instanceof SearchProviderError && e.provider === "exa") {
// e.status + embedded errorText tell you 401 (bad key) vs 429 (quota) vs 5xx
if (e.status === 401) refreshApiKey();
else if (e.status === 429) backoffAndRetry();
} else throw e;
} Prevention
- Validate EXA_API_KEY before making calls (probe once at startup)
- Match parameters against Exa's API docs to avoid 400s
- Back off on 429/5xx instead of hammering
- Read the embedded errorText — Exa returns descriptive JSON error bodies
When it happens
Trigger: Any authenticated callExaSearch REST request where response.ok is false: invalid API key (401), quota exceeded (429), bad request parameters (400), or Exa server errors (5xx) that the classifier doesn't special-case.
Common situations: Expired or wrong EXA_API_KEY; malformed query parameters sent to Exa; Exa outage; exceeding paid-plan limits with unclassified body text.
Related errors
- Exa MCP request failed (${response.status}): ${errorText}
- Jina API response reported failure (${payload.code})
- Bedrock HTTP ${response.status}: ${errBody.slice(0, 1000)}
- Devin auth error ${response.status} ${response.statusText}:
- Devin AssignModel error ${response.status} ${response.status
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/4f80ebd889d68828.
Report an issue: GitHub.