Why does `connectOverCDP()` work in Node.js but fail in Bun with `ConnectionRefused`?

I’m trying to use Playwright’s connectOverCDP() to attach to an existing Chrome instance in debug mode on my M1 Mac. My setup works fine with Node.js, but using Bun I get the following error:

ConnectionRefused: overCDP: Unable to connect. Is the computer able to access the url?
Call log:
  - <ws preparing> retrieving websocket url from http://localhost:9222

Here’s my code:

import { chromium } from 'playwright';

const browser = await chromium.connectOverCDP('http://localhost:9222');
const defaultContext = browser.contexts()[0];
const page = await defaultContext.newPage();
await page.goto('https://google.com');
await page.waitForTimeout(5000);
await browser.close();

I launch Chrome with:

/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
--remote-debugging-port=9222 --no-first-run --no-default-browser-check \
--user-data-dir=chrome-user-data-dir --profile-directory=Default

How can I fix it so I can connect to the Chrome instance using Bun?

Playwright’s connectOverCDP() starts by requesting the DevTools WebSocket URL from http://localhost:9222/json/version.

In Node.js, this works fine because Node has a fully standard HTTP stack.

Bun’s built-in fetch and HTTP client currently have some differences in handling localhost, redirects, or non-200 responses.

If the request to http://localhost:9222/json/version fails, Playwright will throw ConnectionRefused.

Check manually:

curl http://localhost:9222/json/version

If this works, Node can fetch it.

In Bun, fetch(‘http://localhost:9222/json/version’) might fail due to Bun’s DNS or CORS restrictions.

On M1 Macs, Bun sometimes runs with sandboxing that prevents connecting to certain ports on localhost.

Node does not have this restriction.

Try running Bun with explicit network permissions:

bun --allow-net your-script.js

ConnectOverCDP() accepts a WebSocket URL too.

You can fetch it via Node or curl first:

curl http://localhost:9222/json/version

You’ll see something like:

{
  "webSocketDebuggerUrl": "ws://localhost:9222/devtools/browser/abcd1234"
}

Then, in Bun:

import { chromium } from 'playwright';

const browser = await chromium.connectOverCDP('ws://localhost:9222/devtools/browser/abcd1234');
const page = await browser.contexts()[0].newPage();
await page.goto('https://google.com');
await browser.close();

:white_check_mark: Using the WebSocket URL bypasses Bun’s HTTP fetch, which is likely the part that fails.