Recommended Free Tools
To save a PDF triggered by a web-page button, configure Puppeteer’s download policy and writable directory before clicking, use a locator that waits for the control, then wait for navigation or the download request and verify the resulting file. A click alone does not guarantee that a file was saved: the control may navigate to a PDF viewer, start a normal download, or fail before any response is produced.
What you need before writing the script
- Node.js and a project directory.
- Puppeteer installed with
npm install puppeteer. - A writable, dedicated download directory.
- A stable selector for the real download control, such as
button[data-download="pdf"].
Puppeteer is a JavaScript library for controlling Chrome or Firefox through the DevTools Protocol or WebDriver BiDi. The examples below use its locator API and current download-behavior model.
Complete example for a normal PDF download
This example creates a download directory, permits downloads for the browser context, clicks the button, waits for a completed file, and checks that the file is non-empty.
const puppeteer = require('puppeteer');
const fs = require('fs/promises');
const path = require('path');
async function waitForPdf(downloadDir, before, timeoutMs = 60000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const names = await fs.readdir(downloadDir);
const candidates = [];
for (const name of names) {
if (before.has(name) || name.endsWith('.crdownload')) continue;
const full = path.join(downloadDir, name);
const stat = await fs.stat(full).catch(() => null);
if (stat && stat.isFile() && stat.size > 0) candidates.push({ name, size: stat.size });
}
if (candidates.length) return candidates[0];
await new Promise(resolve => setTimeout(resolve, 250));
}
throw new Error('Timed out waiting for a completed download');
}
(async () => {
const downloadDir = path.resolve(__dirname, 'downloads');
await fs.mkdir(downloadDir, { recursive: true });
const browser = await puppeteer.launch({ headless: true });
try {
const context = browser.defaultBrowserContext();
await context.setDownloadBehavior({
policy: 'allow',
downloadPath: downloadDir
});
const page = await context.newPage();
await page.goto('https://example.com/report', {
waitUntil: 'domcontentloaded',
timeout: 60000
});
const before = new Set(await fs.readdir(downloadDir));
const button = page.locator('button[data-download="pdf"]');
await button.click();
const file = await waitForPdf(downloadDir, before);
const source = path.join(downloadDir, file.name);
const destination = path.join(downloadDir, 'report.pdf');
await fs.rename(source, destination);
console.log(`Saved ${destination} (${file.size} bytes)`);
} finally {
await browser.close();
}
})();
Replace the URL and selector with values from your page. The script ignores Chrome’s temporary .crdownload file, waits until a new non-empty file exists, and then renames it. There is no universal filename or completion sentinel prescribed by Puppeteer, so filesystem verification is application-level code that you should adapt to your naming and concurrency rules.
#1 Best Overall
Configure downloads before the click
Set DownloadBehavior before navigation or interaction. Use policy: 'allow' (or 'allowAndName' where your Puppeteer version supports it) and provide downloadPath; the path is required for those policies. Make the directory absolute and writable, and give each job its own directory when several downloads can run at once.
Do not rely on a browser’s default download location. In CI, a default may be unavailable, shared by other jobs, or cleaned before your test reads it.
Wait for the control and the right outcome
Use a locator instead of an immediate DOM query
Locators automatically wait for an element to be present and in a usable state. A selector tied to the control’s accessible name, text, data attribute, or stable role is less brittle than a generated class name:
const download = page.locator('button[data-download="pdf"]');
await download.click();
If the page has several buttons, narrow the locator to the report row, dialog, or other container that identifies the intended PDF.
Free tools Windows power users keep installed
One-click scans. No signup required.
Rank #2
When the click navigates
A button may submit a form or change the page URL before the PDF response is available. Register the navigation wait before clicking, and start both operations together:
const [response] = await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle2' }),
page.locator('button[data-download="pdf"]').click()
]);
if (response) {
console.log('Navigated to', response.url(), response.status());
}
Waiting for navigation only after click() can race: the navigation may begin and finish before the separate wait is registered. Promise.all avoids that ordering problem.
When the click starts a normal download
Normal downloads do not necessarily produce a navigation event. Observe page activity or request completion, then inspect the configured directory. Puppeteer exposes request, response, and request-finished page events; a directory poll, as in the complete example, is useful because it verifies the artifact that your application actually needs.
page.on('requestfinished', request => {
const url = request.url();
if (url.includes('.pdf') || request.resourceType() === 'document') {
console.log('Finished request:', url);
}
});
Event logging helps diagnose a failed click, but do not treat a matching URL as proof that the file is complete. Check the file’s existence and size, and use your own PDF validation if the workflow requires it.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Distinguish a download from a PDF viewer
Real download response
The server responds with a downloadable PDF, commonly through a link or a click handler. Configure DownloadBehavior, click after the locator is ready, wait for the request or filesystem completion, and then move the file.
PDF viewer navigation
Some controls open a PDF document in the browser instead of initiating a download. That is PDF navigation, not a normal download event. Handle the response or navigation path separately, and do not wait forever for a file that the browser never writes.
Puppeteer documents that headless-shell mode does not support navigation to a PDF document. If the site’s behavior depends on viewer navigation, use response-level handling or a browser mode that supports the required navigation, then save the response bytes in your application. The exact behavior also depends on the server’s headers and the browser mode you launch.
Do not use Page.pdf() for a server PDF
Page.pdf() prints the currently rendered HTML into a new PDF. It is the correct API when your requirement is “create a PDF of this page.” It is not a replacement for clicking a button that retrieves an existing server-generated PDF, because printing can omit download-specific content, use different pagination, and produce a different document.
Rank #4
Handling authentication, popups, and hidden controls
- Complete login and any required consent interaction before locating the button.
- If the control appears only after a menu opens, click the menu first and then create the locator for the revealed button.
- For a new tab or window, listen for the target before clicking and then operate on the new page; otherwise you may keep waiting on the original page.
- Keep cookies and authorization in the same browser context that performs the click. A download URL copied into a separate client may require credentials that are not present there.
- Use a dedicated directory and unique output names when jobs run concurrently; otherwise one job can mistake another job’s file for its own.
Troubleshooting
No file appears
- Confirm
policyisalloworallowAndNameand thatdownloadPathis set. - Check that the directory exists and the process can write to it.
- Determine whether the click navigated to a viewer instead of starting a download.
- Inspect request and response events for authentication failures, redirects, or blocked resources.
The script times out intermittently
Register the wait before clicking. Use the Promise.all navigation pattern for navigation-triggering controls, and increase the timeout only after confirming that the page is genuinely slow rather than waiting for the wrong event.
The wrong element is clicked
Prefer a stable role, accessible name, visible text, or data attribute. Scope the locator to the correct card or dialog and verify that the button is enabled and visible. Avoid selectors based solely on framework-generated class names.
A temporary file remains
A .crdownload file indicates that the browser is still writing or that the transfer failed. Wait for the temporary name to disappear and for a non-empty final file before renaming it. If it never completes, inspect the network response and server permissions.
The PDF opens in a viewer
Stop waiting for a normal download event. Treat the action as PDF navigation or response handling, account for headless-shell limitations, and choose a browser mode or response-saving strategy compatible with the site.
Best Value
- Used Book in Good Condition
You actually need to generate a PDF
Use await page.pdf({ path: 'page.pdf' }) after the page has rendered. That creates a print-rendered document rather than downloading the site’s existing PDF.
Reliability, performance, and cost considerations
- Readiness: wait for the specific control, not an arbitrary fixed delay. Add a targeted wait for a selector or application state when the download button is rendered asynchronously.
- Completion: use a bounded timeout and report the URL, status, and directory contents when it expires.
- Isolation: one temporary directory per job prevents filename collisions and makes cleanup deterministic.
- Resource use: close the page and browser in a
finallyblock. Reuse a browser only when contexts and download directories remain isolated. - Validation: non-zero size proves that bytes were written, not that the bytes are a valid PDF. If correctness matters, check the file signature or parse it with a PDF library.
- Security: treat downloaded files as untrusted input. Restrict output paths, avoid deriving filenames directly from user input, and scan or sandbox files before downstream processing.
A practical decision checklist
- Is the requirement to retrieve the site’s PDF, or to print the current HTML? Use a download workflow for the former and
Page.pdf()for the latter. - Does the click start a download or navigate to a viewer? Identify this from observed requests and URL changes.
- Is DownloadBehavior configured with a writable path before the click?
- Is the locator stable and scoped to the intended control?
- Is the wait registered before the click?
- Does the application verify a completed, non-empty file and handle cleanup?
Or skip the browser setup
If you only need a clean screenshot or PDF of a URL rather than the site’s private, click-triggered download, ScreenshotNeo provides a single-request API and an MCP server for AI agents. Cookie and consent banners, newsletter popups, and chat widgets are removed before capture; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP tools include take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.
Use the API documentation at https://screenshotneo.com/docs/ for options such as full-page capture, device presets, custom CSS or JavaScript, waits, authentication headers, cookies, and PDF margins.
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}`);
The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots, and every feature is included on every plan. Create a free ScreenshotNeo account.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Repair Windows errors before they cause bigger problemsFix Now →Frequently Asked Questions
Can Puppeteer choose the downloaded filename?
The browser or server may determine the initial name. Treat it as unknown, detect the completed file in your dedicated directory, and rename it to an application-controlled name after verification.
Should I wait for network idle before every PDF click?
No. Use navigation waiting only when the click navigates, and use request or filesystem completion for a normal download. Network idle is a page-level heuristic, not proof that a download file is complete.
Why does a successful HTTP response still produce no PDF file?
The response may be rendered in a PDF viewer, blocked by download policy, redirected to authentication, or fail while writing locally. Inspect the event type and configured directory rather than relying on status alone.
Is ScreenshotNeo a replacement for an authenticated button download?
Not necessarily. ScreenshotNeo captures a URL and can send custom headers or cookies, but a private workflow that requires interactive clicks may still need Puppeteer.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Clear out junk files and repair common Windows errors3Scan for outdated or missing drivers - takes under a minuteQuick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




