Files
gh-jezweb-claude-skills-ski…/references/common-errors.md
2025-11-30 08:24:08 +08:00

15 KiB

Common Errors and Solutions

Complete reference for all known Browser Rendering errors with sources, root causes, and solutions.


Error 1: "Cannot read properties of undefined (reading 'fetch')"

Full Error:

TypeError: Cannot read properties of undefined (reading 'fetch')

Source: https://developers.cloudflare.com/browser-rendering/faq/#cannot-read-properties-of-undefined-reading-fetch

Root Cause: Browser binding not passed to puppeteer.launch()

Why It Happens:

// ❌ Missing browser binding
const browser = await puppeteer.launch();
//                                    ^ undefined - no binding passed!

Solution:

// ✅ Pass browser binding
const browser = await puppeteer.launch(env.MYBROWSER);
//                                    ^^^^^^^^^^^^^^^^ binding from env

Prevention: Always pass env.MYBROWSER (or your configured binding name) to puppeteer.launch().


Error 2: XPath Selector Not Supported

Full Error:

Error: XPath selectors are not supported in Browser Rendering

Source: https://developers.cloudflare.com/browser-rendering/faq/#why-cant-i-use-an-xpath-selector-when-using-browser-rendering-with-puppeteer

Root Cause: XPath poses security risk to Workers

Why It Happens:

// ❌ XPath selectors not directly supported
const elements = await page.$x('/html/body/div/h1');

Solution 1: Use CSS Selectors

// ✅ Use CSS selector instead
const element = await page.$("div > h1");
const elements = await page.$$("div > h1");

Solution 2: Use XPath in page.evaluate()

// ✅ Use XPath inside page.evaluate()
const innerHtml = await page.evaluate(() => {
  return (
    // @ts-ignore - runs in browser context
    new XPathEvaluator()
      .createExpression("/html/body/div/h1")
      // @ts-ignore
      .evaluate(document, XPathResult.FIRST_ORDERED_NODE_TYPE)
      .singleNodeValue.innerHTML
  );
});

Prevention: Use CSS selectors by default. Only use XPath via page.evaluate() if absolutely necessary.


Error 3: Browser Timeout

Full Error:

Error: Browser session closed due to inactivity

Source: https://developers.cloudflare.com/browser-rendering/platform/limits/#note-on-browser-timeout

Root Cause: Default 60 second idle timeout

Why It Happens:

  • No devtools commands sent for 60 seconds
  • Browser automatically closes to free resources

Solution: Extend Timeout

// ✅ Extend timeout to 5 minutes
const browser = await puppeteer.launch(env.MYBROWSER, {
  keep_alive: 300000  // 5 minutes = 300,000 ms
});

Maximum: 600,000ms (10 minutes)

Use Cases for Extended Timeout:

  • Multi-step workflows
  • Long-running scraping
  • Session reuse across requests

Prevention: Only extend if actually needed. Longer timeout = more billable hours.


Error 4: Rate Limit Exceeded

Full Error:

Error: Rate limit exceeded. Too many concurrent browsers.

Source: https://developers.cloudflare.com/browser-rendering/platform/limits/

Root Cause: Exceeded concurrent browser limit

Limits:

  • Free tier: 3 concurrent browsers
  • Paid tier: 10-30 concurrent browsers

Solution 1: Check Limits Before Launching

const limits = await puppeteer.limits(env.MYBROWSER);

if (limits.allowedBrowserAcquisitions === 0) {
  return new Response(
    JSON.stringify({
      error: "Rate limit reached",
      retryAfter: limits.timeUntilNextAllowedBrowserAcquisition
    }),
    { status: 429 }
  );
}

const browser = await puppeteer.launch(env.MYBROWSER);

Solution 2: Reuse Sessions

// Try to connect to existing session first
const sessions = await puppeteer.sessions(env.MYBROWSER);
const freeSession = sessions.find(s => !s.connectionId);

if (freeSession) {
  try {
    return await puppeteer.connect(env.MYBROWSER, freeSession.sessionId);
  } catch {
    // Session closed, launch new
  }
}

return await puppeteer.launch(env.MYBROWSER);

Solution 3: Use Multiple Tabs

// ❌ Bad: 10 browsers
for (const url of urls) {
  const browser = await puppeteer.launch(env.MYBROWSER);
  // ...
}

// ✅ Good: 1 browser, 10 tabs
const browser = await puppeteer.launch(env.MYBROWSER);
await Promise.all(urls.map(async url => {
  const page = await browser.newPage();
  // ...
  await page.close();
}));
await browser.close();

Prevention: Monitor concurrency usage, implement session reuse, use tabs instead of multiple browsers.


Error 5: Local Development Request Size Limit

Full Error:

Error: Request payload too large (>1MB)

