{"record":{"id":"064b975a590370a2","repo":"heygen-com/hyperframes","slug":"website-capture-blocked-the-loaded-page-matched-a","errorCode":null,"errorMessage":"Website capture blocked: the loaded page matched an access-protection response (${statusDetail}, title ${JSON.stringify(evidence.title)}, ${evidence.textLength} text chars). The site may reject automated or data-center traffic; retry from an allowed network or provide source assets directly.","messagePattern":"Website capture blocked: the loaded page matched an access-protection response \\((.+?), title (.+?), (.+?) text chars\\)\\. The site may reject automated or data-center traffic; retry from an allowed network or provide source assets directly\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/capture/index.ts","lineNumber":311,"sourceCode":"        \"post-navigation content check timed out; continuing with HTTP-status blocked-page detection only\";\n      warnings.push(message);\n      progress(\"warn\", message);\n    }\n\n    const blockedReason = detectBlockedPage({\n      httpStatus: navigationResponse?.status() ?? null,\n      ...(contentCheckTimedOut\n        ? {\n            title: \"\",\n            textLength: 0,\n            bodyChildCount: 0,\n            hasChallengeElement: false,\n          }\n        : pageContentCheck),\n    });\n    if (blockedReason) {\n      phase(\"navigation\", \"degraded\", \"blocked\");\n      throw new Error(blockedReason);\n    }\n\n    phase(\"navigation\", \"completed\");\n    phase(\"core-extraction\", \"started\");\n\n    if (!contentCheckTimedOut && pageContentCheck.textLength < 100) {\n      const reason =\n        \"Page has very little text content (\" +\n        pageContentCheck.textLength +\n        \" chars) — may be blocked or a client-rendered SPA that needs more time\";\n      warnings.push(reason);\n      progress(\"warn\", reason);\n    }\n\n    const lazyLoadBudgetMs = Math.min(15_000, remainingMs());\n    const lazyScroll = await lazyScrollForCapture(page1, lazyLoadBudgetMs, {\n      onWarning: (message) => {\n        warnings.push(message);","sourceCodeStart":293,"sourceCodeEnd":329,"githubUrl":"https://github.com/heygen-com/hyperframes/blob/c2996c8626135db5253519359d8a063d3bafad8d/packages/cli/src/capture/index.ts#L293-L329","documentation":"Thrown by the website-capture pipeline after Puppeteer navigation when the loaded page is classified as an access-protection response (bot-challenge, WAF block, interstitial). The deciding function inspects httpStatus plus page evidence (title, textLength, bodyChildCount, hasChallengeElement); when it returns a blockedReason, the 'navigation' phase is marked degraded/blocked and this error fires. It is distinct from the soft 'very little text' warning that fires afterward for client-rendered SPAs.","triggerScenarios":"Capturing a site whose response trips the blocker heuristics: a Cloudflare/Datadome/PerimeterX challenge page, a 403/429/503 from a data-center IP, or a login wall returning minimal body content. Also when contentCheckTimedOut is true the evidence is zeroed, which can still trip the classifier if the status itself looks protected.","commonSituations":"Running hyperframes capture from CI / a cloud VM whose egress IP is on a blocklist; capturing a site behind corporate SSO; a target that geo-blocks the region the runner is in; an ephemeral challenge that appears only under headless Chrome.","solutions":["Retry the capture from an allowlisted network (residential IP, allowed data center, or corporate network) or run a local capture and point the CLI at local HTML instead.","If the site is yours, allowlist the runner's egress IP or User-Agent, or disable the bot rule for the capture path.","Bypass capture entirely: provide source assets directly (screenshots/HTML) via the relevant --source / assets flags so no live navigation runs.","Increase navigation wait / disable challenge wait only if you control the site and know the challenge is benign.","Confirm it is actually a block and not a transient outage: re-run once, then inspect the statusDetail + title in the message to classify the protection vendor."],"exampleFix":"// before: capture from a blocked data-center runner\nhyperframes product-launch https://app.example.com\n\n// after: capture locally on an allowlisted network, or hand in assets\nhyperframes product-launch ./local-app-snapshot --source assets/","handlingStrategy":"fallback","validationCode":"// Pre-flight: HEAD the target from the runner's egress to anticipate a block\nasync function looksBlocked(url: string): Promise<boolean> {\n  const res = await fetch(url, { redirect: 'follow' });\n  const blocked = [401, 403, 429, 503].includes(res.status);\n  const ct = res.headers.get('content-type') ?? '';\n  return blocked || ct.includes('text/html') && /challenge|captcha|datadome|cloudflare/i.test(await res.text());\n}","typeGuard":null,"tryCatchPattern":"try {\n  await captureSite(url);\n} catch (err) {\n  if (/Website capture blocked/.test(String(err?.message))) {\n    // Fall back to user-provided source assets instead of live capture\n    return captureFromAssets(localAssetDir);\n  }\n  throw err;\n}","preventionTips":["Allowlist the capture runner's egress IP with the target site's WAF/bot-protection vendor.","For sites you do not control, prefer supplying source assets directly over live capture.","Run capture from a residential/allowlisted network in CI, not arbitrary cloud egress."],"tags":["network","capture","bot-protection","headless-browser"],"backgroundTag":null,"analyzedSha":"c2996c8626135db5253519359d8a063d3bafad8d","analyzedAt":"2026-08-12T22:18:56.877Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}