ScreenshotNeo

BlogHow-to

Puppeteer Screenshot Returns a White Page After Navigation: Troubleshooting

Trace a white Puppeteer screenshot from navigation response to page readiness, headless behavior, and PDF handling, then capture only after verifying the page.

By the ScreenshotNeo team4 October 20268 min read

A white Puppeteer screenshot can have several causes, so start with evidence rather than changing screenshot settings at random. Log the final URL and main document response from page.goto(), check that the expected content exists in the DOM, and wait for a page-specific selector before capturing. If you are navigating to a PDF, check the browser mode: Puppeteer documents that headless shell does not support PDF navigation.

Navigation completing does not prove that the intended application view rendered. Puppeteer’s screenshot guide shows waiting for network activity to settle before capturing, while Chrome’s headless rendering guide demonstrates waiting for a known selector. Use the selector that identifies the actual content you need.

1. Check the destination and main response

page.goto() returns the main resource response, or null in documented cases. Redirects resolve with the last response. In headless shell, valid HTTP error statuses such as 404 and 500 do not make goto() throw, so inspect the response status rather than treating a resolved promise as success. Navigating to about:blank or to the same URL with a different hash can return null. See the Puppeteer Page.goto() reference.

Log the requested URL, the response status, and page.url() after navigation. These values help distinguish an unexpected redirect or error route from a page that reached the intended URL but has not rendered its content. A login page, challenge, or empty route is a possibility to investigate, not a diagnosis without your URL and logs.

2. Wait for the content you need

A load event or a quiet network is not a guarantee that a JavaScript application has displayed the view you intend to capture. A page-specific selector is a more direct readiness check. Puppeteer’s guide demonstrates waitForSelector(), and Chrome’s guide notes that lazy-loaded pages may need more time.

Use a selector tied to the real content, then check its text or another meaningful property before taking the screenshot. A generic selector such as body can exist on an otherwise blank or incomplete page.

3. A runnable diagnostic script

This CommonJS script logs the requested URL, final URL, main response status, title, and expected element text. Replace the URL and selector with those for the page you are debugging. It throws if the response is missing or unsuccessful, or if the expected element never appears. A 404 or 500 response can otherwise coexist with a resolved navigation promise.

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();
    const requestedUrl = 'https://example.com';
    const expectedSelector = 'main h1';

    page.on('console', message => {
      console.log(`[console:${message.type()}] ${message.text()}`);
    });
    page.on('pageerror', error => {
      console.error('[pageerror]', error.message);
    });
    page.on('requestfailed', request => {
      console.error('[requestfailed]', request.url(), request.failure()?.errorText);
    });

    const response = await page.goto(requestedUrl, {
      waitUntil: 'networkidle2',
      timeout: 60000,
    });

    console.log('requested URL:', requestedUrl);
    console.log('final URL:', page.url());
    console.log('main response status:', response?.status() ?? 'null response');
    console.log('title:', await page.title());

    if (!response) {
      throw new Error('No main response was returned; check the target URL and navigation type.');
    }
    if (!response.ok()) {
      throw new Error(`Main document returned HTTP ${response.status()}`);
    }

    await page.waitForSelector(expectedSelector, { timeout: 30000 });
    const content = await page.$eval(expectedSelector, element => element.textContent?.trim());
    console.log('expected content:', content);
    if (!content) {
      throw new Error(`Expected element ${expectedSelector} exists but has no text.`);
    }

    await page.screenshot({ path: 'page.png', fullPage: true });
    console.log('saved page.png');
  } finally {
    await browser.close();
  }
})().catch(error => {
  console.error(error);
  process.exitCode = 1;
});

Install Puppeteer in the project with npm install puppeteer, save the script as capture.js, and run node capture.js. Puppeteer’s screenshot API is Page.screenshot(); the documented guide also shows element capture after waiting for a selector. See the screenshots guide and screenshot API reference.

4. Follow the evidence to the cause

Evidence What to investigate Next step
Final URL differs from the requested URL A redirect or route change may have led to a different view. Inspect the destination page, title, response status, and expected selector there.
Main response is 404, 500, or another error The server returned an error document even though navigation may resolve. Handle the status explicitly and inspect the response body and final URL.
Expected selector times out The application may still be loading, may have failed, or may render a different route. Check the selector, console and request failures; use a longer timeout only if the page is known to load slowly.
Selector exists but screenshot is blank DOM presence alone does not establish that visible pixels were painted. Check its text and visibility, inspect console and failed requests, and compare a headful run.
HTML page works but PDF navigation fails The actual launch mode may be headless shell, which does not support PDF navigation. Confirm the browser mode and use a supported approach for the PDF target.

The last two rows are diagnostic suggestions, not universal explanations. Without the target URL, browser and Puppeteer versions, launch options, logs, and readiness condition, the specific cause cannot be established.