Source: https://developers.cloudflare.com/browser-rendering/faq/#does-local-development-support-all-browser-rendering-features

Root Cause: Local development limitation (requests >1MB fail)

Solution: Use Remote Binding

// wrangler.jsonc
{
  "browser": {
    "binding": "MYBROWSER",
    "remote": true  // ← Use real headless browser during dev
  }
}

With Remote Binding:

  • Connects to actual Cloudflare browser (not local simulation)
  • No 1MB request limit
  • Counts toward your quota

Prevention: Enable remote: true for local development if working with large payloads.


Error 6: Bot Protection Triggered

Full Error:

Blocked by bot protection / CAPTCHA challenge

Source: https://developers.cloudflare.com/browser-rendering/faq/#will-browser-rendering-bypass-cloudflares-bot-protection

Root Cause: Browser Rendering requests always identified as bots

Why It Happens:

  • Cloudflare automatically identifies Browser Rendering traffic
  • Cannot bypass bot protection
  • Automatic headers added: cf-biso-request-id, cf-biso-devtools

Solution (If Scraping Your Own Zone): Create WAF skip rule:

  1. Go to Security > WAF > Custom rules
  2. Create skip rule with custom header:
    • Header: X-Custom-Auth
    • Value: your-secret-token
  3. Add header in your Worker:
const browser = await puppeteer.launch(env.MYBROWSER);
const page = await browser.newPage();

// Set custom header
await page.setExtraHTTPHeaders({
  "X-Custom-Auth": "your-secret-token"
});

await page.goto(url);

Solution (If Scraping External Sites):

  • Cannot bypass bot protection
  • Some sites will block Browser Rendering traffic
  • Consider using site's official API instead

Prevention: Use official APIs when available. Only scrape your own zones if possible.


Error 7: Navigation Timeout

Full Error:

TimeoutError: Navigation timeout of 30000 ms exceeded

Root Cause: Page failed to load within timeout

Why It Happens:

  • Slow website
  • Large page assets
  • Network issues
  • Page never reaches desired load state

Solution 1: Increase Timeout

await page.goto(url, {
  timeout: 60000  // 60 seconds
});

Solution 2: Change Wait Condition

// ❌ Strict (waits for all network requests)
await page.goto(url, { waitUntil: "networkidle0" });

// ✅ More lenient (waits for DOMContentLoaded)
await page.goto(url, { waitUntil: "domcontentloaded" });

// ✅ Most lenient (waits for load event only)
await page.goto(url, { waitUntil: "load" });

Solution 3: Handle Timeout Gracefully

try {
  await page.goto(url, { timeout: 30000 });
} catch (error) {
  if (error instanceof Error && error.name === "TimeoutError") {
    console.log("Navigation timeout, taking screenshot anyway");
    const screenshot = await page.screenshot();
    return screenshot;
  }
  throw error;
}

Prevention: Set appropriate timeouts for your use case. Use lenient wait conditions for slow sites.


Error 8: Memory Limit Exceeded

Full Error:

Error: Browser exceeded its memory limit

Root Cause: Page too large or too many tabs open

Why It Happens:

  • Opening many tabs simultaneously
  • Large pages with many assets
  • Memory leaks from not closing pages

Solution 1: Close Pages

const page = await browser.newPage();
// ... use page ...
await page.close();  // ← Don't forget!

Solution 2: Limit Concurrent Tabs

import PQueue from "p-queue";

const browser = await puppeteer.launch(env.MYBROWSER);
const queue = new PQueue({ concurrency: 5 }); // Max 5 tabs

await Promise.all(urls.map(url =>
  queue.add(async () => {
    const page = await browser.newPage();
    await page.goto(url);
    // ...
    await page.close();
  })
));

Solution 3: Use Smaller Viewports

await page.setViewport({
  width: 1280,
  height: 720  // Smaller than default
});

Prevention: Always close pages when done. Limit concurrent tabs. Process URLs in batches.


Error 9: Failed to Connect to Session

Full Error:

Error: Failed to connect to browser session

Root Cause: Session closed between .sessions() and .connect() calls

Why It Happens:

  • Session timed out (60s idle)
  • Session closed by another Worker
  • Session terminated unexpectedly

Solution: Handle Connection Failures

const sessions = await puppeteer.sessions(env.MYBROWSER);
const freeSession = sessions.find(s => !s.connectionId);

if (freeSession) {
  try {
    const browser = await puppeteer.connect(env.MYBROWSER, freeSession.sessionId);
    return browser;
  } catch (error) {
    console.log("Failed to connect to session, launching new browser");
  }
}

// Fall back to launching new browser
return await puppeteer.launch(env.MYBROWSER);

Prevention: Always wrap puppeteer.connect() in try-catch. Have fallback to puppeteer.launch().


Error 10: Too Many Requests Per Minute

Full Error:

Error: Too many browser launches per minute

