ScreenshotNeo

BlogHTML to image & PDF

How to Fix Broken Base64 Images in Puppeteer PDF Headers

A broken Base64 image in a Puppeteer PDF header can come from the data URI, template, PDF options, or browser version. Use this diagnostic sequence to isolate it.

By the ScreenshotNeo team30 September 20269 min read

How to Fix Broken Base64 Images in Puppeteer PDF Headers

A broken Base64 image in a Puppeteer PDF header is not automatically a bad Base64 string. Start by inspecting the exact final headerTemplate and validating the decoded image bytes. Then reduce the PDF call to a minimal reproduction and compare both the Puppeteer package version and the Chrome or Chromium executable it launches.

A 2025 Puppeteer report described a JPEG header image working with Puppeteer 24.3.0 and failing from 24.4.0 onward in that reporter’s setup. A Puppeteer collaborator reproduced a print failure with then-current stable Chrome and said it seemed fixed in Canary. The discussion did not identify a stable Chrome release containing the fix, so no version upgrade can be promised as a universal remedy. Read the report and discussion.

1. Confirm what Puppeteer is being asked to print

Puppeteer’s Page.pdf() does not print header and footer templates by default: displayHeaderFooter defaults to false. A custom headerTemplate is an HTML string. The documented placeholder classes are date, title, url, pageNumber, and totalPages. See the Puppeteer PDFOptions documentation.

Log or save the final template after your application has interpolated variables. Redact secrets if the template contains any. Look for an unresolved placeholder such as {{logo}}, accidental escaping, a missing MIME prefix, or a second prefix added to a value that already contains a full data URI. Check for quotes or whitespace inserted into the encoded payload by template processing.

Keep the header self-contained: literal image src, inline styles, and no dependency on page CSS or JavaScript that is expected to transform the image during printing. The API documents a valid HTML template, but does not promise that it shares the page’s resource context or runs scripts. A historical issue documents a reproduction where a script in header/footer markup did not run. See the script issue.

2. Validate the image and its data URI

Base64 is only an encoding. A string that looks like Base64 does not prove its decoded bytes are a valid image. Decode the exact payload independently, save it with the expected file extension, and open it in an image viewer. Confirm the format matches the declared MIME type:

Trace the image from source bytes through the data URI into the PDF header.
Trace the image from source bytes through the data URI into the PDF header.
  • PNG bytes use data:image/png;base64,.
  • JPEG bytes use data:image/jpeg;base64,.
  • WebP bytes use data:image/webp;base64, if that is the actual file format.

Decide whether the variable contains only encoded bytes or a complete data URI. The following example expects only Base64-encoded PNG bytes in pngBase64; it adds the prefix once. If your input already starts with data:image/, use it directly instead of adding another prefix.

const fs = require('node:fs');
const pngBase64 = fs.readFileSync('./logo.png').toString('base64');
const dataUri = `data:image/png;base64,${pngBase64}`;

// Optional local validation: decode and compare by opening this file.
fs.writeFileSync('./decoded-logo.png', Buffer.from(pngBase64, 'base64'));
console.log(dataUri.slice(0, 40));

For a deployment pipeline, perform this check against the same asset source and transformation steps used by the PDF job. An image that opens locally may still be altered, truncated, or replaced before it reaches the template.

3. Make a minimal PDF reproduction

Use a plain page body and a header containing only the image. Set displayHeaderFooter: true and leave enough top margin for the header. This pattern is a diagnostic starting point, not a guaranteed workaround for the reported version regression.

const fs = require('node:fs');
const puppeteer = require('puppeteer');

(async () => {
  const pngBase64 = fs.readFileSync('./logo.png').toString('base64');
  const browser = await puppeteer.launch();
  try {
    const page = await browser.newPage();
    await page.setContent('<main>PDF body</main>');

    const pdf = await page.pdf({
      path: './report.pdf',
      format: 'A4',
      printBackground: true,
      displayHeaderFooter: true,
      headerTemplate: `
        <div style="width:100%; margin:0; padding:0;">
          <img
            src="data:image/png;base64,${pngBase64}"
            style="display:block; width:110px; height:auto;"
            alt="Company logo"
          />
        </div>
      `,
      footerTemplate: '<span class="pageNumber"></span> / <span class="totalPages"></span>',
      margin: { top: '1in', bottom: '0.5in', left: '0.5in', right: '0.5in' },
    });
    console.log(`Wrote ${pdf.length} bytes`);
  } finally {
    await browser.close();
  }
})();

Save the exact reproduction, including the image input and template string, so you can run it against a second browser build. Avoid making several changes at once; otherwise you cannot tell which one changed the outcome.

4. Compare Puppeteer and Chrome versions

Record the Puppeteer package version and the actual browser executable and version. These are separate version numbers: Puppeteer controls its browser launch, but installations can also point at a separately installed Chrome or Chromium. Do not infer one from the other.

A controlled browser comparison helps separate template problems from version-specific behavior.
A controlled browser comparison helps separate template problems from version-specific behavior.
const puppeteer = require('puppeteer');

(async () => {
  console.log('Puppeteer:', require('puppeteer/package.json').version);
  const browser = await puppeteer.launch();
  try {
    console.log('Browser:', await browser.version());
    console.log('Executable:', puppeteer.executablePath());
  } finally {
    await browser.close();
  }
})();

The issue reporter listed Node 22.14.0, npm 10.9.2, and Windows, and reported that their case worked on Puppeteer 24.3.0 but failed starting with 24.4.0. Treat those as the reporter’s observed setup, not a compatibility guarantee for every operating system or image. Compare the same minimal test on the known working and failing environments, changing one version variable at a time where practical.

