headless_chrome (and older Puppeteer) wrap every CDP call inside
Target.sendMessageToTarget. Obscura was returning "Unknown Target
method" for the wrapper, which broke navigation through the high-level
client API even though the WebSocket itself connected.
Add a wrapper-level handler that:
1. Parses the inner JSON-encoded CDP request from params.message
2. Recursively dispatches it (boxed future to sidestep async-fn
recursion limits) using the wrapper's sessionId so routing works
3. Re-emits the inner response as a Target.receivedMessageFromTarget
event — the legacy contract these clients listen on instead of
correlating responses by id
4. Returns {} for the wrapper itself
This addresses the headless_chrome leg of #26. The chromiumoxide
"data did not match any variant of untagged enum Message" decode
failure is a separate event/response shape issue — needs a focused
investigation against a chromiumoxide repro that's not in the issue,
so it's left out of this PR.
Generated by Ora Studio
Vibe coded by ousamabenyounes
Co-authored-by: Ora Studio <[email protected]>
This commit is contained in:
1 parent
1c295f5b32
commit
f2ce18cfb7
1 file changed
+121
@@ -121,6 +121,14 @@ impl CdpContext {
|
||||
}
|
||||
|
||||
pub async fn dispatch(req: &CdpRequest, ctx: &mut CdpContext) -> CdpResponse {
|
||||
// headless_chrome (and older Puppeteer) wrap every CDP call inside
|
||||
// Target.sendMessageToTarget. Unwrap and recurse BEFORE acquiring the
|
||||
// V8 lock — the recursive dispatch will acquire it for the inner call,
|
||||
// and tokio Mutex is not reentrant.
|
||||
if req.method == "Target.sendMessageToTarget" {
|
||||
return dispatch_send_message_to_target(req, ctx).await;
|
||||
}
|
||||
|
||||
// Issue #19: V8 fatal abort under concurrent CDP work.
|
||||
//
|
||||
// Every CDP handler below may end up calling into a per-Page `JsRuntime`
|
||||
@@ -187,6 +195,63 @@ pub async fn dispatch(req: &CdpRequest, ctx: &mut CdpContext) -> CdpResponse {
|
||||
}
|
||||
}
|
||||
|
||||
async fn dispatch_send_message_to_target(req: &CdpRequest, ctx: &mut CdpContext) -> CdpResponse {
|
||||
let session_id = req
|
||||
.params
|
||||
.get("sessionId")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::to_string);
|
||||
let message = match req.params.get("message").and_then(|v| v.as_str()) {
|
||||
Some(m) => m,
|
||||
None => {
|
||||
return CdpResponse::error(
|
||||
req.id,
|
||||
-32602,
|
||||
"sendMessageToTarget requires a message string".into(),
|
||||
req.session_id.clone(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let inner: CdpRequest = match serde_json::from_str(message) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return CdpResponse::error(
|
||||
req.id,
|
||||
-32700,
|
||||
format!("sendMessageToTarget message is not a valid CDP request: {e}"),
|
||||
req.session_id.clone(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Override the inner session with the one supplied by the wrapper so
|
||||
// the inner dispatch routes against the right page. Boxing the future
|
||||
// sidesteps the async-fn recursion limit.
|
||||
let inner_with_session = CdpRequest {
|
||||
id: inner.id,
|
||||
method: inner.method.clone(),
|
||||
params: inner.params,
|
||||
session_id: session_id.clone().or(inner.session_id),
|
||||
};
|
||||
let inner_response = Box::pin(dispatch(&inner_with_session, ctx)).await;
|
||||
|
||||
// Re-emit the inner response as the legacy event headless_chrome (and
|
||||
// older Puppeteer) listen for instead of correlating responses by id.
|
||||
let inner_serialized = serde_json::to_string(&inner_response).unwrap_or_else(|_| "{}".into());
|
||||
ctx.pending_events.push(CdpEvent {
|
||||
method: "Target.receivedMessageFromTarget".to_string(),
|
||||
params: json!({
|
||||
"sessionId": session_id.clone().unwrap_or_default(),
|
||||
"message": inner_serialized,
|
||||
"targetId": session_id.clone().unwrap_or_default(),
|
||||
}),
|
||||
session_id: req.session_id.clone(),
|
||||
});
|
||||
|
||||
CdpResponse::success(req.id, json!({}), req.session_id.clone())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -217,4 +282,60 @@ mod tests {
|
||||
assert_eq!(err.code, -32601);
|
||||
assert!(err.message.contains("Unknown domain"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_message_to_target_unwraps_inner_call() {
|
||||
let mut ctx = CdpContext::new();
|
||||
let inner = json!({
|
||||
"id": 42,
|
||||
"method": "Browser.getVersion",
|
||||
"params": {},
|
||||
});
|
||||
let outer = CdpRequest {
|
||||
id: 99,
|
||||
method: "Target.sendMessageToTarget".into(),
|
||||
params: json!({
|
||||
"sessionId": "sess-1",
|
||||
"message": inner.to_string(),
|
||||
}),
|
||||
session_id: None,
|
||||
};
|
||||
|
||||
let resp = dispatch(&outer, &mut ctx).await;
|
||||
assert!(resp.error.is_none(), "wrapper must succeed: {:?}", resp.error);
|
||||
assert_eq!(resp.id, 99);
|
||||
assert_eq!(resp.result, Some(json!({})));
|
||||
|
||||
// headless_chrome reads the inner response from the
|
||||
// receivedMessageFromTarget event, not from the wrapper response.
|
||||
let evt = ctx
|
||||
.pending_events
|
||||
.iter()
|
||||
.find(|e| e.method == "Target.receivedMessageFromTarget")
|
||||
.expect("receivedMessageFromTarget event must be emitted");
|
||||
assert_eq!(evt.params["sessionId"], "sess-1");
|
||||
let inner_msg = evt.params["message"].as_str().expect("message is a string");
|
||||
let parsed: serde_json::Value = serde_json::from_str(inner_msg).unwrap();
|
||||
assert_eq!(parsed["id"], 42);
|
||||
// Browser.getVersion returns a populated result object, not an error.
|
||||
assert!(parsed.get("result").is_some(), "inner response carries result");
|
||||
assert!(parsed.get("error").is_none(), "inner response is not an error");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_message_to_target_rejects_invalid_message() {
|
||||
let mut ctx = CdpContext::new();
|
||||
let outer = CdpRequest {
|
||||
id: 5,
|
||||
method: "Target.sendMessageToTarget".into(),
|
||||
params: json!({
|
||||
"sessionId": "sess-1",
|
||||
"message": "{not valid json",
|
||||
}),
|
||||
session_id: None,
|
||||
};
|
||||
let resp = dispatch(&outer, &mut ctx).await;
|
||||
let err = resp.error.expect("malformed inner messages must error");
|
||||
assert_eq!(err.code, -32700);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user