Root Cause: Exceeded "new browsers per minute" limit

Limits:

  • Free tier: 3 per minute (1 every 20 seconds)
  • Paid tier: 30 per minute (1 every 2 seconds)

Solution: Implement Rate Limiting

async function launchWithRateLimit(env: Env): Promise<Browser> {
  const limits = await puppeteer.limits(env.MYBROWSER);

  if (limits.allowedBrowserAcquisitions === 0) {
    const delay = limits.timeUntilNextAllowedBrowserAcquisition || 2000;
    console.log(`Rate limited, waiting ${delay}ms`);
    await new Promise(resolve => setTimeout(resolve, delay));
  }

  return await puppeteer.launch(env.MYBROWSER);
}

Prevention: Check limits before launching. Implement exponential backoff. Reuse sessions instead of launching new browsers.


Error 11: Binding Not Configured

Full Error:

Error: Browser binding not found

Root Cause: Browser binding not configured in wrangler.jsonc

Solution: Add Browser Binding

// wrangler.jsonc
{
  "browser": {
    "binding": "MYBROWSER"
  },
  "compatibility_flags": ["nodejs_compat"]
}

Also Add to TypeScript Types:

interface Env {
  MYBROWSER: Fetcher;
}

Prevention: Always configure browser binding and nodejs_compat flag.


Error 12: nodejs_compat Flag Missing

Full Error:

Error: Node.js APIs not available

Root Cause: nodejs_compat compatibility flag not enabled

Solution: Add Compatibility Flag

// wrangler.jsonc
{
  "compatibility_flags": ["nodejs_compat"]
}

Why It's Required: Browser Rendering needs Node.js APIs and polyfills to work.

Prevention: Always include nodejs_compat when using Browser Rendering.


Error Handling Template

Complete error handling for production use:

import puppeteer, { Browser } from "@cloudflare/puppeteer";

interface Env {
  MYBROWSER: Fetcher;
}

async function withBrowser<T>(
  env: Env,
  fn: (browser: Browser) => Promise<T>
): Promise<T> {
  let browser: Browser | null = null;

  try {
    // Check limits
    const limits = await puppeteer.limits(env.MYBROWSER);
    if (limits.allowedBrowserAcquisitions === 0) {
      throw new Error(
        `Rate limit reached. Retry after ${limits.timeUntilNextAllowedBrowserAcquisition}ms`
      );
    }

    // Try to reuse session
    const sessions = await puppeteer.sessions(env.MYBROWSER);
    const freeSession = sessions.find(s => !s.connectionId);

    if (freeSession) {
      try {
        browser = await puppeteer.connect(env.MYBROWSER, freeSession.sessionId);
      } catch (error) {
        console.log("Failed to connect, launching new browser");
        browser = await puppeteer.launch(env.MYBROWSER);
      }
    } else {
      browser = await puppeteer.launch(env.MYBROWSER);
    }

    // Execute user function
    const result = await fn(browser);

    // Disconnect (keep session alive)
    await browser.disconnect();

    return result;
  } catch (error) {
    // Close on error
    if (browser) {
      await browser.close();
    }

    // Re-throw with context
    if (error instanceof Error) {
      error.message = `Browser operation failed: ${error.message}`;
    }
    throw error;
  }
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    try {
      const screenshot = await withBrowser(env, async (browser) => {
        const page = await browser.newPage();

        try {
          await page.goto("https://example.com", {
            waitUntil: "networkidle0",
            timeout: 30000
          });
        } catch (error) {
          if (error instanceof Error && error.name === "TimeoutError") {
            console.log("Navigation timeout, taking screenshot anyway");
          } else {
            throw error;
          }
        }

        return await page.screenshot();
      });

      return new Response(screenshot, {
        headers: { "content-type": "image/png" }
      });
    } catch (error) {
      console.error("Request failed:", error);

      return new Response(
        JSON.stringify({
          error: error instanceof Error ? error.message : "Unknown error"
        }),
        {
          status: 500,
          headers: { "content-type": "application/json" }
        }
      );
    }
  }
};

Debugging Checklist

When encountering browser errors:

  1. Check browser binding

    • Binding configured in wrangler.jsonc?
    • nodejs_compat flag enabled?
    • Binding passed to puppeteer.launch()?
  2. Check limits

    • Within concurrent browser limit?
    • Within new browsers/minute limit?
    • Call puppeteer.limits() to verify?
  3. Check timeouts

    • Navigation timeout appropriate?
    • Browser keep_alive set if needed?
    • Timeout errors handled gracefully?
  4. Check session management

    • browser.close() called on errors?
    • Pages closed when done?
    • Session reuse implemented correctly?
  5. Check network

    • Target URL accessible?
    • No CORS/bot protection issues?
    • Appropriate wait conditions used?

References


Last Updated: 2025-10-22