How to fix Playwright net::ERR_NAME_NOT_RESOLVED in page.goto?
Playwrightnet::ERR_NAME_NOT_RESOLVED means the browser's DNS resolver could not find an IP address for the hostname in the URL. In Playwright this surfaces from page.goto() and is always a configuration or environment issue, not an application bug. The most common causes are a typo in the URL or baseURL, using a hostname that only exists on a VPN or internal network that isn't active in CI, or pointing at a local service with a .local or custom TLD that requires mDNS resolution unavailable inside containers.
Common mistake
test('loads staging app', async ({ page }) => {
// Typo: 'staging' should be a subdomain, not a different TLD
await page.goto('https://app.staging-example.com');
});
Also common in CI when BASE_URL is set locally but not in the CI environment:
// playwright.config.ts
export default {
use: {
baseURL: process.env.BASE_URL ?? 'https://staging.example.internal',
// ^^^ .internal TLD — no external DNS
},
};
The fix
Verify the hostname resolves from the environment running the tests, then fix config or network access:
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
// Prefer IP addresses for local services to skip DNS entirely
baseURL: process.env.BASE_URL ?? 'http://127.0.0.1:3000',
},
webServer: {
command: 'npm run start',
url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI,
},
});
For staging environments on internal networks:
// playwright.config.ts
export default defineConfig({
use: {
baseURL: process.env.CI
? 'https://staging.example.com' // External DNS — resolvable from CI
: 'https://staging.example.internal', // Internal DNS — resolvable locally
},
});
For debugging, confirm DNS resolution from the CI runner directly:
# Run this in your CI pipeline before tests
nslookup staging.example.com
# or
curl -I https://staging.example.com
ERR_NAME_NOT_RESOLVED when using a VPN
VPNs cause a specific version of this error that isn't a Playwright bug: split-tunnel DNS. A split-tunnel VPN routes traffic for certain IP ranges through the tunnel but leaves DNS queries pointed at your regular (ISP or local) resolver. The internal hostname is only known to the VPN's private DNS server, so the local resolver — the one Chromium actually asks — returns nothing, and page.goto() fails with ERR_NAME_NOT_RESOLVED even though the network route to the host works fine once you have an IP.
The same thing happens with a corporate HTTP(S) proxy that does its own name resolution: the hostname is never meant to resolve on the machine's public DNS at all, only inside the proxy.
Two ways to fix it without touching your VPN's network settings:
// playwright.config.ts — pin the hostname to a known IP inside Chromium
export default defineConfig({
use: {
launchOptions: {
args: ['--host-resolver-rules="MAP staging.internal.example.com 10.20.0.15"'],
},
},
});
// playwright.config.ts — route through the corporate proxy instead of local DNS
export default defineConfig({
use: {
proxy: {
server: 'http://proxy.corp.example.com:8080',
bypass: 'localhost,127.0.0.1', // comma-separated hosts to skip the proxy for
},
},
});
--host-resolver-rules tells Chromium to answer the DNS lookup for that one hostname itself, bypassing the OS resolver chain entirely — it doesn't matter whether the VPN's DNS server is reachable. The proxy option instead hands the whole request to a proxy server that resolves the name on its own side of the network, which is the standard behavior for corporate proxies that front internal-only domains.
To confirm you're looking at a split-tunnel problem rather than a VPN that's simply down, compare what your OS resolver returns against what the VPN's DNS server should return:
# macOS — shows which resolver serves which domain, and in what order
scutil --dns
# Linux — shows the active resolver and per-interface DNS servers
resolvectl status
If the internal domain isn't listed under any resolver's search domain, the VPN client isn't pushing DNS config for it — that's a VPN client setting (often "use this connection's DNS for these domains"), not something Playwright can work around except via --host-resolver-rules or a proxy.
Why it works
Playwright relies on Chromium's built-in DNS resolver, which uses the operating system's resolver chain. In Docker containers, /etc/resolv.conf may not include corporate DNS servers or VPN-specific resolvers, so hostnames that work on a developer's machine fail in CI. Using IP addresses for local services bypasses DNS entirely. Splitting baseURL by environment ensures the correct hostname is used based on which DNS infrastructure is available.
Tips
- If ERR_NAME_NOT_RESOLVED appears for localhost specifically, Chromium on some Linux configurations resolves localhost to ::1 (IPv6) while the server listens only on 127.0.0.1 (IPv4) — use 127.0.0.1 explicitly.
- Custom TLDs like .dev, .local, or .internal require specific DNS configuration that may not be present in ephemeral CI containers — use a real domain with a private DNS record or an IP address instead.
- If DNS resolves but the connection still fails, the problem has moved to the TCP layer — see ERR_CONNECTION_REFUSED.