How to Use Local DNS with Pyppeteer
Map local hostnames to localhost inside Chromium with Pyppeteer using host-resolver-rules, without editing your operating system's hosts file.
Use Chromium’s --host-resolver-rules flag through Pyppeteer’s launch(args=[...]) option. The rule changes hostname resolution for that Chromium process, so http://dev.example can reach 127.0.0.1 without changing your operating system’s hosts file or DNS server.
import asyncio
from pyppeteer import launch
async def main():
browser = await launch(
args=[
'--host-resolver-rules=MAP dev.example 127.0.0.1'
]
)
page = await browser.newPage()
response = await page.goto('http://dev.example', {'waitUntil': 'networkidle0'})
print(response.status if response else 'no response')
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
Install Pyppeteer with pip install pyppeteer. Its launch() function accepts additional browser flags through args; Chromium documents MAP, wildcard, EXCLUDE, IPv6 loopback and port forms for host-resolver-rules. See the Pyppeteer documentation and Chromium host resolver source.
How the mapping works
Chromium receives a resolver expression when Pyppeteer starts it:
--host-resolver-rules=MAP <hostname-pattern> <address>
When a page or a subresource asks Chromium to resolve a matching hostname, Chromium uses the mapped address. The browser still sends the original hostname in the URL and HTTP request. The mapping affects the host resolver only; it does not rewrite URLs, edit /etc/hosts, or configure other applications.
| Part | Example | Meaning |
|---|---|---|
| Exact host | MAP dev.example 127.0.0.1 |
Map one hostname. |
| Subdomain wildcard | MAP *.dev.example 127.0.0.1 |
Map matching subdomains. |
| All hosts | MAP * 127.0.0.1 |
Redirect every hostname in this browser process. |
| Exclusion | MAP * 127.0.0.1, EXCLUDE api.example |
Map all hosts except the named one. |
| IPv6 and port | MAP test.example [::1]:77 |
Use IPv6 loopback and port 77. |
Use the narrowest rule that meets your test’s needs. A wildcard can redirect analytics, API calls, fonts, third-party scripts and unrelated navigation made by the same browser.
Complete setup, step by step
1. Start a local service
Your application must already be listening. DNS mapping does not start a server or select an HTTP port.
python -m http.server 8000
If the service listens on port 8000, include that port in the URL:
http://dev.example:8000
The resolver rule maps the host; the URL’s port controls the TCP destination. If you omit the port, Chromium uses port 80 for HTTP or 443 for HTTPS.
2. Install and launch Pyppeteer
python -m pip install pyppeteer
import asyncio
from pyppeteer import launch
async def main():
browser = await launch(
args=['--host-resolver-rules=MAP dev.example 127.0.0.1']
)
try:
page = await browser.newPage()
response = await page.goto(
'http://dev.example:8000',
{'waitUntil': 'networkidle0', 'timeout': 30000}
)
print('status:', response.status if response else 'no response')
print((await page.title()) or '(untitled)')
await page.screenshot({'path': 'local.png', 'fullPage': True})
finally:
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
The finally block closes Chromium even when navigation fails. Once the process exits, its resolver override disappears.
3. Keep the hostname identical
The URL must contain the name matched by MAP. A rule for dev.example does not match www.dev.example, and a rule for a subdomain does not automatically match its parent.
Useful resolver rules
Map a development domain
args=['--host-resolver-rules=MAP app.test 127.0.0.1']
Map several names
args=[
'--host-resolver-rules=MAP app.test 127.0.0.1, MAP api.test 127.0.0.1'
]
Keep the complete expression in one list item. Pyppeteer passes each item as one browser flag; Chromium parses the comma-separated mapping expression.
Map subdomains
args=['--host-resolver-rules=MAP *.dev.example 127.0.0.1']
Use IPv6 loopback
args=['--host-resolver-rules=MAP dev.example [::1]']
Your service must listen on IPv6 loopback for this to work. If it listens only on 127.0.0.1, use the IPv4 mapping.
Map a destination port
args=['--host-resolver-rules=MAP test.example [::1]:77']
When a port is included in the mapping, verify how your Chromium version applies it to the URL’s port. For predictable local development, putting the port explicitly in the URL and mapping only the address is easier to diagnose.
Map everything with an exception
args=['--host-resolver-rules=MAP * 127.0.0.1, EXCLUDE api.example']
Use this only for an intentionally isolated test. It can send external resources to your local server and produce misleading failures.
HTTPS, certificates and proxies
Local DNS resolution does not make an HTTP service speak HTTPS. For https://dev.example, Chromium still performs a TLS handshake and checks that the certificate is valid for dev.example.
- Prefer a locally trusted certificate whose subject includes the mapped hostname.
- Check certificate errors separately from DNS resolution.
- Pyppeteer’s
ignoreHTTPSErrorsoption exists and defaults toFalse; enable it only for a deliberately controlled test.
browser = await launch(
ignoreHTTPSErrors=True,
args=['--host-resolver-rules=MAP dev.example 127.0.0.1']
)
A proxy can change where connections are made and make a correct resolver rule appear broken. Disable proxy settings while diagnosing, or configure the proxy to bypass your mapped host.
Verification and diagnostics
Check the response, document title and HTML so you can distinguish DNS, transport and application errors.
response = await page.goto(
'http://dev.example:8000',
{'waitUntil': 'domcontentloaded', 'timeout': 30000}
)
print('status:', response.status if response else None)
print('url:', page.url)
print('title:', await page.title())
print('html:', (await page.content())[:500])
A returned HTTP status proves that Chromium reached an HTTP server. A navigation exception usually means the connection, TLS handshake or timeout failed before an HTTP response. A 404, 500 or unexpected page proves that resolution worked and the application responded incorrectly.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
ERR_NAME_NOT_RESOLVED |
The URL host does not match the rule, or the flag was not passed. | Print the exact URL, use an exact MAP rule, and confirm the flag is one item in args. |
| Connection refused | No process listens on the mapped address and port. | Start the local server and verify its bind address and port. |
| Wrong application appears | The port is serving a different virtual host or app. | Check the server’s host routing and send the expected hostname. |
| HTTPS certificate error | The certificate name or trust chain does not match the mapped hostname. | Use a certificate for that name, trust its CA, or intentionally set ignoreHTTPSErrors. |
| External assets fail | A wildcard rule redirected third-party names to localhost. | Replace it with an exact rule or add exclusions. |
| Navigation times out | The page waits for network idle while long-lived requests remain open. | Try domcontentloaded, a selector wait, or a bounded timeout. |
| Rule seems ignored | A proxy, cached browser process or alternate Chromium executable changes behavior. | Remove proxy settings, launch a fresh browser, and use Pyppeteer’s bundled Chromium first. |
| Subdomain is not mapped | An exact host rule does not cover subdomains. | Add MAP *.dev.example ... or an exact rule for each host. |
Reliability, performance and CI
- Process scope: the mapping applies to the Chromium process started by Pyppeteer. It is easy to reproduce in a test command and leaves the machine’s DNS configuration unchanged.
- Browser version: Pyppeteer works best with its bundled Chromium. The project does not guarantee identical behavior with every Chrome or Chromium build supplied through
executablePath. - Startup cost: launching a browser is usually more expensive than opening a page. Reuse one browser for related captures, while creating isolated contexts or pages when tests need separation.
- Rule safety: exact mappings reduce accidental requests to the wrong service. Keep wildcard rules out of shared test suites unless every request is controlled.
- Timeouts: choose explicit navigation and selector timeouts. A network-idle condition can remain pending when the page uses polling, WebSockets or analytics.
- CI: put the resolver flag in code or the test fixture so the runner does not depend on a mutable hosts file. Ensure the service starts before Chromium and bind it to an address reachable from the browser process or container.
When to use another approach
| Approach | Scope | Best for | Trade-off |
|---|---|---|---|
Pyppeteer host-resolver-rules |
One Chromium process | Repeatable browser tests and CI | Only Chromium traffic sees the mapping. |
| Operating-system hosts file | Most applications on one machine | Local development across many tools | Requires machine state and elevated or external configuration. |
| Local DNS server | A network, container set or development environment | Shared team or service discovery | More infrastructure and wider impact. |
Or skip the browser setup
If your goal is a clean screenshot rather than controlling Chromium yourself, ScreenshotNeo provides a one-request capture API. Its consent step accepts cookie banners like a visitor and removes more than 60 known consent platforms, newsletter popups and chat widgets before the capture. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing result.
See the ScreenshotNeo API documentation for all options.
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}`);
ScreenshotNeo also supports full-page and element captures, custom CSS and JavaScript, device and viewport settings, waits, request blocking, headers, cookies, user agents, geolocation, resizing, caching, signed links, asynchronous jobs, bulk capture and PDFs. An MCP server lets Claude, Cursor and other MCP clients call take_screenshot, get_page_info and capture_pdf. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000.
Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.
FAQ
Does this edit my hosts file?
No. The resolver rule is passed to and used by that Chromium process only.
Can I map a hostname to localhost and keep the original Host header?
Yes. The URL remains dev.example; the rule changes address resolution. Your web server still receives the hostname selected by the URL.
Will it work for HTTPS?
Resolution can work for HTTPS, but the certificate must be valid and trusted for the mapped hostname unless you explicitly ignore certificate errors.
Why does a wildcard mapping break unrelated requests?
MAP * applies to every hostname resolved by that browser. Use an exact rule or add exclusions.
Does the mapping affect requests made by Python?
No. It applies to Chromium’s host resolver. Python, your shell and other applications continue using their normal DNS configuration.


