Using Node.js is so 2020. Bun is the new kid on the block and I love it but it's not fulfilling the promise of being a Node.js drop in replacement just yet. While it's mostly good enough to use for everything from hobby projects to production apps you will run into hiccups once in a while. Trying to use Playwright to connect to an already running browser is one of those.
And it's not a new one. The issue on bun's repo has been open for over a year ago.
Why doesn't it work?
Playwright's chromium.connectOverCDP() uses a WebSocket connection underneath and utilizes the ws library to upgrade an HTTP request to a WebSocket.
Here's the first problem: Bun's HTTP client doesn't support protocol upgrades yet (HTTP → WebSocket) like Node's http does. That breaks manual WebSocket clients such as ws.
Bun usually mitigates by intercepting import 'ws' and providing a polyfill that wraps the platform WebSocket.
Here's the second problem: Playwright bundles ws during build, so there's no import 'ws' left for Bun to intercept.
Result: Bun can't connect and connectOverCDP() fails.
Why hasn't it been fixed yet?
Implementing the upgrade protocol in Bun's HTTP client would fix the issue. Unfortunately, it's far from a one line fix and not a high priority for the Bun team right now.
But wait, if there's a workaround to ws imports and playwright bundles it at build time couldn't we build the workaround into playwright directly when using Bun?
Yes, we could and here's a PR doing exactly that, however the playwright team decided it's best to wait for bun to support upgrades and that's why the PR is closed.
What do we do now?
We turn to one of the most fun features of our package manager - patch.
If you've never heard of patch before, here's a quick overview
Let's say we installed a package from the npm registry and it's useful enough that we don't want to write our own implementation from scratch but it's missing enough that you would wanna modify it.
Normally you would file an issue, discuss it with the maintainers and maybe even file a PR add/tweak the functionality you need. But what if your change was too proprietary for that or you didn't want to go back and forth with the maintainers or they just simply rejected your idea?
You could just go into your node_modules folder and apply the changes you need. But then you would have to manually keep those changes up to date with the latest version of the package. And all your deployments would have to include those changes somehow.
This is exactly what patch is for. It allows you to modify the package's code and then save that change and reapply it after every install. As long as it's present in patchedDependencies in your package.json.
How do I actually do this and what do I patch?
Let's start by initializing a new bun project
bun init
Now let's add playwright and browserbase
bun add playwright-core @browserbasehq/sdk
Browserbase is a service that allows you to run browsers in the cloud. I will use it in this example as that's how I originally stumbled onto this issue. But the patch also fixes connecting to a local browser and other providers.
Now let's create a .env file and paste Browserbase's API key and project ID into it.
BROWSERBASE_API_KEY=your_api_key BROWSERBASE_PROJECT_ID=your_project_id
Finally we can add some sample code to our index.ts file so we will have something to test our patch with.
import { chromium } from 'playwright-core'; import Browserbase from "@browserbasehq/sdk"; const bb = new Browserbase({ apiKey: process.env.BROWSERBASE_API_KEY }); const session = await bb.sessions.create({ projectId: process.env.BROWSERBASE_PROJECT_ID!, }); const browser = await chromium.connectOverCDP(session.connectUrl); const defaultContext = browser.contexts()[0]; const page = defaultContext?.pages()[0]; await page?.goto('https://www.example.com') console.log('Page title:', await page?.title()) await page?.close(); await browser.close();
Now comes the fun part. Let's patch playwright
bun patch playwright-core
Let's open node_modules/playwright-core/lib/utilsBundle.js and change
const ws = require("./utilsBundleImpl").ws;
to
const ws = 'Bun' in globalThis ? require('ws') : require('./utilsBundleImpl').ws;
One more command to run
bun patch --commit node_modules/playwright-core --patches-dir=.patches
This will create a .patches folder in your project root and add a playwright-core.patch file to it. It should look something like this
diff --git a/lib/utilsBundle.js b/lib/utilsBundle.js index 6833ed2edb6e23c0bb3603591adc5839c7e73846..409ff8ee5d7861b75f1db08dc61d9a881880631b 100644 --- a/lib/utilsBundle.js +++ b/lib/utilsBundle.js @@ -59,7 +59,7 @@ const ProgramOption = require("./utilsBundleImpl").ProgramOption; const progress = require("./utilsBundleImpl").progress; const SocksProxyAgent = require("./utilsBundleImpl").SocksProxyAgent; const yaml = require("./utilsBundleImpl").yaml; -const ws = require("./utilsBundleImpl").ws; +const ws = 'Bun' in globalThis ? require('ws') : require('./utilsBundleImpl').ws; const wsServer = require("./utilsBundleImpl").wsServer; const wsReceiver = require("./utilsBundleImpl").wsReceiver; const wsSender = require("./utilsBundleImpl").wsSender;
You should also be able to see the patch in your package.json as a patchedDependencies entry.
"patchedDependencies": { "playwright-core": ".patches/playwright-core.patch" }
And we're good to go, we can now run our code with
bun index.ts
We don't have to worry about manually changing playwright's code after every install as it will happen automatically.
If you want to see the full code, you can find it here.
Have fun and remember to playwright responsibly :)