koala73/worldmonitor · error

Exa search failed HTTP ${resp.status}: ${text.slice(0, 120)}

Error message

Exa search failed HTTP ${resp.status}: ${text.slice(0, 120)}

What it means

The exa-search adapter's raw Exa call (POST with x-api-key and a 15s AbortSignal timeout) received a non-2xx status; the message embeds both the code and the first 120 characters of the response body. Exa reports bad keys (401), rate limits (429), and validator rejections (400 INVALID_REQUEST_BODY) this way, and the body slice is the only place the actual reason appears.

Source

Thrown at consumer-prices-core/src/adapters/exa-search.ts:177

          query: `What is the retail price of this product? State amount and ISO currency code (e.g. ${currency} 12.50).`,
        },
      },
    };

    const resp = await fetch('https://api.exa.ai/search', {
      method: 'POST',
      headers: {
        'x-api-key': this.apiKey,
        'Content-Type': 'application/json',
        'User-Agent': CHROME_UA,
      },
      body: JSON.stringify(body),
      signal: AbortSignal.timeout(15_000),
    });

    if (!resp.ok) {
      const text = await resp.text().catch(() => '');
      throw new Error(`Exa search failed HTTP ${resp.status}: ${text.slice(0, 120)}`);
    }

    const data = (await resp.json()) as { results?: ExaResult[] };
    const exaResults = data.results ?? [];

    const payload: SearchPayload = {
      exaResults,
      basketSlug,
      itemCategory: target.category,
      canonicalName,
    };

    // Firecrawl fallback: when all Exa summaries fail price extraction,
    // scrape the first result URL directly (JS-rendered pages expose prices in markdown).
    const anyExaPrice = exaResults.some(
      (r) => matchPrice(r.summary ?? '', currency) !== null || matchPrice(r.title ?? '', currency) !== null,
    );

View on GitHub (pinned to eeab0a219f)

Solutions

  1. Read the embedded body slice first — it names the exact auth, quota, or validator complaint
  2. 401/403: fix EXA_API_KEY; 429: back off and reduce request concurrency
  3. 400: fix the request/schema shape — for Exa extraction schemas use anyOf for nullability, never type arrays
  4. 5xx: retry with backoff; Exa incidents usually clear within minutes

Example fix

// before — schema encoding Firecrawl accepts but Exa rejects (HTTP 400)
const schema = { price: { type: ['number', 'null'] } };

// after — Exa-compatible nullable encoding
const schema = { price: { anyOf: [{ type: 'number' }, { type: 'null' }] } };
Defensive patterns

Strategy: retry

Validate before calling

// cheap preflight: authenticated liveness call before the run
const resp = await fetch('https://api.exa.ai/search', {
  method: 'POST',
  headers: { 'x-api-key': key, 'Content-Type': 'application/json' },
  body: JSON.stringify({ query: 'ping', numResults: 0 }),
});
if (resp.status === 401 || resp.status === 403) {
  throw new Error('EXA_API_KEY rejected — fix credentials before the run');
}

Type guard

function exaStatus(err: unknown): number | null {
  const m = err instanceof Error ? err.message.match(/^Exa search failed HTTP (\d+):/) : null;
  return m ? Number(m[1]) : null;
}

Try / catch

try {
  await exaCall();
} catch (err) {
  const status = exaStatus(err);
  if (status === 429 || (status !== null && status >= 500)) {
    await sleep(backoffMs);
    return await exaCall(); // transient
  }
  throw err; // 4xx (except 429) is a request bug — retrying cannot help
}

Prevention

When it happens

Trigger: 401 with a wrong EXA_API_KEY; 429 when the Exa rate limit is hit during basket discovery; 400 INVALID_REQUEST_BODY when the request JSON violates Exa's validator — notably a JSON Schema using array type unions, which Exa rejects but Firecrawl accepts (#6182); 5xx during an Exa incident.

Common situations: One Exa key shared across too many workers so 429s cluster at run start; porting a Firecrawl-validated schema to Exa verbatim so every call 400s; a key that expired after rotation.

Related errors


AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21). Data as JSON: /api/errors/da358a6bbec36eaa. Report an issue: GitHub.