5. Use the right readiness condition

Puppeteer’s screenshot guide demonstrates waitUntil: 'networkidle2' followed by page.screenshot(). Treat that as a useful starting pattern, not proof that client-side rendering is complete. Pages with ongoing requests can also make network-idle waits unsuitable. A selector that identifies the actual view is often a clearer condition:

await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-testid="report-ready"]', { timeout: 30000 });
await page.screenshot({ path: 'report.png' });

Choose the wait condition from the page’s behavior:

  • domcontentloaded can get you to an early point in navigation; follow it with a page-specific readiness check.
  • networkidle2 is the documented screenshot-guide example and can be useful when network activity settles.
  • waitForSelector() checks for a known element. Verify the element’s content as well when its mere presence is insufficient.
  • A fixed delay can help diagnose timing, but it is not a reliable substitute for checking the state you need.

For a useful region only, wait for its element and use ElementHandle.screenshot(). Puppeteer documents that element screenshots try to scroll an element into view when it is hidden. For a full-page capture, lazy-loaded content may require scrolling or additional waits; Chrome’s headless guide notes that lazy-loaded pages can need more time.

6. If the DOM looks right but the pixels are white

  1. Log the page title and expected element text immediately before capture.
  2. Listen for page console messages, uncaught page errors, and failed requests, as in the script above.
  3. Run the same navigation and capture in a headful browser and compare the result. This is a debugging comparison, not proof that headless mode is the cause.
  4. Check whether the site has rendering logic that changes behavior when it detects headless rendering. Chrome’s guide discusses signaling headless rendering to page code; treat this as one investigation branch, especially because that guide is older.
  5. Check whether your screenshot is taken before the page’s visible content is painted. Wait for the meaningful element and inspect its content before capture.

Do not assume that a white image has one standard fix. The browser console, response, URL, DOM state, and a headful comparison narrow the possibilities.

7. Common errors and fixes

Symptom Likely explanation Fix
goto() resolves, but the page is an error A valid HTTP error status does not necessarily reject navigation in headless shell. Inspect the returned response and check response.ok() or response.status().
response is null Some documented navigations, including about:blank and a same-URL hash change, return no response. Handle the null case and confirm the navigation type; do not dereference the response unconditionally.
waitForSelector() times out The selector may be wrong, the route may be unexpected, or the content may not have loaded. Confirm page.url(), title, selector, console output, and failed requests. Increase the timeout only when evidence supports slower loading.
Navigation times out on a busy page Waiting for network quiet may not match a page that keeps making requests. Try an earlier navigation condition followed by a specific selector wait.
PDF navigation does not work Headless shell does not support navigation to PDF documents. Verify the launch mode and use a browser mode that supports the required PDF workflow.
Element screenshot omits the expected region The selector may identify the wrong element or the element may not be ready. Inspect its text and dimensions, wait for the intended element, and use element capture only when a region is desired.

8. Performance, reliability, and cost

For reliable captures, wait for the condition that represents the content you need rather than applying a long fixed delay to every page. Reuse a browser process when capturing multiple pages in one job, while creating an appropriate page for each capture; always close pages and the browser when the job ends. Set navigation and selector timeouts deliberately, log the URL and response, and make failures visible to the caller instead of silently saving an unverified image.

Network-idle waits can add latency or fail to match pages with persistent requests; selector waits can fail when a page changes its markup. A bounded timeout plus clear diagnostics gives a useful failure instead of an indefinite wait. The research sources provide no universal benchmark or cost figure for running Puppeteer, so performance and infrastructure costs depend on your browser environment, page, capture frequency, and concurrency.

Or skip the browser setup

If your goal is a clean screenshot rather than debugging your own Puppeteer browser, ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request returns a PNG, JPEG, WebP, or PDF. The API documentation covers its options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
  • Cookie banners are accepted like a visitor; known consent banners, newsletter popups, and chat widgets are removed before the shot, and each step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. Response headers report the page verdict and billing status.
  • An MCP server gives Claude, Cursor, and other MCP clients screenshot tools, including take_screenshot, get_page_info, and capture_pdf.
  • The free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots.

Sign up for 1,000 free screenshots a month, with no card required.

FAQ

Does a successful page.goto() mean the page loaded correctly?

No. Check the returned response and final URL, then verify the expected page content before capturing.

Should I always use networkidle2?

No. It is a documented example, but the best readiness condition depends on the page. A page-specific selector is a useful check for rendered application content.

Can Puppeteer navigate to a PDF?

Puppeteer documents that headless shell does not support PDF navigation. Confirm your launch mode before applying that limitation to your setup.

What information is needed to identify the exact cause?

The target URL, Puppeteer and browser versions, launch options, navigation response and final URL, readiness condition, console and request errors, and the screenshot result.