ScreenshotNeo

BlogHow-to

How to Fix Website Screenshots with Blocked Images in Puppeteer

Find out why images are missing from Puppeteer screenshots, inspect failed requests, fix interception rules, and wait for the images your capture needs.

By the ScreenshotNeo team4 October 20269 min read

When images are missing from a Puppeteer screenshot, first check whether request interception is enabled and whether every intercepted request is resolved. A handler that aborts images, or leaves requests unresolved, can prevent them from loading. Then inspect the failed image URLs and page state, and wait for the specific images your screenshot needs. A navigation wait such as networkidle2 does not prove that every image loaded successfully.

This guide uses Puppeteer’s JavaScript API. It shows how to find the failure, fix interception, wait for image readiness, and isolate cache or service-worker behavior when the evidence points there.

1. Confirm which images failed

An empty region in a screenshot does not by itself prove that a network request was blocked. The image may not have been requested yet, may still be loading, may have failed, or may have loaded outside the captured area. Record the image URLs and inspect both request failures and the page’s image elements.

const failedRequests = [];

page.on('requestfailed', request => {
  failedRequests.push({
    url: request.url(),
    error: request.failure()?.errorText ?? 'Unknown request failure',
    resourceType: request.resourceType(),
  });
});

// After navigation or the capture attempt:
const images = await page.evaluate(() =>
  [...document.images].map(image => ({
    src: image.currentSrc || image.src,
    complete: image.complete,
    naturalWidth: image.naturalWidth,
    naturalHeight: image.naturalHeight,
    loading: image.loading,
  })),
);

console.log({ failedRequests, images });

complete indicates that the browser finished the image load attempt; it does not mean the image succeeded. A completed image with naturalWidth === 0 is a useful failure clue. An incomplete image may need more time, or the page may not have requested it yet because it is lazy-loaded.

Puppeteer’s page.evaluate() runs a function in the page context. Its page.waitForFunction() API can wait for a page-side condition. Use these tools to tie the next step to the actual image URLs and state.

2. Audit request interception

Search the capture code for setRequestInterception(true), request listeners, and calls to continue(), respond(), or abort(). Check every handler: an image-blocking rule elsewhere in the code can affect the screenshot, and multiple listeners may interact.

Once interception is enabled, each request stalls until it is continued, answered, aborted, or completed from browser cache. Puppeteer’s official request interception guide demonstrates aborting image requests; the same pattern can explain missing images if copied into a capture flow without changing the condition.

If interception is not needed, remove it or disable it. If it is needed for other resource types, use a narrow condition and continue images you want to capture:

await page.setRequestInterception(true);

page.on('request', request => {
  if (request.isInterceptResolutionHandled()) return;

  // Example policy: block fonts, but allow images and other requests.
  if (request.resourceType() === 'font') {
    void request.abort();
    return;
  }

  void request.continue();
});

Do not register a handler that only handles the requests it intends to block and silently ignores all others. Every intercepted request needs a resolution. If another handler may already have resolved a request, check isInterceptResolutionHandled() before acting and follow Puppeteer’s guidance on cooperative interception priorities.

3. Wait for the images the screenshot needs

Puppeteer’s screenshot guide shows navigation with waitUntil: 'networkidle2' followed by page.screenshot(). That can be useful for pages that settle after navigation, but network idleness measures network activity; it does not verify that a particular image succeeded. For reliable captures, wait on the target images or on the application’s own readiness condition.

await page.goto(url, {
  waitUntil: 'networkidle2',
  timeout: 30_000,
});

// This waits until every image currently in the document has completed
// its load attempt. It does not treat broken images as successes.
await page.waitForFunction(
  () => [...document.images].every(image => image.complete),
  { timeout: 10_000 },
);

const imageStatus = await page.evaluate(() =>
  [...document.images].map(image => ({
    src: image.currentSrc || image.src,
    complete: image.complete,
    naturalWidth: image.naturalWidth,
  })),
);

const broken = imageStatus.filter(image => image.complete && image.naturalWidth === 0);
if (broken.length) {
  throw new Error(`Images failed to load: ${broken.map(image => image.src).join(', ')}`);
}

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

This is a runnable pattern once url and page are defined. The predicate waits for completion, then the diagnostic distinguishes successful images from failed ones. Decide whether broken images should fail the capture or be tolerated for your use case.

For a page with lazy-loaded images, the document may not request every image until its content is brought into view. Scroll through the page or use the site’s own readiness signal before waiting. If only a specific section matters, identify those image elements and wait for those, rather than requiring unrelated page images to succeed.

Always set a finite timeout. If it expires, report the remaining image URLs and inspect their request outcomes. A page with continuing background requests may also be a poor fit for a broad network-idle wait. Puppeteer documents network-idle behavior and its idle-time and concurrency settings in Page.waitForNetworkIdle() and WaitForNetworkIdleOptions.

4. Isolate cache and service-worker behavior

Try these controls only when the page uses a service worker, images vary between runs, or request evidence suggests stale or intercepted cached responses. Change one setting at a time and compare the same URL and image.

// Diagnostic comparison: bypass the page's service worker.
await page.setBypassServiceWorker(true);

// Diagnostic comparison: disable the browser cache.
await page.setCacheEnabled(false);

setBypassServiceWorker() ignores service workers for requests. setCacheEnabled() controls cache use; cache is enabled by default. If either change alters the image result, investigate the application’s service-worker or cache behavior and use the altered setting only if it fits your capture requirements.

5. Check the specific browser restriction

net::ERR_BLOCKED_BY_CLIENT

