puppeteer

Library

puppeteer/puppeteer

JavaScript API for controlling Chrome and Firefox browsers.

Overview

Puppeteer provides a high-level API to control Chrome or Firefox over DevTools Protocol or WebDriver BiDi. It runs in headless mode by default and is widely used for browser automation, testing, and web scraping. It supports Node.js and offers tools for screenshot, PDF generation, and form interaction.

README Preview

# Puppeteer\n\n[](https://github.com/puppeteer/puppeteer/actions/workflows/ci.yml)\n[](https://npmjs.org/package/puppeteer)\n\n\n\n> Puppeteer is a JavaScript library which provides a high-level API to control\n> Chrome or Firefox over the\n> [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) or [WebDriver BiDi](https://pptr.dev/webdriver-bidi).\n> Puppeteer runs in the headless (no visible UI) by default\n\n## [Get started](https://pptr.dev/docs) | [API](https://pptr.dev/api) | [FAQ](https://pptr.dev/faq) | [Contributing](https://pptr.dev/contributing) | [Troubleshooting](https://pptr.dev/troubleshooting)\n\n## Installation\n\n```bash npm2yarn\nnpm i puppeteer # Downloads compatible Chrome during installation.\nnpm i puppeteer-core # Alternatively, install as a library, without downloading Chrome.\n```\n\n:::note\n\nModern package managers (including npm (see the [RFC](https://github.com/npm/rfcs/pull/868)), pnpm, Yarn, Bun, and Deno) block dependency install scripts by default. If the install script is blocked, Puppeteer will not download the browser during installation, leading to runtime errors.\n\nYou can manually download the required browsers after installation by running:\n\n```bash npm2yarn\nnpx puppeteer browsers install\n```\n\nAlternatively, you can configure your package manager to allow the install script to run (for example, with npm, by adding `"puppeteer"` to `"allowScripts"` in your `package.json`).\n\n:::\n\n## MCP\n\nInstall [`chrome-devtools-mcp`](https://github.com/ChromeDevTools/chrome-devtools-mcp),\na Puppeteer-based MCP server for browser automation and debugging.\n\nPuppeteer also supports the experimental [WebMCP](https://pptr.dev/guides/webmcp) API.\n\n## Example\n\n```ts\nimport puppeteer from 'puppeteer';\n// Or import puppeteer from 'puppeteer-core';\n\n// Launch the browser and open a new blank page.\nconst browser = await puppeteer.launch();\nconst page = await browser.newPage();\n\n// Navigate the page to a UR

FAQ (5)

Troubleshooting
How to fix ECONNREFUSED ::1 error when connecting Puppeteer to Chrome via browserWSEndpoint with localhost?

This happens because Node.js 18+ resolves localhost to the IPv6 address ::1, but Chrome's DevTools server only listens on IPv4 127.0.0.1. The webSocketDebuggerUrl returned by http://localhost:9222/json/version contains ws://localhost:..., so the connection to ::1:9222 is refused (this broke in Puppeteer 17.0.0). **Workaround:** replace localhost with 127.0.0.1 in the endpoint before connecting:

const { webSocketDebuggerUrl } = await fetch('http://127.0.0.1:9222/json/version').then(r => r.json());
const browser = await puppeteer.connect({
  browserWSEndpoint: webSocketDebuggerUrl.replace('localhost', '127.0.0.1')
});

Alternatively, set Node's DNS result order to IPv4 first via dns.setDefaultResultOrder('ipv4first') or launch Node with --dns-result-order=ipv4first.

GitHub Issue #8873
Troubleshooting
Why are embedded video thumbnails blank in Puppeteer full page screenshots and how to fix it?

Embedded videos and iframes are often lazy-loaded, so they never render before page.screenshot({ fullPage: true }) captures the page. Fix: scroll through the page first to trigger lazy loading, then scroll back to the top and take the screenshot.

Option 1 — simulate scrolling with the mouse wheel:

await page.goto('https://example.com', { waitUntil: 'networkidle0' });

