ScreenshotNeo

BlogHow-to

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.

By the ScreenshotNeo team1 October 20267 min read

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 ignoreHTTPSErrors option exists and defaults to False; 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.