koala73/worldmonitor · error

Exa returned no content for ${url}

Error message

Exa returned no content for ${url}

What it means

ExaProvider.fetch calls exa-js getContents([url]) and indexes results[0]. Exa answered HTTP 200 but returned an empty results array — it has no cached or crawlable content for that exact URL — so the provider throws rather than fabricate an empty page (html/markdown stay coupled in FetchResult).

Source

Thrown at consumer-prices-core/src/acquisition/exa.ts:23

  readonly name = 'exa' as const;

  private readonly apiKey: string;
  private readonly baseUrl = 'https://api.exa.ai';
  private client: Exa;

  constructor(apiKey: string) {
    this.apiKey = apiKey;
    this.client = new Exa(apiKey);
  }

  async fetch(url: string, _opts: FetchOptions = {}): Promise<FetchResult> {
    const result = await this.client.getContents([url], {
      text: { maxCharacters: 100_000 },
      highlights: { numSentences: 5, highlightsPerUrl: 3 },
    });

    const item = result.results[0];
    if (!item) throw new Error(`Exa returned no content for ${url}`);

    return {
      url,
      html: item.text ?? '',
      markdown: item.text ?? '',
      statusCode: 200,
      provider: this.name,
      fetchedAt: new Date(),
      metadata: { highlights: item.highlights },
    };
  }

  async search(query: string, opts: SearchOptions = {}): Promise<SearchResult[]> {
    const result = await this.request<{
      results?: Array<{
        url: string;
        title?: string;
        text?: string;

View on GitHub (pinned to eeab0a219f)

Solutions

  1. Verify the URL loads in a browser to rule out a typo or 404
  2. Fall back to another acquisition provider for that URL (e.g. Firecrawl scrape) instead of retrying Exa
  3. Normalize to the canonical URL (strip tracking params) or submit the URL to Exa for crawling
  4. Treat it as a per-URL condition: skip and continue the batch, not a provider outage

Example fix

// before
const result = await exa.fetch(url);   // throws 'Exa returned no content for ...'

// after — degrade to the scrape provider for this URL only
let result;
try {
  result = await exa.fetch(url);
} catch (err) {
  if (!/returned no content/.test(err.message)) throw err;
  result = await firecrawl.fetch(url);
}
Defensive patterns

Strategy: fallback

Validate before calling

// Cheap liveness check before spending an Exa call
async function urlLooksLive(url) {
  try { const res = await fetch(url, { method: 'HEAD' }); return res.status < 400; }
  catch { return false; }
}

Type guard

interface ExaContentsResult { results?: Array<{ text?: string; highlights?: string[] }> }
function hasContentRow(r: ExaContentsResult | undefined): r is { results: NonNullable<ExaContentsResult['results']> } {
  return Array.isArray(r?.results) && r.results.length > 0;
}

Try / catch

try {
  return await exa.fetch(url);
} catch (err) {
  if (err instanceof Error && /returned no content/.test(err.message)) {
    return await firecrawl.fetch(url);      // per-URL fallback, provider stays healthy
  }
  throw err;                                 // auth/quota/HTTP errors are not content gaps
}

Prevention

When it happens

Trigger: URL unknown to Exa's index (freshly published page, obscure site, typo'd URL); paywalled or robots-excluded content Exa will not serve; a URL whose canonical form differs from the requested one, so Exa stores it under another key.

Common situations: Scraping newly published product pages; deep product URLs carrying session or tracking parameters; niche retailer domains with thin Exa coverage.

Related errors


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