for (let i = 0; i < 10; i++) {
  await page.mouse.wheel({ deltaY: 400 });
  await new Promise(resolve => setTimeout(resolve, 1000));
}

await page.evaluate(() => window.scrollTo(0, 0));
await new Promise(resolve => setTimeout(resolve, 3000));

await page.screenshot({ fullPage: true, path: 'thumbnails.png' });

Option 2 — scroll each video/iframe into view directly:

await page.evaluate(async () => {
  for (const el of document.querySelectorAll('video, iframe')) {
    el.scrollIntoView();
    await new Promise(r => setTimeout(r, 200));
  }
  window.scrollTo(0, 0);
});
await page.screenshot({ path: 'fullpage.png', fullPage: true });

Both approaches force lazy-loaded thumbnails to render before capture. Confirmed working on Puppeteer 22.13.1.

GitHub Issue #12778
Troubleshooting
Why are intercepted requests from dedicated workers never fulfilled in Puppeteer (request.respond/continue/abort have no effect)?

This is a known bug in Puppeteer up to v25.12.0. When a dedicated worker calls fetch(), Chrome emits Fetch.requestPaused on the page session but Network.requestWillBeSent on the worker session. Due to a race, Puppeteer stores the paused event without its session and then sends Fetch.fulfillRequest (or continue/abort) on the worker session, which has no Fetch domain. Chrome rejects it with 'Fetch.fulfillRequest' wasn't found, Puppeteer swallows the error, and the worker request hangs forever. Fix: upgrade Puppeteer to a version including PR #15605 (fixed on main after 25.12.0), e.g. npm install puppeteer@latest. There is no reliable workaround on affected versions since it is a session-routing race condition; requests only succeed when requestWillBeSent happens to arrive first.

GitHub Issue #15497
Troubleshooting
Why does the fixed/sticky header appear in the wrong position (middle or bottom) in Puppeteer fullPage screenshots, and how to fix it?

Cause: page.screenshot({ fullPage: true }) uses captureBeyondViewport: true by default, which captures the full document without resetting the scroll position. If your code scrolls the page (e.g., an autoScroll helper that scrolls to the bottom to trigger lazy loading), window.scrollY stays at the bottom. Elements with position: fixed/sticky or scroll-based JS then render at the current viewport position instead of the top of the page.

Fix 1 — scroll back to top before capturing:

await autoScroll(page);
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(300); // let sticky elements settle
await page.screenshot({ path: 'example.png', fullPage: true });

Fix 2 — force the header to absolute position or hide it entirely via injected CSS:

await page.addStyleTag({ content: 'header, .navbar, .sticky-nav { position: absolute !important; }' });
// or to remove it completely:
// await page.addStyleTag({ content: 'header { display: none !important; }' });
await page.screenshot({ path: 'example.png', fullPage: true });

Optionally, page.screenshot({ fullPage: true, captureBeyondViewport: false }) changes the capture behavior, but resetting the scroll position is the most reliable workaround.

GitHub Issue #8191
Troubleshooting
Why does puppeteer.connect() drop the path prefix from browserURL and call /json/version instead of /t/token/json/version?

This happens because Puppeteer uses new URL('/json/version', browserURL). In the WHATWG URL API, a path beginning with / is absolute and replaces the base URL's entire pathname. The issue is fixed in Puppeteer PR #15435 by appending json/version to the existing pathname. Upgrade puppeteer-core or puppeteer to a release containing that fix.

Temporary workaround: resolve the version endpoint yourself while preserving the prefix and connect with browserWSEndpoint:

const versionURL = new URL(browserURL);
versionURL.pathname = versionURL.pathname.replace(/\/?$/, '/') + 'json/version';

const response = await fetch(versionURL.href);
const { webSocketDebuggerUrl } = await response.json();

const browser = await puppeteer.connect({
  browserWSEndpoint: webSocketDebuggerUrl,
});

This correctly changes http://host:9222/t/token into http://host:9222/t/token/json/version.

GitHub Issue #15250