{"id":782,"date":"2026-07-18T20:33:56","date_gmt":"2026-07-18T17:33:56","guid":{"rendered":"https:\/\/mexela.com\/blog\/puppeteer-proxy-setup\/"},"modified":"2026-07-19T21:18:19","modified_gmt":"2026-07-19T18:18:19","slug":"puppeteer-proxy-setup","status":"publish","type":"post","link":"https:\/\/mexela.com\/blog\/puppeteer-proxy-setup\/","title":{"rendered":"Puppeteer Proxy Setup: Authentication, Sessions, and Testing"},"content":{"rendered":"<p class=\"mexela-answer\">To use a proxy with Puppeteer, pass the proxy endpoint to Chromium through the <code>--proxy-server<\/code> launch argument, create the page, and call <code>page.authenticate()<\/code> before navigation when the HTTP proxy requires a username and password. Verify the observed exit from inside that same page before starting the real workflow.<\/p>\n<p class=\"mexela-scope\"><strong>Scope:<\/strong> this tutorial covers one HTTP or HTTPS-tunneling proxy per browser process in Node.js. For generic Node clients and curl, use <a href=\"\/blog\/use-proxies-curl-python-nodejs\/\">the multi-client proxy guide<\/a>; for the full protocol distinction, use the <a href=\"\/blog\/proxy-setup-developer-guides\/\">Proxy Setup and Developer Guides hub<\/a>.<\/p>\n<h2 id=\"launch\">Start Chromium with a proxy launch argument<\/h2>\n<p>Puppeteer passes additional browser flags through the <code>args<\/code> property documented in the official <a href=\"https:\/\/pptr.dev\/api\/puppeteer.launchoptions\" rel=\"noopener\">Puppeteer LaunchOptions reference<\/a>. Keep the endpoint separate from credentials so error messages, process listings, and copied commands do not reveal a password.<\/p>\n<pre><code>import puppeteer from 'puppeteer';\n\nconst PROXY_HOST = 'HOST';\nconst PROXY_PORT = 'PORT';\n\nconst browser = await puppeteer.launch({\n  headless: true,\n  args: ['--proxy-server=' + 'http:\/\/' + PROXY_HOST + ':' + PROXY_PORT],\n});\n\nconst page = await browser.newPage();<\/code><\/pre>\n<p>This launch argument applies at browser-process level. Opening another page in the same process does not create a different upstream proxy. If two workers must use different endpoints, launch two browser processes or use an architecture that explicitly supports per-context routing.<\/p>\n<h2 id=\"authentication\">Add proxy authentication before the first navigation<\/h2>\n<p>The official <a href=\"https:\/\/pptr.dev\/api\/puppeteer.page.authenticate\" rel=\"noopener\"><code>page.authenticate()<\/code> reference<\/a> describes credential handling and notes that authentication enables request interception internally. Call it before <code>page.goto()<\/code>. Read secrets from a protected runtime store; do not commit them to a JavaScript file.<\/p>\n<pre><code>const PROXY_USER = 'USER';\nconst PROXY_PASSWORD = 'PASSWORD';\n\nawait page.authenticate({\n  username: PROXY_USER,\n  password: PROXY_PASSWORD,\n});<\/code><\/pre>\n<p>A 407 response usually means the proxy was reached but rejected the presented proxy authentication. Check the assigned method, credential spelling, account status, and whether source-IP authorization is expected instead. The <a href=\"\/blog\/proxy-authentication-username-password-vs-ip-auth\/\">proxy authentication guide<\/a> separates those models.<\/p>\n<h2 id=\"verify\">Perform exit verification inside Puppeteer<\/h2>\n<p>Use a trusted HTTPS endpoint that returns the requester&#8217;s address, then compare it with a direct baseline. Inspect status and content type before parsing the body; an HTML challenge page is not an IP result.<\/p>\n<pre><code>const response = await page.goto('https:\/\/api.ipify.org?format=json', {\n  waitUntil: 'domcontentloaded',\n  timeout: 30000,\n});\n\nif (!response || !response.ok()) {\n  throw new Error(`Exit check failed: ${response?.status() ?? 'no response'}`);\n}\n\nconst observed = await page.evaluate(() =&gt; JSON.parse(document.body.innerText));\nconsole.log({ exitIp: observed.ip });<\/code><\/pre>\n<p class=\"mexela-expected\"><strong>Expected observation:<\/strong> the page reports the assigned proxy exit, HTTPS validation remains enabled, and a second request succeeds with the same endpoint when the plan is static. If the direct IP remains visible, stop and inspect the browser launch arguments rather than continuing the automation.<\/p>\n<h2 id=\"sessions\">Design session isolation deliberately<\/h2>\n<p>Session isolation means keeping cookies, cache, credentials, and one intended egress together for the life of a workflow. Use a separate browser context for separate application sessions, but remember that contexts inside one Chromium process still inherit its process-level proxy.<\/p>\n<pre><code>const context = await browser.createBrowserContext();\nconst sessionPage = await context.newPage();\nawait sessionPage.authenticate({ username: PROXY_USER, password: PROXY_PASSWORD });\n\/\/ Perform one authorized session, then close its context.\nawait context.close();<\/code><\/pre>\n<p>Do not rotate the address halfway through a login or transaction simply because a request failed. First classify the failure. Changing the egress can invalidate state and make a reproducible bug look intermittent.<\/p>\n<h2 id=\"troubleshooting\">Puppeteer troubleshooting by symptom<\/h2>\n<table>\n<thead>\n<tr>\n<th>Symptom<\/th>\n<th>Likely boundary<\/th>\n<th>First check<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>ERR_PROXY_CONNECTION_FAILED<\/code><\/td>\n<td>Gateway DNS, port, route, or listener<\/td>\n<td>Test the documented host and port once outside Puppeteer<\/td>\n<\/tr>\n<tr>\n<td>407 response<\/td>\n<td>Authentication<\/td>\n<td>Check method, credentials, and source-IP allowlist<\/td>\n<\/tr>\n<tr>\n<td>Timeout after connection<\/td>\n<td>Tunnel, TLS, or destination<\/td>\n<td>Compare a known exit endpoint with the real target<\/td>\n<\/tr>\n<tr>\n<td>Direct IP still visible<\/td>\n<td>Launch configuration<\/td>\n<td>Log the non-secret endpoint label and actual browser args<\/td>\n<\/tr>\n<tr>\n<td>Only one website fails<\/td>\n<td>Destination policy or application state<\/td>\n<td>Preserve status and response class; do not retry blindly<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The layered sequence in <a href=\"\/blog\/common-proxy-errors-fixes\/\">Common Proxy Errors and Fixes<\/a> helps separate connectivity, DNS, TLS, and destination responses.<\/p>\n<p class=\"mexela-limits\"><strong>Operational limits:<\/strong> run automation only on systems and accounts you are authorized to test, follow destination terms and rate limits, cap concurrency, and redact cookies, tokens, and proxy credentials from screenshots and logs. A proxy changes a route; it does not grant access.<\/p>\n<h2 id=\"next-step\">Choose an endpoint after the Puppeteer test is reproducible<\/h2>\n<p>Record the required country, protocol, authentication method, session duration, browser count, and measured latency. If the workflow needs a stable exclusive egress, compare those requirements with the current <a href=\"\/private-proxies\/\">private proxy options<\/a> and start with a small controlled allocation.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Configure a proxy in Puppeteer with a Chromium launch argument, supply credentials safely, verify the exit IP, and diagnose the layer that failed.<\/p>\n","protected":false},"author":0,"featured_media":783,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[189],"tags":[],"_links":{"self":[{"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/posts\/782"}],"collection":[{"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/comments?post=782"}],"version-history":[{"count":1,"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/posts\/782\/revisions"}],"predecessor-version":[{"id":793,"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/posts\/782\/revisions\/793"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/media\/783"}],"wp:attachment":[{"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/media?parent=782"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/categories?post=782"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/tags?post=782"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}