fix: bypass proxy for CDP localhost connections (#31219)

When HTTP_PROXY / HTTPS_PROXY / ALL_PROXY environment variables are set,
CDP connections to localhost/127.0.0.1 can be incorrectly routed through
the proxy (e.g. via global-agent or undici proxy dispatcher), causing
browser control to fail.

Fix:
- New cdp-proxy-bypass module with utilities for direct localhost connections
- WebSocket (ws) CDP connections: pass explicit http.Agent to bypass any
  global proxy agent patching
- fetch-based CDP probes: wrap in withNoProxyForLocalhost() to temporarily
  set NO_PROXY for the duration of the call
- Playwright connectOverCDP: wrap in withNoProxyForLocalhost() since
  Playwright reads env vars internally
- 13 new tests covering getDirectAgentForCdp, hasProxyEnv, and
  withNoProxyForLocalhost (env save/restore, error recovery)
This commit is contained in:
Marcus Widing
2026-03-02 09:03:29 +01:00
committed by Peter Steinberger
parent 1184d39e1d
commit c96234b51d
5 changed files with 275 additions and 6 deletions

View File

@@ -1,6 +1,7 @@
import WebSocket from "ws";
import { isLoopbackHost } from "../gateway/net.js";
import { rawDataToString } from "../infra/ws.js";
import { getDirectAgentForCdp, withNoProxyForLocalhost } from "./cdp-proxy-bypass.js";
import { getChromeExtensionRelayAuthHeaders } from "./extension-relay.js";
export { isLoopbackHost };
@@ -122,7 +123,10 @@ async function fetchChecked(url: string, timeoutMs = 1500, init?: RequestInit):
const t = setTimeout(ctrl.abort.bind(ctrl), timeoutMs);
try {
const headers = getHeadersWithAuth(url, (init?.headers as Record<string, string>) || {});
const res = await fetch(url, { ...init, headers, signal: ctrl.signal });
// Bypass proxy for loopback CDP connections (#31219)
const res = await withNoProxyForLocalhost(() =>
fetch(url, { ...init, headers, signal: ctrl.signal }),
);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
@@ -146,9 +150,12 @@ export async function withCdpSocket<T>(
typeof opts?.handshakeTimeoutMs === "number" && Number.isFinite(opts.handshakeTimeoutMs)
? Math.max(1, Math.floor(opts.handshakeTimeoutMs))
: 5000;
// Bypass proxy for loopback CDP connections (#31219)
const agent = getDirectAgentForCdp(wsUrl);
const ws = new WebSocket(wsUrl, {
handshakeTimeout: handshakeTimeoutMs,
...(Object.keys(headers).length ? { headers } : {}),
...(agent ? { agent } : {}),
});
const { send, closeWithError } = createCdpSender(ws);