This error is not a unique diagnosis for missing images. Check the failing URL and request type first. Puppeteer documents a Chrome for Testing HTTPS-first case in which a remote HTTP navigation can produce this error. That specific navigation scenario should not be assumed to explain an image subresource failure. See Puppeteer’s troubleshooting guide for the documented case and workaround.

Experimental URL allowlist or blocklist

If Puppeteer connects to Chrome with experimental URL allowlist or blocklist patterns, check whether the image host matches. These Chrome-only options can affect subresource requests such as images, use URLPattern, and are not a complete network sandbox. The behavior and limitations are described in Puppeteer’s ConnectOptions documentation.

6. Run a complete capture with diagnostics

The following example launches Puppeteer, records failed requests, navigates, waits for image load attempts, reports broken images, and saves a full-page screenshot. Install Puppeteer in your project first. Replace the example URL with the page you need to capture.

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();
    const failedRequests = [];

    page.on('requestfailed', request => {
      failedRequests.push({
        url: request.url(),
        resourceType: request.resourceType(),
        error: request.failure()?.errorText ?? 'Unknown request failure',
      });
    });

    await page.goto('https://example.com', {
      waitUntil: 'networkidle2',
      timeout: 30_000,
    });

    await page.waitForFunction(
      () => [...document.images].every(image => image.complete),
      { timeout: 10_000 },
    );

    const images = await page.evaluate(() =>
      [...document.images].map(image => ({
        src: image.currentSrc || image.src,
        complete: image.complete,
        naturalWidth: image.naturalWidth,
      })),
    );
    const brokenImages = images.filter(
      image => image.complete && image.naturalWidth === 0,
    );

    console.log(JSON.stringify({ failedRequests, images }, null, 2));
    if (brokenImages.length) {
      throw new Error(
        `Broken images: ${brokenImages.map(image => image.src).join(', ')}`,
      );
    }

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

For a capture that should preserve partial results, replace the throw with structured logging or return a result containing both the screenshot path and the failed URLs. Keep diagnostics attached to the same capture attempt so you can match each visible omission to its request and image state.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. A GET request returns a PNG, JPEG, WebP, or PDF; the Puppeteer setup, browser lifecycle, and image-wait logic are handled by the service. See the ScreenshotNeo API documentation for request options and formats.

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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));

ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the capture; each cleanup step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Every feature is available on every plan.

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

Performance, reliability, and cost

  • Wait narrowly: waiting for the images needed by the output avoids spending time on unrelated page activity. Keep navigation, image, and overall job timeouts finite.
  • Limit diagnostic overhead: request listeners and image inventories are useful for debugging. In production, retain the checks that support your reliability needs and avoid collecting more page data than necessary.
  • Handle partial failure deliberately: decide whether a single broken image should fail the entire screenshot, produce a warning, or be accepted. Preserve failed URLs in logs so intermittent failures can be investigated.
  • Control retries: retry only when the failure is plausibly transient, and impose a retry limit. Repeatedly capturing a permanently blocked URL adds latency and browser work without fixing the cause.
  • Account for browser work: full-page captures and pages with many large images require more loading and rendering work than a viewport capture. Capture only the needed area when the output allows it.
  • Compare service cost explicitly: running Puppeteer means operating the browser process and handling its capture failures in your own environment. ScreenshotNeo’s published plans are Free: 1,000 monthly shots; Starter: $5 for 3,000; Growth: $15 for 15,000; Pro: $39 for 60,000; Scale: $99 for 250,000; Business: $249 for 1,000,000. Yearly billing gives two months free. Only clean shots are billed.

Troubleshooting checklist

Symptom Likely cause to check Next action
All images are missing after enabling interception A handler aborts images or fails to resolve requests Remove interception if unnecessary; otherwise continue allowed requests and resolve every intercepted request.
Only some images are missing A selective URL or resource-type rule, failed host request, lazy loading, or image-specific page behavior Match the missing element’s URL to request events and inspect its complete and naturalWidth values.
Screenshot is blank in an image area, but no request failed The image may not have been requested, may still be loading, or may sit outside the captured region Check lazy loading, scroll the relevant content into view, and inspect the actual capture bounds.
waitForFunction times out An image never completed, or the page keeps adding images Log remaining URLs and failed requests; wait for only the required images or a page-specific readiness signal.
networkidle2 completes but an image is broken Network idleness did not guarantee successful loading of that image Check image state and request outcome; use an image-specific predicate and report broken assets.
Results change between runs Possible cache or service-worker behavior Compare with service-worker bypass and cache disabled, one setting at a time.
net::ERR_BLOCKED_BY_CLIENT Meaning depends on the request; Puppeteer documents a particular Chrome HTTPS-first HTTP-navigation case Inspect the exact URL and whether it is navigation or an image request before applying that case’s workaround.
Only assets from certain hosts fail A configured Chrome URL allowlist or blocklist may match those subresources Review the patterns passed through Puppeteer connection options.

FAQ

Does networkidle2 guarantee that every image loaded?

No. It waits for network activity to become idle under Puppeteer’s criteria. Check the image elements and required asset URLs separately.

Should I always disable the browser cache?

No. Cache is enabled by default. Disable it as a diagnostic comparison when stale cache is plausible, then use the result to guide a targeted fix.

Should a broken image always fail the screenshot job?

That depends on what the screenshot is for. A visual regression capture may need a hard failure; a preview or archive may be more useful with a warning and a record of missing URLs.

Can Puppeteer tell me whether an image loaded successfully?

You can inspect the page’s image elements through page.evaluate(). Use completion state together with natural dimensions and request outcomes; no single empty screenshot region identifies the cause by itself.

Primary references