koala73/worldmonitor · error
Exa ${endpoint.slice(1)} failed HTTP ${response.status}: ${d
Error message
Exa ${endpoint.slice(1)} failed HTTP ${response.status}: ${detail.slice(0, 120)} What it means
All Exa REST calls in this provider (search, contents) go through request(), which throws on any non-2xx with the endpoint name, HTTP status, and the first 120 characters of the response body — enough to distinguish 401 (bad API key), 400 INVALID_REQUEST_BODY, 429 rate/quota, and 5xx incidents.
Source
Thrown at consumer-prices-core/src/acquisition/exa.ts:141
return false;
}
}
private async request<T>(endpoint: string, body: Record<string, unknown>, timeout = 30_000): Promise<T> {
const response = await globalThis.fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: {
'x-api-key': this.apiKey,
'Content-Type': 'application/json',
'User-Agent': 'worldmonitor-consumer-prices/1.0',
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(timeout),
});
if (!response.ok) {
const detail = await response.text().catch(() => '');
throw new Error(`Exa ${endpoint.slice(1)} failed HTTP ${response.status}: ${detail.slice(0, 120)}`);
}
return (await response.json()) as T;
}
}
function parseStructuredSummary<T>(summary: unknown): T | null {
if (summary && typeof summary === 'object') return summary as T;
if (typeof summary !== 'string') return null;
const trimmed = summary.trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '');
if (!trimmed) return null;
try {
const parsed: unknown = JSON.parse(trimmed);
return parsed && typeof parsed === 'object' ? (parsed as T) : null;
} catch {
return null;
}View on GitHub (pinned to eeab0a219f)
Solutions
- Read the status and body slice in the message: 401 → fix the EXA API key env; 400 → fix the request/schema shape
- For nullable fields use anyOf: [{ type: 'number' }, { type: 'null' }] — never type arrays
- On 429, back off exponentially and check the plan quota before resuming the batch
- On 5xx, retry with backoff and check the Exa status page
Example fix
// before — nullable field as a JSON-Schema type array
const outputSchema = {
type: 'object',
properties: { price: { type: ['number', 'null'] } }, // Exa 400: INVALID_REQUEST_BODY
};
// after — express nullability with anyOf, which Exa's validator accepts
const outputSchema = {
type: 'object',
properties: { price: { anyOf: [{ type: 'number' }, { type: 'null' }] } },
}; Defensive patterns
Strategy: try-catch
Validate before calling
// Cheap preflight: key present and non-empty before any Exa call
if (!process.env.EXA_API_KEY) throw new Error('EXA_API_KEY not set'); Try / catch
try {
await exa.search(q);
} catch (err) {
const m = /failed HTTP (\d+): (.*)/.exec(err.message);
if (!m) throw err;
const [, status, detail] = m;
if (status === '401') throw new ConfigError('Exa API key invalid');
if (status === '429') return backoffRetry(fn); // rate/quota — retryable
if (Number(status) >= 500) return backoffRetry(fn); // provider incident
throw new Error(`Exa request rejected: ${detail}`); // 400 — fix the payload
} Prevention
- Fail fast on missing API keys; surface 401 as config, not runtime, errors
- Validate schema shapes against Exa's validator (anyOf for nullability) before sending
- Back off on 429 and monitor quota consumption per batch
When it happens
Trigger: 401 from a missing or typo'd Exa API key; 400 when the summary schema uses a form Exa's validator rejects — notably JSON-Schema type arrays like ['number','null'] instead of anyOf; 429 rate limit or exhausted credits during burst scraping; 5xx provider incidents.
Common situations: API key rotated but the env var not updated; schema evolution introducing nullable union types; a large scrape run draining the plan quota mid-batch.
Understand the failure class
Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.
Related errors
- Firecrawl scrape failed: HTTP ${resp.status}
- Firecrawl search failed: HTTP ${resp.status}
- ${label} HTTP ${response.status}
- ${operation} HTTP ${status}: ${safeCode}
- get-country-intel-brief HTTP ${res.status}${code ? `: ${code
AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21).
Data as JSON: /api/errors/09e04f2adfdc9fce.
Report an issue: GitHub.