Obscura CDP Compatibility — Zero-Change Migration for Puppeteer and Playwright
Obscura speaks CDP to replace Headless Chrome for Puppeteer and Playwright. Core CDP domains, lifecycle mapping, known differences, and migration checklist.
Why CDP Compatibility Matters
The Chrome DevTools Protocol (CDP) is the underlying protocol Puppeteer and Playwright use to communicate with the browser engine. If you already have scraping and automation scripts written for Puppeteer or Playwright, switching from Headless Chrome to Obscura requires changing only one line of connection code.
Obscura implements the core CDP domains covering the majority of everyday browser automation needs: page navigation, DOM queries, JS execution, network and cookie management, request interception, and input events.
Start the CDP Server
# Basic mode
obscura serve --port 9222
# With anti-detection
obscura serve --port 9222 --stealth
# Docker
docker run -d --name obscura -p 127.0.0.1:9222:9222 h4ckf0r0day/obscuraThe server binds to 127.0.0.1 by default for security. Set --host 0.0.0.0 for Docker.
Puppeteer
Use puppeteer-core (not puppeteer, which bundles Chrome):
npm install puppeteer-coreimport puppeteer from 'puppeteer-core';
const browser = await puppeteer.connect({
browserWSEndpoint: 'ws://127.0.0.1:9222/devtools/browser',
});
const page = await browser.newPage();
await page.goto('https://httpbin.org/anything');
const stories = await page.evaluate(() =>
Array.from(document.querySelectorAll('.titleline > a'))
.map(a => ({ title: a.textContent, url: a.href }))
);
console.log(stories);
// Request interception
await page.setRequestInterception(true);
page.on('request', req => {
if (['image', 'media', 'font'].includes(req.resourceType())) {
req.abort();
} else {
req.continue();
}
});
await browser.disconnect();Playwright
Use playwright-core:
npm install playwright-coreimport { chromium } from 'playwright-core';
const browser = await chromium.connectOverCDP('ws://127.0.0.1:9222');
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://httpbin.org/anything');
console.log(await page.title());
// Form submission
await page.goto('https://httpbin.org/post');
await page.fill('#username', 'admin');
await page.fill('#password', 'admin');
await page.click('form input[type="submit"]');
await browser.close();Note: Playwright uses connectOverCDP, taking the WebSocket endpoint as a string. Do not use connect, which speaks Playwright's private protocol that Obscura does not implement.
Obscura handles POST requests, follows 302 redirects, and keeps cookies consistent.
CDP Domain Coverage
Obscura implements the CDP domains most relevant to automation. The table below lists the main domains and representative methods so you can check whether the capabilities your script depends on are covered:
| Domain | Representative Methods |
|---|---|
| Target | createTarget, closeTarget, attachToTarget, createBrowserContext, disposeBrowserContext |
| Page | navigate, getFrameTree, addScriptToEvaluateOnNewDocument, lifecycleEvent |
| DOM | getDocument, querySelector, querySelectorAll, getOuterHTML, resolveNode |
| DOMSnapshot | captureSnapshot |
| Runtime | evaluate, callFunctionOn, getProperties, addBinding |
| Network | enable, setCookie, getCookies, setExtraHTTPHeaders, setUserAgentOverride |
| Fetch | enable, continueRequest, fulfillRequest, failRequest |
| Input | dispatchMouseEvent, dispatchKeyEvent |
| Storage | getCookies, setCookies, deleteCookies |
| Accessibility | getFullAXTree |
| Browser | getVersion, getWindowBounds, setWindowBounds |
Two things worth noting. First, DOMSnapshot primarily serves the perception layer of DOM-agent frameworks like browser-use. Second, page-to-Markdown conversion is exposed through the CLI's --dump markdown and the MCP browser_markdown tool, not through a custom CDP domain. If a method your script needs is not in the table, run your script once under obscura serve --verbose and check the logs for an unknown method message before deciding whether to extend Obscura yourself.
Lifecycle Event Mapping
Obscura's lifecycle state machine:
init → commit → domcontentloaded → load → networkidle2 → networkidle0waitUntil mapping:
| Puppeteer/Playwright | Obscura |
|---|---|
load (default) | load |
domcontentloaded | domcontentloaded |
networkidle0 | networkidle0 |
networkidle | networkidle0 |
Practical tip: when driven over CDP, the default waitUntil is domcontentloaded (rather than the CLI's load), to match the default expectation of Puppeteer and Playwright clients. If your page fills content asynchronously via XHR, explicitly setting waitUntil: 'networkidle0' is the safer choice.
Docker
Running Obscura in Docker:
# Start the container, expose the CDP port
docker run -d --name obscura \
-p 127.0.0.1:9222:9222 \
h4ckf0r0day/obscura
# Connect from the host
const browser = await puppeteer.connect({
browserWSEndpoint: 'ws://127.0.0.1:9222/devtools/browser',
});To connect from another container, put both on the same Docker network:
docker network create obscura-net
docker run -d --name obscura --network obscura-net h4ckf0r0day/obscura
docker run --rm --network obscura-net my-appKnown Differences
1. Shared V8 Isolate
All pages share one V8 Isolate, serialized through tokio::sync::Mutex. Target.createTarget is concurrent (returns immediately), but actual navigation runs asynchronously in the background. Many concurrent pages can therefore contend for V8. Keep concurrent page counts bounded, or use --workers N for multi-process isolation.
2. canAccessOpener
The TargetInfo payload includes canAccessOpener. Strict CDP clients (chromiumoxide) rely on this field and fail to parse the payload if it is missing. Obscura always includes it.
3. DevTools Panels
Obscura does not serve Performance, Memory, or other DevTools panels. Debugging relies on logs and CDP command responses.
4. setRequestInterception
Puppeteer's setRequestInterception(true) and Playwright's page.route() both work, backed by Fetch.enable under the hood.
5. file:// URLs
Disabled by default. Enable with --allow-file-access.
Migration Checklist
- Replace
puppeteerwithpuppeteer-core - Start
obscura serveinstead ofpuppeteer.launch() - Use
puppeteer.connect()instead ofpuppeteer.launch() - Verify you do not depend on Chrome DevTools panels
- Test request interception, cookies, and evaluate
- Enable
--stealthif needed
Summary
Obscura provides a highly CDP-compatible interface. For most scraping and automation scenarios, migration requires changing only one line of connection code. The extension mechanism allows adding new CDP domains and Web APIs on demand.
Need an enterprise proxy plan?
We can tailor architecture to your target domains, concurrency, and reliability goals.