koala73/worldmonitor · error

P0 error: ${data.error ?? 'no content'}

Error message

P0 error: ${data.error ?? 'no content'}

What it means

Thrown by P0Provider.fetch() when POST /scrape answers HTTP 2xx but the JSON body has success:false AND no html payload. This is P0 reporting a scrape-level failure in-band: the render timed out, the proxy was blocked by the target, or the page never produced content — data.error carries P0's own message ('no content' when absent). Unlike the HTTP-status error, the transport was fine; the page acquisition itself failed.

Source

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

    const resp = await fetch(`${this.baseUrl}/scrape`, {
      method: 'POST',
      headers: this.headers(),
      body: JSON.stringify({
        url,
        render_js: true,
        wait_for: opts.waitForSelector,
        timeout: Math.floor((opts.timeout ?? 30_000) / 1_000),
        output_format: 'html',
        premium_proxy: true,
      }),
      signal: AbortSignal.timeout((opts.timeout ?? 30_000) + 10_000),
    });

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

    const data = (await resp.json()) as P0ScrapeResponse;
    if (!data.success && !data.html) {
      throw new Error(`P0 error: ${data.error ?? 'no content'}`);
    }

    return {
      url,
      html: data.html ?? '',
      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,

View on GitHub (pinned to eeab0a219f)

Solutions

  1. Read the interpolated data.error — it distinguishes render timeout from blocked-by-target from no content
  2. Raise opts.timeout (it becomes seconds) or drop waitForSelector when the selector is unreliable on that retailer
  3. If one whole domain is blocked, move that retailer to another provider via acquisition.fallback
  4. Verify wait selectors against the live page before encoding them in config

Example fix

// before — default 30s budget with a selector that rarely resolves
await p0.fetch(url, { waitForSelector: '#price' });

// after — larger render budget; selector stays best-effort
await p0.fetch(url, { waitForSelector: '#price', timeout: 60_000 });
Defensive patterns

Strategy: fallback

Validate before calling

// sanity-check the render budget before sending: P0 takes floor(ms/1000) seconds
const secs = Math.floor((opts.timeout ?? 30_000) / 1000);
if (secs < 5) throw new RangeError('P0 render budget too small — raise opts.timeout');

Type guard

function isP0ContentError(err: unknown): boolean {
  return err instanceof Error && err.message.startsWith('P0 error:');
}

Try / catch

try {
  return await p0.fetch(url, opts);
} catch (err) {
  if (isP0ContentError(err)) {
    // in-band scrape failure: retry once selector-free, then fall back
    return await p0.fetch(url, { ...opts, waitForSelector: undefined });
  }
  throw err;
}

Prevention

When it happens

Trigger: A JS-heavy page exceeding the render budget sent as floor(timeout/1000) seconds; anti-bot defenses defeating premium_proxy for a specific domain; a waitForSelector (opts.waitForSelector) that never appears on the page; P0 returning success:false with an empty error string.

Common situations: A retailer's CDN starts blocking P0 egress IPs so every product page on that domain returns success:false; a selector typo makes every scrape wait until timeout; a small opts.timeout truncating to near-zero seconds after the ms-to-s conversion so renders fail immediately.

Related errors


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