wkhtmltopdf --cookie, --custom-header and --username in Chrome
wkhtmltopdf could fetch a page that needs a login: --cookie, --custom-header, --username and --password. Chrome's command line has no flags for any of them. We tested each replacement on Chromium 141 against a small local server that returns 401 unless the request carries the secret, and read every PDF back with pdftotext. One of the obvious Playwright options also sends your token to third-party servers.
1. What works, option by option
| wkhtmltopdf option | Chrome replacement | Result in our test |
|---|---|---|
--cookie session abc | Playwright context.addCookies([{ name, value, url }]) before goto | Page and its image both got the cookie; the PDF had the private text. Without the cookie the PDF said "Please sign in". |
--custom-header X-Token s3cret | newContext({ extraHTTPHeaders }) | Works for the page, but the header was also sent to an image on another origin. See section 2. |
--custom-header, same-origin only | page.route(origin + "/**", r => r.continue({ headers: { ...r.request().headers(), "x-token": "s3cret" } })) | The header reached our server and not the other origin. |
--username u --password p | Playwright newContext({ httpCredentials: { username, password } }) | Works when the server answers 401 with WWW-Authenticate: Basic: the first request had no Authorization header, the retry had it, and the PDF had the page text. |
--username u --password p | Chrome CLI with http://u:p@host/path | Works with --headless=new: the PDF had the page text. Without the credentials in the URL the PDF had no report text. |
| Any cookie or header | Chrome CLI flags | None found. For cookies and custom headers use Playwright, Puppeteer, or print from a logged-in browser. |
2. The header that leaked
extraHTTPHeaders is the closest match to --custom-header, and it is the one we would have guessed. In our test the page referenced an image served from a different origin (another port on 127.0.0.1). The server for that image logged x-token=s3cret: the header is attached to every request the page makes, including cross-origin ones. If the header is a bearer token and your report loads a logo, font or chart from a CDN, you have just sent the token to that CDN.
The fix we tested is to set the header only on your own origin with a route handler:
const origin = "https://reports.example.com";
await page.route(origin + "/**", (route) =>
route.continue({ headers: { ...route.request().headers(), "x-token": process.env.REPORT_TOKEN } }));
await page.goto(origin + "/invoice/42");
await page.pdf({ path: "invoice-42.pdf", format: "A4", printBackground: true });
With that handler, our other-origin image request arrived with no x-token. Read the token from an environment variable, not from the source file, and avoid it in the URL or the command line, where it shows up in process lists and logs.
3. Cookies
addCookies needs either a url or a domain and path. We used url. The cookie was sent with the page request and with the image request on the same host. wkhtmltopdf's --cookie-jar (a file to load and save cookies) has no direct match; export your cookies yourself and pass them to addCookies.
const context = await browser.newContext();
await context.addCookies([{ name: "session", value: process.env.SESSION, url: "https://reports.example.com" }]);
const page = await context.newPage();
await page.goto("https://reports.example.com/invoice/42");
await page.pdf({ path: "invoice-42.pdf" });
4. Check the status, or you will print the login page
Our first run, with no credentials, produced a valid PDF that said "Please sign in". Nothing failed. If the secret expires or a cookie is wrong, your job keeps writing PDFs of the error page. Check the response before printing:
const res = await page.goto(url);
if (!res || !res.ok()) throw new Error("not printable: HTTP " + (res && res.status()));
In the runs above the unauthenticated request returned 401 and the authenticated ones 200, so this check would have stopped the bad PDF. A login redirect that ends in a 200 sign-in page would pass it, so for those, also look for a word that only the real page contains.
5. The easiest route: no login at all
If you generate the document yourself, skip the fetch. Fill an HTML template from your own data and print a local file or page.setContent(html): no cookie, header or password is involved, and nothing private crosses the network. That is how Foliokit works: the command fills a template from JSON on your machine and prints it with the Chrome you have installed.
To convert a whole wkhtmltopdf command, paste it into the free command translator. It maps --cookie, --custom-header and --username to Playwright code, and notes that the header goes to every request. Use the same-origin route from section 2 if your page loads anything from another site.
Related guides
Waiting for JavaScript, headers and footers, page size and margins, contents and bookmarks.
Limits of this page
Tested on 18 October 2026: Chromium 141 / Playwright 1.56 on Linux, plain HTTP on localhost, one cookie, one header, Basic auth. Not tested: HTTPS, Digest or NTLM auth, SameSite and Secure cookie rules, cookies set with a domain instead of a url, Puppeteer (its setCookie and setExtraHTTPHeaders are similar, but we did not run them), and wkhtmltopdf's own behaviour. The Chrome command line was run with the Playwright-bundled Chromium and --headless=new, and we did not test other Chrome builds.