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.

16Yun Engineering TeamJul 2, 20264 min read

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/obscura

The 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-core
import 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-core
import { 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:

DomainRepresentative Methods
TargetcreateTarget, closeTarget, attachToTarget, createBrowserContext, disposeBrowserContext
Pagenavigate, getFrameTree, addScriptToEvaluateOnNewDocument, lifecycleEvent
DOMgetDocument, querySelector, querySelectorAll, getOuterHTML, resolveNode
DOMSnapshotcaptureSnapshot
Runtimeevaluate, callFunctionOn, getProperties, addBinding
Networkenable, setCookie, getCookies, setExtraHTTPHeaders, setUserAgentOverride
Fetchenable, continueRequest, fulfillRequest, failRequest
InputdispatchMouseEvent, dispatchKeyEvent
StoragegetCookies, setCookies, deleteCookies
AccessibilitygetFullAXTree
BrowsergetVersion, 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 → networkidle0

waitUntil mapping:

Puppeteer/PlaywrightObscura
load (default)load
domcontentloadeddomcontentloaded
networkidle0networkidle0
networkidlenetworkidle0

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-app

Known 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

  1. Replace puppeteer with puppeteer-core
  2. Start obscura serve instead of puppeteer.launch()
  3. Use puppeteer.connect() instead of puppeteer.launch()
  4. Verify you do not depend on Chrome DevTools panels
  5. Test request interception, cookies, and evaluate
  6. Enable --stealth if 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.