{"record":{"id":"07c7b91bf137004e","repo":"koala73/worldmonitor","slug":"p0-scrape-failed-http-resp-status","errorCode":null,"errorMessage":"P0 scrape failed: HTTP ${resp.status}","messagePattern":"P0 scrape failed: HTTP (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"consumer-prices-core/src/acquisition/p0.ts","lineNumber":54,"sourceCode":"    };\n  }\n\n  async fetch(url: string, opts: FetchOptions = {}): Promise<FetchResult> {\n    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`, {","sourceCodeStart":36,"sourceCodeEnd":72,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/consumer-prices-core/src/acquisition/p0.ts#L36-L72","documentation":"P0Provider.fetch() throws when POST {P0_BASE_URL}/scrape (default https://api.parallelai.dev/v1/scrape) returns a non-2xx status. P0 is the JS-rendering, anti-bot, premium-proxy scraping provider; the request sends x-api-key auth with render_js:true, premium_proxy:true, and an AbortSignal deadline 10s above the render budget. Any HTTP-level rejection — bad key, quota, malformed body, provider outage — surfaces here with the raw status code.","triggerScenarios":"401/403 with an invalid or revoked P0_API_KEY; 402/429 when the plan is exhausted or rate-limited; 4xx for a scrape body P0 rejects; 5xx during a P0 outage; P0_BASE_URL pointing at the wrong host or path so every call hits a gateway error.","commonSituations":"P0_API_KEY rotated or expired so every scrape in a retailer run 401s; a new environment pointing P0_BASE_URL at a different stage; rate limits hit because render_js + premium_proxy calls are expensive per request; registry.fetchWithFallback logs '[acquisition] p0 failed ... Falling back to ...' and this error re-throws only when no fallback is configured.","solutions":["Map the status: 401/403 fix P0_API_KEY, 402/429 check plan/quota, 5xx retry later","Set acquisition.fallback (e.g. 'playwright' or 'firecrawl') in the retailer config so fetchWithFallback absorbs the failure","Verify P0_BASE_URL resolves to the intended environment and API version","Confirm the timeout budget (opts.timeout, sent as floor(ms/1000) seconds) is within what the plan allows"],"exampleFix":"// before — single provider, one HTTP failure kills the target\nconst r = await getProvider('p0').fetch(url);\n\n// after — declare a fallback chain in the retailer acquisition config\nconst acquisition = { provider: 'p0', fallback: 'playwright', options: { waitForSelector: '.price' } };\nconst r = await fetchWithFallback(url, acquisition);","handlingStrategy":"fallback","validationCode":"// fail fast before the run: provider reachable?\nif (!(await getProvider('p0').validate())) { // GET /health\n  throw new Error('P0 unavailable — configure acquisition.fallback before scraping');\n}","typeGuard":"function isP0HttpError(err: unknown): boolean {\n  return err instanceof Error && /^P0 scrape failed: HTTP \\d+$/.test(err.message);\n}","tryCatchPattern":"try {\n  return await p0.fetch(url, opts);\n} catch (err) {\n  if (isP0HttpError(err)) {\n    return await getProvider('playwright').fetch(url, opts); // mirror registry.fetchWithFallback\n  }\n  throw err;\n}","preventionTips":["Always configure acquisition.fallback for P0-backed retailers — fetchWithFallback exists for exactly this shape","Run P0Provider.validate() (GET /health) at startup and drop P0 from the chain when it fails","Keep P0_API_KEY and P0_BASE_URL in one env source so they rotate together"],"tags":["p0","web-scraping","http-status","api-response","rate-limit"],"backgroundTag":"http-api-error","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-08-23T13:39:53.451Z"}