Playwright's `page.pdf()` issues a `Page.printToPDF` CDP request which
Obscura was not handling, so the dispatcher fell through to the generic
catch-all and clients saw:
Page.pdf: Protocol error (Page.printToPDF): Unknown Page method: printToPDF
This reads like an oversight rather than a deliberate limitation.
Obscura intentionally has no layout engine — option (2) from the issue —
so make the limitation explicit by handling the method and returning a
clear error that names the method, says why it isn't supported, and
points users at a working alternative (extract HTML via
Runtime.evaluate, then render to PDF in their client).
The behavior change is small but meaningful for DX: the JSON-RPC error
code stays -32601 (the dispatcher's mapping for `Err(_)`), but the
human-readable message becomes actionable instead of looking like a
forgotten method.
Adds a unit test that pins the regression: printToPDF must NOT fall
through to the "Unknown Page method" catch-all and must mention both
"not supported" and a workaround.
Generated by Ora Studio
Vibe coded by ousamabenyounes
Co-authored-by: Ora Studio <[email protected]>
obscura serve --user-agent <ua> only logged the value via tracing::info!
and never passed it to the CDP server, so every navigation kept using
the default Chrome UA string and httpbin.org/user-agent reported the
default rather than the override.
Plumb the option end-to-end:
- BrowserContext::with_full_options(id, proxy, stealth, user_agent)
initializes both BrowserContext::user_agent and the underlying
ObscuraHttpClient::user_agent RwLock at construction time so the
first navigation already sees the override.
- CdpContext::new_with_full_options forwards the UA to the default
BrowserContext.
- obscura_cdp::start_with_full_options accepts the UA and passes it
through cdp_processor.
- main.rs Serve arm calls start_with_full_options with the parsed UA.
- run_multi_worker_serve forwards --user-agent <ua> when spawning
worker subprocesses so multi-worker mode honors the flag too.
Existing two-arg start_with_options and three-arg with_options stay
as thin shims (call the full variant with None) so older callers and
the public crate API remain source-compatible.
Three new unit tests in obscura-browser cover the propagation, the
Chrome default fallback, and the legacy with_options path.
Generated by Ora Studio
Vibe coded by ousamabenyounes
Co-authored-by: Ora Studio <[email protected]>
Puppeteer's FrameManager.initialize calls Audits.enable on connect.
Without it puppeteer.connect() fails with "Unknown domain: Audits"
before any user code runs. Playwright doesn't enable Audits, which is
why the workaround in #57 sidesteps the issue.
Add Audits to the existing accepted-but-no-op domain list, alongside
Log/Performance/Security/etc. that already swallow Puppeteer's connect
chatter. No new module needed.
Generated by Ora Studio
Vibe coded by ousamabenyounes
Co-authored-by: Ora Studio <[email protected]>
Playwright (and recent Puppeteer) call Target.attachToBrowserTarget on
connect to obtain a session for the implicit "browser" target. Without
the handler the dispatcher returns "Unknown Target method:
attachToBrowserTarget" and the connect handshake aborts before any
user code runs.
Add a handler that mirrors Target.attachToTarget but for the browser
target: register a "browser-session" → "browser" mapping in
ctx.sessions, emit Target.attachedToTarget with type=browser, and
return the new session id.
Note: this addresses item (1) from #64. The Rocket Loader / Cloudflare
JS execution gap and Page.getLayoutMetrics are tracked separately
(getLayoutMetrics is fixed in PR #79).
Generated by Ora Studio
Vibe coded by ousamabenyounes
Co-authored-by: Ora Studio <[email protected]>
Playwright calls Page.getLayoutMetrics before every page.screenshot() to
size the capture. Without it the call fails with "Unknown Page method:
getLayoutMetrics" and screenshot() throws.
Obscura has no visual layout engine, so we return a fixed 1280x720
viewport (Chrome's default) and try to derive the content height from
document.documentElement.scrollHeight via the JS runtime, falling back
to the viewport height when no live page is bound.
Generated by Ora Studio
Vibe coded by ousamabenyounes
Co-authored-by: Ora Studio <[email protected]>
Addresses follow-up issues from PR #44 bot review:
- Add parentId field to AXNode (walk DOM ancestors to resolve AX parent)
- Omit optional fields when empty (name, value, properties, childIds)
instead of emitting null/empty arrays, per CDP spec
- Add ax_value_boolean() and ax_value_integer() helpers for consistent
AXValue construction across all property types
- Use typed helpers throughout compute_properties() instead of inline
JSON construction
CDP spec reference: Accessibility.AXNode requires parentId as AXNodeId,
and optional fields should be absent when not applicable.
Co-authored-by: zhangyang-crazy-one <[email protected]>
Fetch.enable was setting page.intercept_enabled to false and clearing
intercept_block_patterns, which caused should_block_url() to always
return false. As a result, Fetch.requestPaused events were never
triggered and Playwright's page.route() handlers never executed.
- Set intercept_enabled to true (was false)
- Propagate URL patterns to page.intercept_block_patterns (was cleared)
Fixes#49
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Per qodo-ai-reviewer feedback on PR #44: Accessibility.getFullAXTree
was emitting AXNode.role as a plain string and name/value as plain
strings or null, instead of CDP-compliant AXValue objects.
Changes:
- Add ax_value_role() and ax_value_string() helpers
- Change compute_name()/compute_value() to return Option<String>
- Wrap role, name, value as AXValue in build_ax_node()
The properties array already used the correct AXValue format.
Fixes#1: replace unwrap() panics in run_multi_worker_serve with match on the Result.
Fixes#2: route /json round-robin via worker_port instead of hardcoded port+1.
Fixes#3: return 502 Bad Gateway when a worker is unreachable instead of silently dropping the connection.
Fixes#5: thread --stealth through start_with_options -> CdpContext -> BrowserContext::with_options so the flag actually has an effect in serve mode (was let _ before). Log message now reflects the actual capability based on the stealth cargo feature being compiled in.
Fixes#15: try/catch the parentNode assignment in the attachShadow polyfill so it doesn't throw a TypeError against the read-only Node.parentNode getter (Cloudflare Turnstile triggers this path).
Brings Obscura's CDP server in line with what Playwright's
`chromium.connectOverCDP()` expects. Each fix is small and surgical;
together they make `connect → newContext → newPage → goto → page.title()`
work end-to-end against `https://example.com`.
1. **server.rs — emit pending events before command response.**
Chromium CDP semantics require side-effect events (e.g.
`Target.targetCreated` + `Target.attachedToTarget` from
`Target.createTarget`) to arrive *before* the originating command's
response. Playwright's wire reader assumes this ordering — it
resolves the response promise on the response and then synchronously
looks up the freshly-created session. Reversing the order makes that
lookup miss and the page hang at `newPage()`.
2. **domains/browser.rs — accept Browser.setWindowBounds as a no-op.**
Playwright calls `Browser.setWindowBounds` during teardown. Returning
"Unknown Browser method" causes Playwright to treat the target as
broken and tear it down mid-test, surfacing as "Frame has been
detached" on the next `page.title()`.
3. **obscura-browser/page.rs — frame_id == target_id for main frame.**
Was `format!("{id}.1", id)`. Playwright's frame manager indexes
frames by frame id and looks up the main frame by target id, so the
`.1` suffix made every main-frame lookup miss.
4. **domains/page.rs + dispatch.rs — track isolated worlds and re-emit
them post-navigation.** Playwright registers a utility world via
`Page.createIsolatedWorld` and expects `Runtime.executionContextCreated`
for each registered world after every navigation. Without re-emission,
the utility-world handle goes stale post-`goto` and `page.title()`
hangs waiting for the world's execution context. Falls back to the
legacy `__puppeteer_utility_world__24.40.0` name when no worlds have
been registered, preserving prior Puppeteer support.
5. **obscura-js/runtime.rs — strip trailing semicolons before wrapping.**
Playwright's utility-script expression is an IIFE ending in `})();`.
Both `wrap_expression` (CDP `Runtime.evaluate`) and `evaluate_for_cdp`
(the `meta_code` IIFE) embed the user expression inside `({EXPR})` /
`return ({EXPR});`. With the trailing `;` left in, this produces
`({...;})` — a SyntaxError. Parse errors are not catchable, so the
surrounding try/catch never fires and the call silently returns
`undefined`, surfacing as `page.title() === ""`.
Verified end-to-end against `https://example.com`:
[probe] connect OK
[probe] newContext OK
[probe] newPage OK
[probe] goto OK
[probe] title = Example Domain