{"record":{"id":"f391e53c52f6847b","repo":"koala73/worldmonitor","slug":"p0-error-data-error-no-content","errorCode":null,"errorMessage":"P0 error: ${data.error ?? 'no content'}","messagePattern":"P0 error: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"consumer-prices-core/src/acquisition/p0.ts","lineNumber":58,"sourceCode":"    const resp = await fetch(`${this.baseUrl}/scrape`, {\n      method: 'POST',\n      headers: this.headers(),\n      body: JSON.stringify({\n        url,\n        render_js: true,\n        wait_for: opts.waitForSelector,\n        timeout: Math.floor((opts.timeout ?? 30_000) / 1_000),\n        output_format: 'html',\n        premium_proxy: true,\n      }),\n      signal: AbortSignal.timeout((opts.timeout ?? 30_000) + 10_000),\n    });\n\n    if (!resp.ok) throw new Error(`P0 scrape failed: HTTP ${resp.status}`);\n\n    const data = (await resp.json()) as P0ScrapeResponse;\n    if (!data.success && !data.html) {\n      throw new Error(`P0 error: ${data.error ?? 'no content'}`);\n    }\n\n    return {\n      url,\n      html: data.html ?? '',\n      markdown: data.markdown,\n      statusCode: data.statusCode ?? 200,\n      provider: this.name,\n      fetchedAt: new Date(),\n    };\n  }\n\n  async search(query: string, opts: SearchOptions = {}): Promise<SearchResult[]> {\n    const resp = await fetch(`${this.baseUrl}/search`, {\n      method: 'POST',\n      headers: this.headers(),\n      body: JSON.stringify({\n        query,","sourceCodeStart":40,"sourceCodeEnd":76,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/consumer-prices-core/src/acquisition/p0.ts#L40-L76","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Read the interpolated data.error — it distinguishes render timeout from blocked-by-target from no content","Raise opts.timeout (it becomes seconds) or drop waitForSelector when the selector is unreliable on that retailer","If one whole domain is blocked, move that retailer to another provider via acquisition.fallback","Verify wait selectors against the live page before encoding them in config"],"exampleFix":"// before — default 30s budget with a selector that rarely resolves\nawait p0.fetch(url, { waitForSelector: '#price' });\n\n// after — larger render budget; selector stays best-effort\nawait p0.fetch(url, { waitForSelector: '#price', timeout: 60_000 });","handlingStrategy":"fallback","validationCode":"// sanity-check the render budget before sending: P0 takes floor(ms/1000) seconds\nconst secs = Math.floor((opts.timeout ?? 30_000) / 1000);\nif (secs < 5) throw new RangeError('P0 render budget too small — raise opts.timeout');","typeGuard":"function isP0ContentError(err: unknown): boolean {\n  return err instanceof Error && err.message.startsWith('P0 error:');\n}","tryCatchPattern":"try {\n  return await p0.fetch(url, opts);\n} catch (err) {\n  if (isP0ContentError(err)) {\n    // in-band scrape failure: retry once selector-free, then fall back\n    return await p0.fetch(url, { ...opts, waitForSelector: undefined });\n  }\n  throw err;\n}","preventionTips":["Treat selector waits as best-effort: retry once without waitForSelector before abandoning","Track P0 success rate per domain — a sudden block shows up as success:false clusters on one retailer","Remember the ms-to-seconds conversion: P0 takes floor(timeout/1000), so tiny budgets fail instantly"],"tags":["p0","web-scraping","render-timeout","anti-bot","selector"],"backgroundTag":"web-scraping-blocked","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-08-23T16:17:53.355Z"}