heygen-com/hyperframes · error

Website capture blocked: the loaded page matched an access-p

Error message

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.

What it means

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.

Source

Thrown at packages/cli/src/capture/index.ts:311

        "post-navigation content check timed out; continuing with HTTP-status blocked-page detection only";
      warnings.push(message);
      progress("warn", message);
    }

    const blockedReason = detectBlockedPage({
      httpStatus: navigationResponse?.status() ?? null,
      ...(contentCheckTimedOut
        ? {
            title: "",
            textLength: 0,
            bodyChildCount: 0,
            hasChallengeElement: false,
          }
        : pageContentCheck),
    });
    if (blockedReason) {
      phase("navigation", "degraded", "blocked");
      throw new Error(blockedReason);
    }

    phase("navigation", "completed");
    phase("core-extraction", "started");

    if (!contentCheckTimedOut && pageContentCheck.textLength < 100) {
      const reason =
        "Page has very little text content (" +
        pageContentCheck.textLength +
        " chars) — may be blocked or a client-rendered SPA that needs more time";
      warnings.push(reason);
      progress("warn", reason);
    }

    const lazyLoadBudgetMs = Math.min(15_000, remainingMs());
    const lazyScroll = await lazyScrollForCapture(page1, lazyLoadBudgetMs, {
      onWarning: (message) => {
        warnings.push(message);

View on GitHub (pinned to c2996c8626)

Solutions

  1. 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.
  2. If the site is yours, allowlist the runner's egress IP or User-Agent, or disable the bot rule for the capture path.
  3. Bypass capture entirely: provide source assets directly (screenshots/HTML) via the relevant --source / assets flags so no live navigation runs.
  4. Increase navigation wait / disable challenge wait only if you control the site and know the challenge is benign.
  5. 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.

Example fix

// before: capture from a blocked data-center runner
hyperframes product-launch https://app.example.com

// after: capture locally on an allowlisted network, or hand in assets
hyperframes product-launch ./local-app-snapshot --source assets/
Defensive patterns

Strategy: fallback

Validate before calling

// Pre-flight: HEAD the target from the runner's egress to anticipate a block
async function looksBlocked(url: string): Promise<boolean> {
  const res = await fetch(url, { redirect: 'follow' });
  const blocked = [401, 403, 429, 503].includes(res.status);
  const ct = res.headers.get('content-type') ?? '';
  return blocked || ct.includes('text/html') && /challenge|captcha|datadome|cloudflare/i.test(await res.text());
}

Try / catch

try {
  await captureSite(url);
} catch (err) {
  if (/Website capture blocked/.test(String(err?.message))) {
    // Fall back to user-provided source assets instead of live capture
    return captureFromAssets(localAssetDir);
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of heygen-com/hyperframes@c2996c8626 (2026-08-12). Data as JSON: /api/errors/064b975a590370a2. Report an issue: GitHub.