fix(cdp): unblock puppeteer connect, bound Runtime.evaluate, full TargetInfo (#181)

Three independent gaps that hurt browser-based MCP clients connecting
to obscura's CDP endpoint:

- Runtime.enable returned "No page" when called before a session was
  attached. puppeteer-extra's FrameManager.initialize fires it on the
  browser-level WebSocket BEFORE any page target exists, so the
  standard puppeteer.connect / browser.newPage flow died on connect.
  Now reply with {} when no session is attached and let the next
  Target.attachToTarget emit executionContextCreated.

- Runtime.evaluate had no per-call timeout. A long synchronous JS
  expression held the global V8 lock indefinitely and starved every
  other CDP command on the session. Honor the CDP `timeout` parameter
  and default to 30s (Chrome's protocolTimeout). Returns a clear error
  on expiry so client-side deadlines fire cleanly instead of hanging.

- Target.getTargetInfo for the browser target omitted canAccessOpener.
  Strict CDP clients (chromiumoxide) panic when the field is missing.
  Fill it in (false for the browser target itself).

Partial fix for #122. The remaining items (networkIdle hard-deadline
on SPAs, /json/list hardcoded data, docs on cheap-vs-queued methods)
need bigger or non-code changes and are deferred.
This commit is contained in:
SGavrl authored and GitHub committed 2026-05-24 20:14:35 +00:00
1 parent 71b9b4fbab
commit f4f49ebab4
2 files changed
+93 -21

No files matched your search

+25
View File
@@ -224,6 +224,9 @@ pub async fn handle(method: &str, params: &Value, ctx: &mut CdpContext) -> Resul
}))
}
None => {
// canAccessOpener is required on every TargetInfo per the
// CDP spec. Strict clients (chromiumoxide) panic if it's
// missing. The browser target itself has no opener.
Ok(json!({
"targetInfo": {
"targetId": "browser",
@@ -231,6 +234,7 @@ pub async fn handle(method: &str, params: &Value, ctx: &mut CdpContext) -> Resul
"title": "",
"url": "",
"attached": true,
"canAccessOpener": false,
}
}))
}
@@ -277,4 +281,25 @@ mod tests {
.expect_err("unknown methods must surface as errors");
assert!(err.contains("Unknown Target method"));
}
/// Regression for #122 item 5: every TargetInfo payload must carry the
/// `canAccessOpener` field. The browser-target branch of getTargetInfo
/// (no targetId passed → no page) used to omit it; strict CDP clients
/// like chromiumoxide panic when the field is missing.
#[tokio::test]
async fn get_target_info_browser_target_includes_can_access_opener() {
let mut ctx = CdpContext::new();
// No targetId → falls through to the browser-target branch.
let result = handle("getTargetInfo", &json!({}), &mut ctx)
.await
.expect("getTargetInfo with no targetId must return browser info");
let info = &result["targetInfo"];
assert_eq!(info["type"], "browser", "must be the browser target");
assert!(
info.get("canAccessOpener").is_some(),
"canAccessOpener must be present on every TargetInfo, got: {result}"
);
assert_eq!(info["canAccessOpener"], false);
}
}