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

  1. Read the embedded errorText — Exa's JSON error body usually states the exact problem.
  2. Verify EXA_API_KEY is valid and active at dashboard.exa.ai (401/403).
  3. Check request parameters (query, numResults, category) against Exa's API docs (400).
  4. Retry with backoff for 429/5xx; check rate limits on your Exa plan.
  5. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/4f80ebd889d68828. Report an issue: GitHub.