koala73/worldmonitor · error

P0 search failed: HTTP ${resp.status}

Error message

P0 search failed: HTTP ${resp.status}

What it means

P0Provider.search() throws when POST {P0_BASE_URL}/search returns a non-2xx status. The search endpoint takes query, num_results (default 10) and include_domains with x-api-key auth. Unlike scrape, no fallback path is wired for search — Exa is the pipeline's primary search provider — so an HTTP failure here propagates straight to the caller.

Source

Thrown at consumer-prices-core/src/acquisition/p0.ts:82

      markdown: data.markdown,
      statusCode: data.statusCode ?? 200,
      provider: this.name,
      fetchedAt: new Date(),
    };
  }

  async search(query: string, opts: SearchOptions = {}): Promise<SearchResult[]> {
    const resp = await fetch(`${this.baseUrl}/search`, {
      method: 'POST',
      headers: this.headers(),
      body: JSON.stringify({
        query,
        num_results: opts.numResults ?? 10,
        include_domains: opts.includeDomains,
      }),
    });

    if (!resp.ok) throw new Error(`P0 search failed: HTTP ${resp.status}`);

    const data = (await resp.json()) as P0SearchResponse;
    return (data.results ?? []).map((r) => ({
      url: r.url,
      title: r.title,
      text: r.snippet,
    }));
  }

  async validate(): Promise<boolean> {
    try {
      const resp = await fetch(`${this.baseUrl}/health`, {
        headers: this.headers(),
        signal: AbortSignal.timeout(5_000),
      });
      return resp.ok;
    } catch {
      return false;

View on GitHub (pinned to eeab0a219f)

Solutions

  1. Map the status code to auth (401/403), quota/rate (402/429), or outage (5xx) and fix accordingly
  2. Verify P0_BASE_URL ends in the correct API version path
  3. Route search through Exa — the provider the pipeline is designed around — and keep P0 for scraping only
  4. Retry with backoff on 429/5xx before giving up

Example fix

// before
const results = await p0.search(query);

// after — retry transient statuses, fail fast on auth
const results = await retryOn(() => p0.search(query), {
  retries: 2,
  if: (e) => /HTTP (429|5\d\d)/.test(String(e?.message)),
});
Defensive patterns

Strategy: retry

Validate before calling

// probe the provider before relying on search
if (!(await p0.validate())) {
  throw new Error('P0 /health failed — do not route search through it');
}

Type guard

function isP0SearchHttpError(err: unknown): boolean {
  return err instanceof Error && /^P0 search failed: HTTP \d+$/.test(err.message);
}

Try / catch

try {
  return await p0.search(q, opts);
} catch (err) {
  if (isP0SearchHttpError(err) && /HTTP (429|5\d\d)/.test((err as Error).message)) {
    await sleep(2_000);
    return await p0.search(q, opts); // transient only
  }
  throw err; // 401/403/4xx is permanent — surface it
}

Prevention

When it happens

Trigger: 401/403 invalid P0_API_KEY on /search; 429 when search quota is exhausted; 5xx during a P0 outage; P0_BASE_URL misconfigured so /search resolves to a wrong path or gateway.

Common situations: Using P0 for search without provisioning search on the plan; P0_API_KEY rotated but only some workers restarted; a gateway returning 502 between the worker and P0.

Related errors


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