The collaborator’s April 4, 2025 observation was that the print failure could be reproduced with then-current stable Chrome and seemed fixed with Chrome Canary. That is useful evidence for testing an upstream browser difference, but it does not identify a stable release with the fix. If the minimal data URI case fails, a controlled Canary comparison can help isolate the cause. Verify the exact installed build before calling it a production fix. See the dated collaborator comment.

5. Check template layout and asset assumptions

Once the data decodes correctly and the minimal case is known, check layout and loading assumptions:

  • Header is not enabled: set displayHeaderFooter: true.
  • Header is clipped: increase the top margin and keep the header image’s width and height within the printable region.
  • Wrong MIME type: match the prefix to the actual decoded image format.
  • Duplicate data URI prefix: either construct the URI from raw Base64 bytes or use the complete URI as-is.
  • Header depends on page styles: move essential styles inline so the template stands on its own.
  • Header depends on script execution: precompute the URI in Node and put it directly in the HTML. A 2018 issue records a script that did not run in a header/footer template reproduction.
  • Relative asset path: verify the URL resolves in the browser context actually used for printing. A 2018 report describes a relative logo path yielding a gray outline; it does not establish that relative URLs always fail. See the historical report.

Do not assume that replacing a relative URL with Base64 necessarily solves the problem: the issue under investigation is itself a Base64 image failure. Each source form has its own assumptions, so compare a known-valid local file, a controlled absolute URL if suitable, and the data URI without changing other PDF settings.

6. Troubleshooting common symptoms

Symptom Likely checks Next action
Image is missing, but PDF generation succeeds Check displayHeaderFooter, final template, URI prefix, and decoded bytes. Print the minimal header and verify the standalone decoded image opens.
Image appears as a broken-image outline Inspect malformed or duplicated URI, incorrect MIME type, truncation, or unresolved template data. Log the final src prefix and compare payload length and source bytes.
Image is present but clipped Check header dimensions and top margin. Use a smaller explicit width and increase the top margin for a controlled run.
PDF call rejects with “Printing failed” Record browser version and executable, plus Puppeteer version. Rerun the minimal data URI case with a controlled browser build; do not assume a particular stable version contains a fix.
Works on a developer machine but not in deployment Compare the actual browser binary, image bytes, generated template, runtime, and PDF options. Capture these values in a sanitized diagnostic log in both environments.

These are diagnostic branches, not a claim that the cited issues enumerate every possible failure mode. The sources do not establish the complete set of malformed-data or MIME failure causes.

7. Options that affect PDF output

For a header image problem, focus on the options that enable and position the header, then keep the rest of the reproduction stable. Relevant documented controls include:

  • displayHeaderFooter: enables the header/footer templates; default is false.
  • headerTemplate and footerTemplate: HTML strings for the respective regions.
  • margin: page margins, including the top space needed by the header and bottom space for the footer.
  • format, width, and height: choose page dimensions. Use a consistent choice during diagnosis.
  • printBackground: controls whether background graphics print; it is separate from whether an <img> source decodes.
  • landscape, pageRanges, scale, and preferCSSPageSize: useful for final layout, but avoid changing them while isolating a missing image.

Consult the current PDFOptions reference for the complete option list and types. Defaults and supported behavior can evolve; the troubleshooting case should record the exact library and browser versions that ran.

8. Performance, reliability, and cost considerations

Inline Base64 makes the template self-contained, but it also places the encoded image data in the HTML string passed to the browser. For a small logo this can be straightforward. For larger assets or many PDFs, consider whether your application can reuse a controlled asset source and avoid repeatedly rebuilding large strings. Regardless of strategy, compare the final bytes and resulting output when debugging.

For reliability, preserve a minimal regression fixture containing the image, exact generated header, PDF options, Puppeteer version, browser version, and runtime. When you upgrade Puppeteer or change the browser executable, run that fixture in the same environment used for production. The reported version transition shows why tracking both package and browser matters; it does not establish the behavior of every later release.

Cost depends on where PDF generation runs: browser runtime, memory, storage, and operational maintenance are application-specific, and the cited sources provide no benchmark or cost figures. Measure your own document sizes and job volume before choosing between an in-process browser and a hosted capture service.

Or skip the browser setup

If the goal is a clean screenshot of a web page rather than a custom Puppeteer PDF header, ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. One GET request returns a PNG, JPEG, WebP, or PDF. Its API options include PDF paper size, margins, landscape, and page ranges. ScreenshotNeo does not replace a custom Puppeteer header template; use it when a hosted page capture fits the job.

Example using cURL:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

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)

Node.js:

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}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));

See the ScreenshotNeo API documentation for request options. Cookie banners, popups, and chat widgets are removed before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers indicate the page verdict and billing status. Its MCP server has take_screenshot, get_page_info, and capture_pdf tools for AI agents. Free 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.

FAQ

Does Base64 guarantee the image will work in a PDF header?

No. The encoded payload must decode to valid image bytes, the MIME prefix must match those bytes, and the PDF/browser combination must render the template correctly.

Should I downgrade to Puppeteer 24.3.0?

The issue reporter said their setup worked with 24.3.0 and failed from 24.4.0. That report is not a general compatibility recommendation. Reproduce with your actual browser and application before selecting a version.

Is Chrome Canary a confirmed production fix?

No. The collaborator described a dated reproduction where Canary seemed to fix the failure, without naming the build or a stable release containing the change.

Can headerTemplate JavaScript repair the image at print time?

Do not rely on it. Keep the template simple and construct the final image URI in your application before calling page.pdf().

Does ScreenshotNeo generate a custom branded Puppeteer header?

The supplied product details describe hosted website capture and PDF options, not arbitrary Puppeteer header-template behavior. Use Puppeteer when that custom template is required.