pub use cdp_server::{
parse_message, serialize_event, serialize_response, CdpError, CdpMessage, CdpResponse,
};
use cdp_server::{error_response, ok_response, ERR_METHOD_NOT_FOUND};
pub(crate) use cdp_server::ERR_INVALID_PARAMS;
use serde_json::Value;
use crate::devtools_dom;
use crate::servo_bridge::{
BridgeCommand, BridgeErrorKind, BridgeSender, FetchResolveAction, DEFAULT_BROWSER_CONTEXT_ID,
};
pub(crate) const ERR_NOT_SUPPORTED: i64 = -32000;
fn not_supported(method: &str, reason: &str) -> CdpError {
CdpError {
code: ERR_NOT_SUPPORTED,
message: format!("'{method}' not supported: {reason}"),
}
}
pub fn handle_command(
msg: CdpMessage,
target_id: &str,
params: &Option<Value>,
bridge: Option<&BridgeSender>,
) -> CdpResponse {
let parts: Vec<&str> = msg.method.splitn(2, '.').collect();
let domain = parts.first().copied().unwrap_or("");
let command = parts.get(1).copied().unwrap_or("");
let result = match domain {
"Target" => handle_target(command, target_id, params, bridge),
"Page" => handle_page(command, target_id, params, bridge),
"Runtime" => handle_runtime(command, target_id, params, bridge),
"DOM" => handle_dom(command, target_id, params, bridge),
"Network" => handle_network(command, target_id, params, bridge),
"CSS" => handle_css(command, target_id, params, bridge),
"Emulation" => handle_emulation(command, target_id, params, bridge),
"Input" => handle_input(command, target_id, params, bridge),
"Overlay" => handle_overlay(command, target_id, params, bridge),
"Debugger" => handle_debugger(command, target_id, params, bridge),
"Log" => handle_log(command),
"Fetch" => handle_fetch(command, target_id, params, bridge),
"Storage" => handle_storage(command, target_id, params, bridge),
"Security" => handle_security(command, target_id, params, bridge),
"Profiler" => handle_profiler(command, target_id, params, bridge),
"HeapProfiler" => handle_heap_profiler(command, target_id, bridge),
"Memory" => handle_memory(command, target_id, bridge),
"Performance" => handle_performance(command, target_id, bridge),
"SystemInfo" => handle_system_info(command),
"ServiceWorker" => handle_service_worker(command, target_id, params, bridge),
"Browser" => handle_browser(command),
_ => Err(CdpError {
code: ERR_METHOD_NOT_FOUND,
message: format!("'{}' wasn't found", msg.method),
}),
};
match result {
Ok(r) => ok_response(msg.id, r),
Err(e) => error_response(msg.id, e.code, e.message),
}
}
pub(crate) type HandlerResult = Result<Value, CdpError>;
fn params_str(params: &Option<Value>, key: &str) -> String {
params
.as_ref()
.and_then(|p| p.get(key))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string()
}
fn bridge_send(bridge: Option<&BridgeSender>, cmd: BridgeCommand) -> HandlerResult {
match bridge {
Some(b) => {
let resp = b.send(cmd);
resp.result.map_err(|e| CdpError {
code: match e.kind {
BridgeErrorKind::NoBridge => -32603,
BridgeErrorKind::DomainError => ERR_NOT_SUPPORTED,
},
message: e.message,
})
}
None => Err(CdpError {
code: -32603,
message: "no servo bridge connected".into(),
}),
}
}
fn ok_empty() -> HandlerResult {
Ok(serde_json::json!({}))
}
pub(crate) fn eval_json(
bridge: Option<&BridgeSender>,
tid: &str,
expression: &str,
) -> HandlerResult {
let resp = bridge_send(
bridge,
BridgeCommand::EvaluateJs {
target_id: tid.to_string(),
expression: expression.to_string(),
return_by_value: true,
},
)?;
let extracted = resp.get("result").and_then(|r| r.get("value")).cloned();
let extracted = match extracted {
Some(Value::String(ref text)) => match serde_json::from_str::<Value>(text) {
Ok(v) => Some(v),
Err(_) => Some(Value::String(text.clone())),
},
other => other,
};
extracted
.filter(|v| v.is_object())
.ok_or_else(|| CdpError {
code: ERR_NOT_SUPPORTED,
message: "page query failed: target did not return a JSON document".into(),
})
}
fn live_target_info(
target_id: &str,
bridge: Option<&BridgeSender>,
context_hint: Option<&str>,
) -> Value {
let title = bridge
.and_then(|b| {
b.send(BridgeCommand::GetTitle {
target_id: target_id.to_string(),
})
.result
.ok()
})
.and_then(|v| v.as_str().map(|s| s.to_string()))
.unwrap_or_else(|| "Bao".into());
let url = bridge
.and_then(|b| {
b.send(BridgeCommand::GetUrl {
target_id: target_id.to_string(),
})
.result
.ok()
})
.and_then(|v| v.as_str().map(|s| s.to_string()))
.unwrap_or_else(|| "about:blank".into());
serde_json::json!({
"targetId": target_id,
"type": "page",
"title": title,
"url": url,
"attached": true,
"browserContextId": context_hint
.filter(|s| !s.is_empty())
.unwrap_or(DEFAULT_BROWSER_CONTEXT_ID),
})
}
fn target_context_id(target_id: &str, bridge: Option<&BridgeSender>) -> String {
bridge
.and_then(|b| {
b.send(BridgeCommand::GetTargetContextId {
target_id: target_id.to_string(),
})
.result
.ok()
})
.and_then(|v| {
v.get("browserContextId")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
})
.unwrap_or_else(|| DEFAULT_BROWSER_CONTEXT_ID.to_string())
}
fn handle_target(
command: &str,
target_id: &str,
params: &Option<Value>,
bridge: Option<&BridgeSender>,
) -> HandlerResult {
match command {
"getTargets" | "getTargetTargets" => {
let mut target_infos: Vec<Value> = Vec::new();
let listed = bridge
.and_then(|b| b.send(BridgeCommand::ListTargets).result.ok())
.and_then(|v| v.as_array().cloned());
match listed {
Some(entries) => {
for entry in entries {
let id = entry.get("id").and_then(|v| v.as_str()).unwrap_or("");
let context = entry
.get("browserContextId")
.and_then(|v| v.as_str())
.unwrap_or(DEFAULT_BROWSER_CONTEXT_ID);
target_infos.push(live_target_info(id, bridge, Some(context)));
}
}
None => target_infos.push(live_target_info(target_id, bridge, None)),
}
push_worker_target_infos(&mut target_infos, target_id, bridge);
Ok(serde_json::json!({ "targetInfos": target_infos }))
}
"createTarget" => {
let url = params
.as_ref()
.and_then(|v| v.get("url"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.unwrap_or("about:blank");
let context_id = params
.as_ref()
.and_then(|v| v.get("browserContextId"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty());
match context_id {
Some(ctx) => bridge_send(
bridge,
BridgeCommand::CreateTargetInContext {
url: url.to_string(),
browser_context_id: ctx.to_string(),
},
),
None => bridge_send(
bridge,
BridgeCommand::CreateTarget {
url: url.to_string(),
},
),
}
}
"createBrowserContext" => bridge_send(bridge, BridgeCommand::CreateBrowserContext),
"disposeBrowserContext" => {
let context_id = params
.as_ref()
.and_then(|v| v.get("browserContextId"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.ok_or_else(|| CdpError {
code: ERR_INVALID_PARAMS,
message: "'Target.disposeBrowserContext' requires a non-empty browserContextId param"
.into(),
})?;
bridge_send(
bridge,
BridgeCommand::DisposeBrowserContext {
browser_context_id: context_id.to_string(),
},
)
}
"getBrowserContexts" => bridge_send(bridge, BridgeCommand::ListBrowserContexts),
"closeTarget" => {
let tid = params
.as_ref()
.and_then(|v| v.get("targetId"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.unwrap_or(target_id);
bridge_send(
bridge,
BridgeCommand::ClosePage {
target_id: tid.to_string(),
},
)?;
Ok(serde_json::json!({ "success": true }))
}
"setAutoAttach" | "setDiscoverTargets" => ok_empty(),
"getTargetInfo" => {
let tid = params
.as_ref()
.and_then(|v| v.get("targetId"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.unwrap_or(target_id);
let context = target_context_id(tid, bridge);
Ok(serde_json::json!({
"targetInfo": live_target_info(tid, bridge, Some(&context))
}))
}
"attachToTarget" => Err(not_supported(
"Target.attachToTarget",
"session minting requires the WS session registry (bao_browser); the stateless internal backend has no session table",
)),
"detachFromTarget" | "sendMessageToTarget" => Err(not_supported(
&format!("Target.{command}"),
"session routing requires the WS session registry (bao_browser); the stateless internal backend has no session table",
)),
_ => Err(CdpError {
code: -32601,
message: format!("'Target.{}' wasn't found", command),
}),
}
}
fn push_worker_target_infos(
target_infos: &mut Vec<Value>,
target_id: &str,
bridge: Option<&BridgeSender>,
) {
let Some(b) = bridge else {
return;
};
let Ok(worker_resp) = b
.send(BridgeCommand::ListWorkerTargets {
target_id: target_id.to_string(),
})
.result
else {
return;
};
let Some(workers) = worker_resp.get("workerTargets").and_then(|v| v.as_array()) else {
return;
};
for w in workers {
target_infos.push(w.clone());
}
}
fn handle_page(
command: &str,
target_id: &str,
params: &Option<Value>,
bridge: Option<&BridgeSender>,
) -> HandlerResult {
let tid = target_id.to_string();
match command {
"enable" | "disable" => ok_empty(),
"setLifecycleEventsEnabled" => ok_empty(),
"navigate" => {
let url = params
.as_ref()
.and_then(|v| v.get("url"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.unwrap_or("about:blank");
bridge_send(
bridge,
BridgeCommand::Navigate {
target_id: tid,
url: url.to_string(),
},
)
}
"reload" => {
let ignore_cache = params
.as_ref()
.and_then(|p| p.get("ignoreCache"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
bridge_send(
bridge,
BridgeCommand::Reload {
target_id: tid,
ignore_cache,
},
)
}
"getFrameTree" => {
let mut frame = eval_json(
bridge,
&tid,
r#"(function(){ return JSON.stringify({
url: location.href,
mimeType: document.contentType,
name: window.name,
securityOrigin: location.origin
}); })()"#,
)?;
if let Some(obj) = frame.as_object_mut() {
obj.insert(
"id".into(),
serde_json::json!(crate::servo_bridge::main_frame_id_for_target(&tid)),
);
}
Ok(serde_json::json!({ "frameTree": { "frame": frame } }))
}
"getNavigationHistory" => Err(not_supported(
"Page.getNavigationHistory",
"servo WebView does not expose session-history entry enumeration (only can_go_back/go_forward traversal)",
)),
"captureScreenshot" => {
let format = params
.as_ref()
.and_then(|p| p.get("format"))
.and_then(|v| v.as_str())
.unwrap_or("png")
.to_string();
let quality = params
.as_ref()
.and_then(|p| p.get("quality"))
.and_then(|v| v.as_u64())
.map(|q| q as u8);
bridge_send(
bridge,
BridgeCommand::TakeScreenshot {
target_id: tid,
format,
quality,
},
)
}
"setContent" => {
let html = params_str(params, "html");
if html.is_empty() {
return Err(CdpError {
code: ERR_INVALID_PARAMS,
message: "'Page.setContent' requires a non-empty html param".into(),
});
}
let js = format!(
r#"(function() {{ document.open(); document.write({}); document.close(); }})()"#,
serde_json::to_string(&html).unwrap_or_default(),
);
bridge_send(
bridge,
BridgeCommand::EvaluateJs {
target_id: tid,
expression: js,
return_by_value: true,
},
)?;
ok_empty()
}
"close" => {
bridge_send(bridge, BridgeCommand::ClosePage { target_id: tid })?;
ok_empty()
}
"bringToFront" => {
bridge_send(
bridge,
BridgeCommand::EvaluateJs {
target_id: tid,
expression: "window.focus()".into(),
return_by_value: true,
},
)?;
ok_empty()
}
"getLayoutMetrics" => {
let m = eval_json(
bridge,
&tid,
r#"(function(){ var d = document.documentElement;
return JSON.stringify({
iw: window.innerWidth, ih: window.innerHeight,
cw: d.scrollWidth, ch: d.scrollHeight
}); })()"#,
)?;
Ok(serde_json::json!({
"layoutViewport": { "x": 0, "y": 0, "width": m["iw"], "height": m["ih"] },
"contentSize": { "x": 0, "y": 0, "width": m["cw"], "height": m["ch"] },
"cssLayoutViewport": { "x": 0, "y": 0, "width": m["iw"], "height": m["ih"] },
"cssContentSize": { "x": 0, "y": 0, "width": m["cw"], "height": m["ch"] }
}))
}
"addScriptToEvaluateOnNewDocument" => {
let source = params_str(params, "source");
bridge_send(
bridge,
BridgeCommand::AddScriptToEvaluateOnNewDocument {
target_id: tid,
source,
},
)
}
"removeScriptToEvaluateOnNewDocument" => {
let identifier = params_str(params, "identifier");
if identifier.is_empty() {
return Err(CdpError {
code: ERR_INVALID_PARAMS,
message: "'Page.removeScriptToEvaluateOnNewDocument' requires an identifier param"
.into(),
});
}
bridge_send(
bridge,
BridgeCommand::RemoveScriptToEvaluateOnNewDocument {
target_id: tid,
identifier,
},
)
}
_ => Err(CdpError {
code: -32601,
message: format!("'Page.{}' wasn't found", command),
}),
}
}
fn handle_runtime(
command: &str,
target_id: &str,
params: &Option<Value>,
bridge: Option<&BridgeSender>,
) -> HandlerResult {
let tid = target_id.to_string();
match command {
"enable" => ok_empty(),
"disable" => ok_empty(),
"evaluate" => {
let expression = params
.as_ref()
.and_then(|p| p.get("expression"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let return_by_value = params
.as_ref()
.and_then(|p| p.get("returnByValue"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
if bridge.is_some() && !expression.is_empty() {
let envelope = bridge_send(
bridge,
BridgeCommand::EvaluateJs {
target_id: tid,
expression,
return_by_value,
},
)?;
let mut out = serde_json::json!({ "result": envelope["result"] });
if let Some(details) = envelope.get("exceptionDetails") {
out["exceptionDetails"] = details.clone();
}
Ok(out)
} else {
Ok(
serde_json::json!({ "result": { "type": "undefined" }, "exceptionDetails": null }),
)
}
}
"callFunctionOn" => {
let object_id = params
.as_ref()
.and_then(|p| p.get("objectId"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let function_declaration = params
.as_ref()
.and_then(|p| p.get("functionDeclaration"))
.and_then(|v| v.as_str())
.unwrap_or("function(){}")
.to_string();
let arguments = params.as_ref().and_then(|p| p.get("arguments")).cloned();
let return_by_value = params
.as_ref()
.and_then(|p| p.get("returnByValue"))
.and_then(|v| v.as_bool());
let execution_context_id = params
.as_ref()
.and_then(|p| p.get("executionContextId"))
.and_then(|v| v.as_i64());
let await_promise = params
.as_ref()
.and_then(|p| p.get("awaitPromise"))
.and_then(|v| v.as_bool());
let object_group = params
.as_ref()
.and_then(|p| p.get("objectGroup"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if bridge.is_some() {
bridge_send(
bridge,
BridgeCommand::RuntimeCallFunctionOn {
target_id: tid,
object_id,
execution_context_id,
function_declaration,
arguments,
return_by_value,
await_promise,
object_group,
},
)
} else {
Ok(serde_json::json!({ "result": { "type": "undefined" } }))
}
}
"getProperties" => {
let object_id = params
.as_ref()
.and_then(|p| p.get("objectId"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let own_properties = params
.as_ref()
.and_then(|p| p.get("ownProperties"))
.and_then(|v| v.as_bool());
if bridge.is_some() && !object_id.is_empty() {
bridge_send(
bridge,
BridgeCommand::RuntimeGetProperties {
target_id: tid,
object_id,
own_properties,
},
)
} else {
Ok(serde_json::json!({ "result": [] }))
}
}
"evaluateAsync" | "runScript" => {
Ok(serde_json::json!({ "result": { "type": "undefined" } }))
}
"releaseObject" => {
let object_id = params
.as_ref()
.and_then(|p| p.get("objectId"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if bridge.is_some() && !object_id.is_empty() {
bridge_send(
bridge,
BridgeCommand::RuntimeReleaseObject {
target_id: tid,
object_id,
},
)
} else {
ok_empty()
}
}
"releaseObjectGroup" => {
let object_group = params
.as_ref()
.and_then(|p| p.get("objectGroup"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if bridge.is_some() && !object_group.is_empty() {
bridge_send(
bridge,
BridgeCommand::RuntimeReleaseObjectGroup {
target_id: tid,
object_group,
},
)
} else {
ok_empty()
}
}
"compileScript" | "callArgument" => ok_empty(),
"runIfWaitingForDebugger" => ok_empty(),
_ => Err(CdpError {
code: -32601,
message: format!("'Runtime.{}' wasn't found", command),
}),
}
}
fn handle_dom(
command: &str,
target_id: &str,
params: &Option<Value>,
bridge: Option<&BridgeSender>,
) -> HandlerResult {
let tid = target_id.to_string();
match command {
"enable" | "disable" => ok_empty(),
"getDocument" => bridge_send(
bridge,
BridgeCommand::GetDocument {
target_id: tid.clone(),
},
),
"describeNode" => {
let node_ref = devtools_dom::require_node_ref(params)?;
let depth = params
.as_ref()
.and_then(|p| p.get("depth"))
.and_then(|v| v.as_i64())
.unwrap_or(1);
devtools_dom::eval_dom(
bridge,
&tid,
&devtools_dom::describe_node_body(&node_ref, depth),
)
}
"querySelector" => {
let selector = devtools_dom::require_selector(params)?;
devtools_dom::eval_dom(
bridge,
&tid,
&devtools_dom::query_selector_body(&selector),
)
}
"querySelectorAll" => {
let selector = devtools_dom::require_selector(params)?;
devtools_dom::eval_dom(
bridge,
&tid,
&devtools_dom::query_selector_all_body(&selector),
)
}
"getBoxModel" => {
let node_ref = devtools_dom::require_node_ref(params)?;
devtools_dom::eval_dom(bridge, &tid, &devtools_dom::box_model_body(&node_ref))
}
"getFlattenedDocument" => {
let depth = params
.as_ref()
.and_then(|p| p.get("depth"))
.and_then(|v| v.as_i64())
.unwrap_or(1);
devtools_dom::eval_dom(
bridge,
&tid,
&devtools_dom::flattened_document_body(depth),
)
}
"getNodeForOwner" => {
let object_id = devtools_dom::require_object_id(params)?;
devtools_dom::eval_dom(
bridge,
&tid,
&devtools_dom::node_for_owner_body(&object_id),
)
}
"setAttributeValue" => {
let node_id = params
.as_ref()
.and_then(|p| p.get("nodeId"))
.and_then(|v| v.as_i64())
.unwrap_or(0);
let name = params_str(params, "name");
let value = params_str(params, "value");
bridge_send(
bridge,
BridgeCommand::SetAttributeValue {
target_id: tid.clone(),
node_id,
name,
value,
},
)
}
"removeAttribute" | "setOuterHTML" | "insertBefore" | "removeNode" => Err(not_supported(
&format!("DOM.{}", command),
"no mutation delivery path exists on this face (the servo embedder bridge carries no such write command)",
)),
"getOuterHTML" => {
let node_id = params
.as_ref()
.and_then(|p| p.get("nodeId"))
.and_then(|v| v.as_i64());
bridge_send(
bridge,
BridgeCommand::GetOuterHtml {
target_id: tid,
node_id,
},
)
}
"resolveNode" => {
let node_ref = devtools_dom::require_node_ref(params).or_else(|_| {
let backend = params
.as_ref()
.and_then(|p| p.get("backendNodeId"))
.and_then(|v| v.as_i64())
.ok_or_else(|| CdpError {
code: ERR_INVALID_PARAMS,
message: "missing required parameter: nodeId, objectId or backendNodeId"
.into(),
})?;
Ok(devtools_dom::NodeRef::NodeId(backend))
})?;
let object_group = params
.as_ref()
.and_then(|p| p.get("objectGroup"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
devtools_dom::eval_dom(
bridge,
&tid,
&devtools_dom::resolve_node_body(&node_ref, object_group.as_deref()),
)
}
"pushNodesByBackendIdsToFrontend" => {
let backend_ids = params
.as_ref()
.and_then(|p| p.get("backendNodeIds"))
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_i64())
.collect::<Vec<i64>>()
})
.ok_or_else(|| CdpError {
code: ERR_INVALID_PARAMS,
message: "missing required parameter: backendNodeIds".into(),
})?;
devtools_dom::eval_dom(
bridge,
&tid,
&devtools_dom::push_nodes_body(&backend_ids),
)
}
_ => Err(CdpError {
code: -32601,
message: format!("'DOM.{}' wasn't found", command),
}),
}
}
fn handle_network(
command: &str,
target_id: &str,
params: &Option<Value>,
bridge: Option<&BridgeSender>,
) -> HandlerResult {
let tid = target_id.to_string();
match command {
"enable" => {
if bridge.is_some() {
bridge_send(bridge, BridgeCommand::NetworkEnable { target_id: tid })?;
}
ok_empty()
}
"disable" => {
if bridge.is_some() {
bridge_send(bridge, BridgeCommand::NetworkDisable { target_id: tid })?;
}
ok_empty()
}
"getCookies" => {
let urls: Vec<String> = params
.as_ref()
.and_then(|p| p.get("urls"))
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
if bridge.is_some() {
bridge_send(
bridge,
BridgeCommand::GetCookies {
target_id: tid,
urls,
},
)
} else {
Ok(serde_json::json!({ "cookies": [] }))
}
}
"getAllCookies" => {
if bridge.is_some() {
bridge_send(bridge, BridgeCommand::GetAllCookies { target_id: tid })
} else {
Ok(serde_json::json!({ "cookies": [] }))
}
}
"setCookie" => {
let name = params_str(params, "name");
let value = params_str(params, "value");
let url = params
.as_ref()
.and_then(|p| p.get("url"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let domain = params
.as_ref()
.and_then(|p| p.get("domain"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if bridge.is_some() {
bridge_send(
bridge,
BridgeCommand::SetCookie {
target_id: tid,
name,
value,
url,
domain,
},
)
} else {
Ok(serde_json::json!({ "success": true }))
}
}
"deleteCookies" => {
let name = params_str(params, "name");
let url = params
.as_ref()
.and_then(|p| p.get("url"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if bridge.is_some() {
bridge_send(
bridge,
BridgeCommand::DeleteCookie {
target_id: tid,
name,
url,
},
)
} else {
ok_empty()
}
}
"getResponseBody" => {
let request_id = params_str(params, "requestId");
bridge_send(
bridge,
BridgeCommand::GetResponseBody {
target_id: tid,
request_id,
},
)
}
"setCacheDisabled" => {
let cache_disabled = params
.as_ref()
.and_then(|p| p.get("cacheDisabled"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
if bridge.is_some() {
bridge_send(
bridge,
BridgeCommand::NetworkSetCacheDisabled {
target_id: tid,
cache_disabled,
},
)?;
}
ok_empty()
}
"setExtraHTTPHeaders" => {
let headers = params
.as_ref()
.and_then(|p| p.get("headers"))
.cloned()
.unwrap_or(serde_json::json!({}));
bridge_send(
bridge,
BridgeCommand::NetworkSetExtraHTTPHeaders {
target_id: tid,
headers,
},
)
}
"clearBrowserCache" => {
if bridge.is_some() {
bridge_send(
bridge,
BridgeCommand::NetworkClearBrowserCache { target_id: tid },
)?;
}
ok_empty()
}
"clearBrowserCookies" => {
if bridge.is_some() {
bridge_send(
bridge,
BridgeCommand::NetworkClearBrowserCookies { target_id: tid },
)?;
}
ok_empty()
}
"setCookies" => {
let cookies = params
.as_ref()
.and_then(|p| p.get("cookies"))
.and_then(|v| v.as_array())
.cloned()
.ok_or_else(|| CdpError {
code: ERR_INVALID_PARAMS,
message: "missing required parameter: cookies".into(),
})?;
for c in &cookies {
let name = c
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let value = c
.get("value")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let url = c.get("url").and_then(|v| v.as_str()).map(|s| s.to_string());
let domain = c
.get("domain")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if bridge.is_some() {
bridge_send(
bridge,
BridgeCommand::SetCookie {
target_id: tid.clone(),
name,
value,
url,
domain,
},
)?;
}
}
ok_empty()
}
"setUserAgentOverride" => {
let ua = params_str(params, "userAgent");
let accept_language = params
.as_ref()
.and_then(|p| p.get("acceptLanguage"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let platform = params
.as_ref()
.and_then(|p| p.get("platform"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if bridge.is_none() {
return Err(CdpError {
code: -32603,
message: "no servo bridge connected".into(),
});
}
if !ua.is_empty() {
bridge_send(
bridge,
BridgeCommand::SetUserAgent {
target_id: tid.clone(),
user_agent: ua,
},
)?;
}
if accept_language.is_some() || platform.is_some() {
let mut js = String::from("(function(){var d=Object.defineProperty;");
if let Some(lang) = &accept_language {
js.push_str(&format!(
"d(navigator,'language',{{get:function(){{return {};}}}});",
serde_json::to_string(lang).unwrap_or_default()
));
}
if let Some(p) = &platform {
js.push_str(&format!(
"d(navigator,'platform',{{get:function(){{return {};}}}});",
serde_json::to_string(p).unwrap_or_default()
));
}
js.push_str("})();");
bridge_send(
bridge,
BridgeCommand::EvaluateJs {
target_id: tid.clone(),
expression: js,
return_by_value: false,
},
)?;
}
ok_empty()
}
"overrideNetworkState" => {
let offline = params
.as_ref()
.and_then(|p| p.get("offline"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
if bridge.is_some() {
let js = format!(
"(function(){{Object.defineProperty(navigator,'onLine',\
{{get:function(){{return {};}}}});}})();",
!offline
);
bridge_send(
bridge,
BridgeCommand::EvaluateJs {
target_id: tid.clone(),
expression: js,
return_by_value: false,
},
)?;
}
ok_empty()
}
"canClearBrowserCache" => Ok(serde_json::json!({ "result": true })),
"canClearBrowserCookies" => Ok(serde_json::json!({ "result": true })),
"canEmulateNetworkConditions" => Ok(serde_json::json!({ "result": false })),
"getRequestPostData" => Err(not_supported(
"Network.getRequestPostData",
"servo does not store request post data for embedder access (same class as Network.getResponseBody)",
)),
"getCertificate" => Err(not_supported(
"Network.getCertificate",
"servo does not expose TLS certificates to the embedder",
)),
"getResponseBodyForInterception" => Err(not_supported(
"Network.getResponseBodyForInterception",
"request interception is not implemented โ no interceptionId can exist",
)),
"takeResponseBodyForInterceptionAsStream" => Err(not_supported(
"Network.takeResponseBodyForInterceptionAsStream",
"request interception is not implemented โ no stream handle can exist",
)),
"searchInResponseBody" => Err(not_supported(
"Network.searchInResponseBody",
"servo does not store response bodies for embedder access",
)),
"streamResourceContent" => Err(not_supported(
"Network.streamResourceContent",
"per-request response streaming state is not tracked by the bridge",
)),
"replayXHR" => Err(not_supported(
"Network.replayXHR",
"servo does not expose XHR replay to the embedder",
)),
"setBlockedURLs" => Err(not_supported(
"Network.setBlockedURLs",
"the servo net stack has no embedder-visible request blocklist",
)),
"setBypassServiceWorker" => Err(not_supported(
"Network.setBypassServiceWorker",
"no runtime service-worker bypass toggle on the servo embedding face",
)),
"emulateNetworkConditionsByRule" => Err(not_supported(
"Network.emulateNetworkConditionsByRule",
"no network-condition emulation facility exists (Network.canEmulateNetworkConditions answers false)",
)),
"configureDurableMessages" => Err(not_supported(
"Network.configureDurableMessages",
"response bodies are not buffered outside the renderer (Network.getResponseBody reports the same limitation)",
)),
"setAttachDebugStack" => Err(not_supported(
"Network.setAttachDebugStack",
"script stack ids are not attached to network requests",
)),
"getSecurityIsolationStatus" => Err(not_supported(
"Network.getSecurityIsolationStatus",
"COEP/COOP/CSP isolation state is not exposed by the servo embedding face",
)),
"enableReportingApi" => Err(not_supported(
"Network.enableReportingApi",
"Reporting API tracking is not implemented โ no reports can be delivered",
)),
"enableDeviceBoundSessions" => Err(not_supported(
"Network.enableDeviceBoundSessions",
"device-bound session tracking is not implemented",
)),
"fetchSchemefulSite" => Err(not_supported(
"Network.fetchSchemefulSite",
"schemeful-site computation requires the public-suffix list, which is not linked into bao_cdp",
)),
"loadNetworkResource" => Err(not_supported(
"Network.loadNetworkResource",
"embedder-side network fetch is not wired to the CDP face",
)),
"setCookieControls" => Err(not_supported(
"Network.setCookieControls",
"third-party-cookie restriction is not runtime-controllable on the servo embedding face",
)),
"setAcceptedEncodings" | "clearAcceptedEncodingsOverride" => ok_empty(),
"setRequestInterception" => {
let patterns = parse_interception_patterns(
params,
PatternFace::LegacyNetwork,
"Network.setRequestInterception",
)?;
bridge_send(
bridge,
BridgeCommand::FetchEnable {
target_id: tid,
patterns,
legacy_network_events: true,
},
)
}
"continueInterceptedRequest" => {
let interception_id = interception_id(params, "Network.continueInterceptedRequest")?;
reject_request_modification("Network.continueInterceptedRequest", params)?;
if params.as_ref().is_some_and(|p| p.get("rawResponse").is_some()) {
return Err(not_supported(
"Network.continueInterceptedRequest",
"raw HTTP wire-format response reassembly is phase-2 โ the servo pause face answers with Fetch.fulfillRequest-shaped synthesized responses, not raw response bytes (design ยง7)",
));
}
let action = match params.as_ref().and_then(|p| p.get("errorReason")) {
Some(Value::String(s)) => {
FetchResolveAction::Fail { error_reason: Some(s.clone()) }
}
Some(_) => {
return Err(invalid_params(
"Network.continueInterceptedRequest: 'errorReason' must be a CDP ErrorReason string",
))
}
None => FetchResolveAction::Continue,
};
bridge_send(
bridge,
BridgeCommand::FetchResolve {
target_id: tid,
interception_id,
action,
},
)
}
"emulateNetworkConditions" => ok_empty(),
_ => Err(CdpError {
code: -32601,
message: format!("'Network.{}' wasn't found", command),
}),
}
}
fn handle_storage(
command: &str,
target_id: &str,
params: &Option<Value>,
bridge: Option<&BridgeSender>,
) -> HandlerResult {
let tid = target_id.to_string();
match command {
"getStorageItemsForOrigin" => {
let origin = params_str(params, "origin");
let storage_type = params_str(params, "storageType");
bridge_send(
bridge,
BridgeCommand::StorageGetStorageItemsForOrigin {
target_id: tid,
origin,
storage_type,
},
)
}
"clearDataForOrigin" => {
let origin = params_str(params, "origin");
let storage_type = params_str(params, "storageType");
bridge_send(
bridge,
BridgeCommand::StorageClearDataForOrigin {
target_id: tid,
origin,
storage_type,
},
)
}
"getCookies" => {
let urls: Vec<String> = params
.as_ref()
.and_then(|p| p.get("urls"))
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
bridge_send(
bridge,
BridgeCommand::GetCookies {
target_id: tid,
urls,
},
)
}
_ => Err(CdpError {
code: -32601,
message: format!("'Storage.{}' wasn't found", command),
}),
}
}
fn handle_security(
command: &str,
target_id: &str,
params: &Option<Value>,
bridge: Option<&BridgeSender>,
) -> HandlerResult {
let tid = target_id.to_string();
match command {
"enable" => bridge_send(bridge, BridgeCommand::SecurityEnable { target_id: tid }),
"disable" => bridge_send(bridge, BridgeCommand::SecurityDisable { target_id: tid }),
"setOverrideCertificateErrors" => {
let override_errors = params
.as_ref()
.and_then(|p| p.get("override"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
bridge_send(
bridge,
BridgeCommand::SecuritySetOverrideCertificateErrors {
target_id: tid,
override_errors,
},
)
}
"handleCertificateError" | "certificateError" => ok_empty(),
_ => Err(CdpError {
code: -32601,
message: format!("'Security.{}' wasn't found", command),
}),
}
}
fn handle_css(
command: &str,
target_id: &str,
params: &Option<Value>,
bridge: Option<&BridgeSender>,
) -> HandlerResult {
let tid = target_id.to_string();
match command {
"enable" | "disable" => ok_empty(),
"getComputedStyleForNode" => {
let node_id = devtools_dom::require_node_id(params)?;
devtools_dom::eval_dom(
bridge,
&tid,
&devtools_dom::computed_style_body(node_id),
)
}
"getMatchedStylesForNode" => {
let node_id = devtools_dom::require_node_id(params)?;
devtools_dom::eval_dom(
bridge,
&tid,
&devtools_dom::matched_styles_body(node_id),
)
}
"getInlineStylesForNode" => {
let node_id = devtools_dom::require_node_id(params)?;
devtools_dom::eval_dom(
bridge,
&tid,
&devtools_dom::inline_styles_body(node_id),
)
}
"setStyleTexts" => {
let edits = devtools_dom::require_edits(params)?;
devtools_dom::eval_dom(
bridge,
&tid,
&devtools_dom::set_style_texts_body(&edits),
)
}
_ => Err(CdpError {
code: -32601,
message: format!("'CSS.{}' wasn't found", command),
}),
}
}
fn handle_emulation(
command: &str,
target_id: &str,
params: &Option<Value>,
bridge: Option<&BridgeSender>,
) -> HandlerResult {
let tid = target_id.to_string();
match command {
"setEmulatedMedia" => ok_empty(),
"setDeviceMetricsOverride" => {
let width = params
.as_ref()
.and_then(|p| p.get("width"))
.and_then(|v| v.as_u64())
.unwrap_or(1920) as u32;
let height = params
.as_ref()
.and_then(|p| p.get("height"))
.and_then(|v| v.as_u64())
.unwrap_or(1080) as u32;
let dsf = params
.as_ref()
.and_then(|p| p.get("deviceScaleFactor"))
.and_then(|v| v.as_f64());
if bridge.is_some() {
bridge_send(
bridge,
BridgeCommand::SetViewport {
target_id: tid,
width,
height,
device_scale_factor: dsf,
},
)
} else {
ok_empty()
}
}
"clearDeviceMetricsOverride" => ok_empty(),
"setUserAgentOverride" => {
let ua = params_str(params, "userAgent");
if bridge.is_none() {
return Err(CdpError {
code: -32603,
message: "no servo bridge connected".into(),
});
}
if !ua.is_empty() {
bridge_send(
bridge,
BridgeCommand::SetUserAgent {
target_id: tid,
user_agent: ua,
},
)?;
}
ok_empty()
}
"setTouchEmulationEnabled" | "setScriptExecutionDisabled" => ok_empty(),
"setFocusEmulationEnabled" | "setCPUThrottlingRate" => ok_empty(),
"setDefaultBackgroundColorOverride" => ok_empty(),
_ => Err(CdpError {
code: -32601,
message: format!("'Emulation.{}' wasn't found", command),
}),
}
}
fn handle_input(
command: &str,
target_id: &str,
params: &Option<Value>,
bridge: Option<&BridgeSender>,
) -> HandlerResult {
let tid = target_id.to_string();
match command {
"dispatchMouseEvent" => {
let event_type = params_str(params, "type");
let x = params
.as_ref()
.and_then(|p| p.get("x"))
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
let y = params
.as_ref()
.and_then(|p| p.get("y"))
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
let button = params
.as_ref()
.and_then(|p| p.get("button"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let click_count = params
.as_ref()
.and_then(|p| p.get("clickCount"))
.and_then(|v| v.as_i64());
bridge_send(
bridge,
BridgeCommand::DispatchMouseEvent {
target_id: tid.clone(),
event_type,
x,
y,
button,
click_count,
},
)
}
"dispatchKeyEvent" => {
let event_type = params_str(params, "type");
let key = params_str(params, "key");
let code = params_str(params, "code");
let text = params
.as_ref()
.and_then(|p| p.get("text"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let modifiers = params
.as_ref()
.and_then(|p| p.get("modifiers"))
.and_then(|v| v.as_u64())
.map(|v| v as u32)
.unwrap_or(0);
let location = params
.as_ref()
.and_then(|p| p.get("location"))
.and_then(|v| v.as_i64())
.unwrap_or(0);
let repeat = params
.as_ref()
.and_then(|p| p.get("autoRepeat"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
bridge_send(
bridge,
BridgeCommand::DispatchKeyEvent {
target_id: tid.clone(),
event_type,
key,
code,
text,
modifiers,
location,
repeat,
},
)
}
"dispatchTouchEvent" => Err(not_supported(
"Input.dispatchTouchEvent",
"no touch delivery path exists on this face (the servo embedder bridge carries no touch command)",
)),
"insertText" => {
let text = params_str(params, "text");
if text.is_empty() {
return ok_empty();
}
bridge_send(
bridge,
BridgeCommand::InsertText {
target_id: tid,
text,
},
)
}
"setIgnoreInputEvents" | "setInterceptDrags" => ok_empty(),
_ => Err(CdpError {
code: -32601,
message: format!("'Input.{}' wasn't found", command),
}),
}
}
fn handle_overlay(
command: &str,
target_id: &str,
params: &Option<Value>,
bridge: Option<&BridgeSender>,
) -> HandlerResult {
let tid = target_id.to_string();
match command {
"enable" | "disable" => ok_empty(),
"highlightNode" => {
let (node_id, object_id) = match require_overlay_node_ref(params)? {
devtools_dom::NodeRef::NodeId(id) => (Some(id), None),
devtools_dom::NodeRef::ObjectId(oid) => (None, Some(oid)),
};
let highlight_spec = params
.as_ref()
.and_then(|p| p.get("highlightSpec"))
.cloned();
bridge_send(
bridge,
BridgeCommand::OverlayHighlightNode {
target_id: tid,
node_id,
object_id,
highlight_spec,
},
)
}
"hideHighlight" => bridge_send(
bridge,
BridgeCommand::OverlayHideHighlight { target_id: tid },
),
"setInspectMode" => Err(not_supported(
"Overlay.setInspectMode",
"inspect mode needs the hoverโOverlay.nodeHighlightRequested event chain, which does not exist on this face",
)),
"setPausedInDebuggerMessage" => ok_empty(),
_ => Err(CdpError {
code: -32601,
message: format!("'Overlay.{}' wasn't found", command),
}),
}
}
fn require_overlay_node_ref(
params: &Option<Value>,
) -> Result<devtools_dom::NodeRef, CdpError> {
devtools_dom::require_node_ref(params).or_else(|_| {
let backend = params
.as_ref()
.and_then(|p| p.get("backendNodeId"))
.and_then(|v| v.as_i64())
.ok_or_else(|| CdpError {
code: ERR_INVALID_PARAMS,
message: "missing required parameter: nodeId, objectId or backendNodeId".into(),
})?;
Ok(devtools_dom::NodeRef::NodeId(backend))
})
}
fn handle_debugger(
command: &str,
target_id: &str,
params: &Option<Value>,
bridge: Option<&BridgeSender>,
) -> HandlerResult {
let tid = target_id.to_string();
match command {
"enable" => {
if bridge.is_some() {
bridge_send(bridge, BridgeCommand::DebuggerEnable { target_id: tid })
} else {
ok_empty()
}
}
"disable" => {
if bridge.is_some() {
bridge_send(bridge, BridgeCommand::DebuggerDisable { target_id: tid })
} else {
ok_empty()
}
}
"setBreakpointByUrl" => {
let url = params
.as_ref()
.and_then(|p| p.get("url"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(|s| s.to_string());
let url_regex = params
.as_ref()
.and_then(|p| p.get("urlRegex"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(|s| s.to_string());
let line = params
.as_ref()
.and_then(|p| p.get("lineNumber"))
.and_then(|v| v.as_u64())
.unwrap_or(0) as u32;
let column = params
.as_ref()
.and_then(|p| p.get("columnNumber"))
.and_then(|v| v.as_u64())
.map(|c| c as u32);
bridge_send(
bridge,
BridgeCommand::DebuggerSetBreakpoint {
target_id: tid,
url,
url_regex,
line,
column,
},
)
}
"removeBreakpoint" => {
let breakpoint_id = params
.as_ref()
.and_then(|p| p.get("breakpointId"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if bridge.is_some() {
bridge_send(
bridge,
BridgeCommand::DebuggerRemoveBreakpoint {
target_id: tid,
breakpoint_id,
},
)
} else {
ok_empty()
}
}
"pause" => {
if bridge.is_some() {
bridge_send(bridge, BridgeCommand::DebuggerInterrupt { target_id: tid })
} else {
ok_empty()
}
}
"resume" => {
if bridge.is_some() {
bridge_send(
bridge,
BridgeCommand::DebuggerResume {
target_id: tid,
step_type: None,
},
)
} else {
ok_empty()
}
}
"stepOver" => {
if bridge.is_some() {
bridge_send(
bridge,
BridgeCommand::DebuggerResume {
target_id: tid,
step_type: Some("next".into()),
},
)
} else {
ok_empty()
}
}
"stepInto" => {
if bridge.is_some() {
bridge_send(
bridge,
BridgeCommand::DebuggerResume {
target_id: tid,
step_type: Some("step".into()),
},
)
} else {
ok_empty()
}
}
"stepOut" => {
if bridge.is_some() {
bridge_send(
bridge,
BridgeCommand::DebuggerResume {
target_id: tid,
step_type: Some("finish".into()),
},
)
} else {
ok_empty()
}
}
"setSkipAllPauses" | "setBreakpointsActive" => ok_empty(),
"evaluateOnCallFrame" => {
let expression = params
.as_ref()
.and_then(|p| p.get("expression"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let frame_actor_id = params
.as_ref()
.and_then(|p| p.get("callFrameId"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if bridge.is_some() && !expression.is_empty() {
bridge_send(
bridge,
BridgeCommand::DebuggerEval {
target_id: tid,
expression,
frame_actor_id,
},
)
} else {
Ok(serde_json::json!({ "result": { "type": "undefined" } }))
}
}
"getPossibleBreakpoints" => {
let start_script_id = params
.as_ref()
.and_then(|p| p.get("start"))
.and_then(|v| v.get("scriptId"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if bridge.is_some() {
bridge_send(
bridge,
BridgeCommand::DebuggerGetPossibleBreakpoints {
target_id: tid,
start_script_id,
},
)
} else {
Ok(serde_json::json!({ "locations": [] }))
}
}
"getScriptSource" => {
let script_id = params
.as_ref()
.and_then(|p| p.get("scriptId"))
.and_then(|v| v.as_str())
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or(0);
if bridge.is_some() {
bridge_send(
bridge,
BridgeCommand::DebuggerGetScriptSource {
target_id: tid,
script_id,
},
)
} else {
Ok(serde_json::json!({ "scriptSource": "" }))
}
}
"setPauseOnExceptions" => ok_empty(),
"blackbox" | "unblackbox" => Err(not_supported(
&format!("Debugger.{command}"),
"SpiderMonkey's Debugger API has no native blackbox; bao does not emulate one",
)),
_ => Err(CdpError {
code: -32601,
message: format!("'Debugger.{}' wasn't found", command),
}),
}
}
fn handle_log(command: &str) -> HandlerResult {
match command {
"enable" | "disable" | "clear" => ok_empty(),
"startViolationsReport" | "stopViolationsReport" => ok_empty(),
_ => Err(CdpError {
code: -32601,
message: format!("'Log.{}' wasn't found", command),
}),
}
}
const FULFILL_BODY_MAX_DECODED_BYTES: usize = 8 * 1024 * 1024;
const FULFILL_BODY_MAX_BASE64_CHARS: usize = (FULFILL_BODY_MAX_DECODED_BYTES / 3 + 1) * 4;
fn invalid_params(message: impl std::fmt::Display) -> CdpError {
CdpError {
code: ERR_INVALID_PARAMS,
message: message.to_string(),
}
}
#[derive(Clone, Copy)]
enum PatternFace {
Fetch,
LegacyNetwork,
}
fn parse_interception_patterns(
params: &Option<Value>,
face: PatternFace,
label: &str,
) -> Result<Vec<crate::servo_bridge::FetchPattern>, CdpError> {
use crate::servo_bridge::{FetchPattern, FetchRequestStage};
let patterns_val = params
.as_ref()
.and_then(|p| p.get("patterns"))
.filter(|v| !v.is_null());
let Some(patterns_val) = patterns_val else {
return Ok(vec![FetchPattern {
url_pattern: None,
resource_type: None,
request_stage: FetchRequestStage::Request,
}]);
};
let arr = patterns_val
.as_array()
.ok_or_else(|| invalid_params(format!("{label}: 'patterns' must be an array of pattern objects")))?;
let mut out = Vec::with_capacity(arr.len());
for entry in arr {
let obj = entry.as_object().ok_or_else(|| {
invalid_params(format!(
"{label}: each pattern must be an object with optional urlPattern / resourceType / requestStage"
))
})?;
let url_pattern = match obj.get("urlPattern") {
None | Some(Value::Null) => None,
Some(Value::String(s)) => Some(s.clone()),
Some(_) => {
return Err(invalid_params(format!(
"{label}: 'urlPattern' must be a string glob"
)))
}
};
let resource_type = match obj.get("resourceType") {
None | Some(Value::Null) => None,
Some(Value::String(s)) if !s.is_empty() => Some(s.clone()),
Some(_) => {
return Err(invalid_params(format!(
"{label}: 'resourceType' must be a non-empty CDP ResourceType string"
)))
}
};
let request_stage = match face {
PatternFace::Fetch => match obj.get("requestStage") {
None | Some(Value::Null) => FetchRequestStage::Request,
Some(Value::String(s)) if s == "Request" => FetchRequestStage::Request,
Some(Value::String(s)) if s == "Response" => FetchRequestStage::Response,
Some(_) => {
return Err(invalid_params(format!(
"{label}: 'requestStage' must be \"Request\" or \"Response\""
)))
}
},
PatternFace::LegacyNetwork => match obj.get("interceptionStage") {
None | Some(Value::Null) => FetchRequestStage::Request,
Some(Value::String(s)) if s == "Request" => FetchRequestStage::Request,
Some(_) => {
return Err(invalid_params(format!(
"{label}: interception stage must be \"Request\" โ the servo pause point is request-time only, there is no headers-received pause"
)))
}
},
};
out.push(FetchPattern {
url_pattern,
resource_type,
request_stage,
});
}
Ok(out)
}
fn interception_id(params: &Option<Value>, label: &str) -> Result<String, CdpError> {
match params.as_ref().and_then(|p| p.get("interceptionId")) {
Some(Value::String(s)) if !s.is_empty() => Ok(s.clone()),
Some(_) => Err(invalid_params(format!(
"{label}: 'interceptionId' must be a non-empty string"
))),
None => Err(invalid_params(format!(
"{label}: missing required 'interceptionId' (the resolution key carried on Fetch.requestPaused / Network.requestIntercepted)"
))),
}
}
fn reject_request_modification(command: &str, params: &Option<Value>) -> Result<(), CdpError> {
for field in ["url", "method", "postData", "headers"] {
if params.as_ref().is_some_and(|p| p.get(field).is_some()) {
return Err(invalid_params(format!(
"{command}: '{field}' request modification is not supported โ the servo embedder request face can only let the paused request through or answer it with a synthesized response; it cannot re-issue a reconstructed request (design ยง7 boundary)"
)));
}
}
Ok(())
}
fn handle_fetch(
command: &str,
target_id: &str,
params: &Option<Value>,
bridge: Option<&BridgeSender>,
) -> HandlerResult {
use crate::servo_bridge::FetchResolveAction;
let tid = target_id.to_string();
match command {
"enable" => {
if params.as_ref().and_then(|p| p.get("handleAuthRequests"))
== Some(&Value::Bool(true))
{
return Err(invalid_params(
"Fetch.enable: handleAuthRequests=true is not supported โ the auth interception facet is not wired (no Fetch.authRequired event is ever emitted and Fetch.continueWithAuth is an explicit error; design ยง7)",
));
}
let patterns = parse_interception_patterns(params, PatternFace::Fetch, "Fetch.enable")?;
bridge_send(
bridge,
BridgeCommand::FetchEnable {
target_id: tid,
patterns,
legacy_network_events: false,
},
)
}
"disable" => bridge_send(bridge, BridgeCommand::FetchDisable { target_id: tid }),
"continueRequest" => {
let interception_id = interception_id(params, "Fetch.continueRequest")?;
reject_request_modification("Fetch.continueRequest", params)?;
bridge_send(
bridge,
BridgeCommand::FetchResolve {
target_id: tid,
interception_id,
action: FetchResolveAction::Continue,
},
)
}
"failRequest" => {
let interception_id = interception_id(params, "Fetch.failRequest")?;
let error_reason = match params.as_ref().and_then(|p| p.get("errorReason")) {
None | Some(Value::Null) => None,
Some(Value::String(s)) => Some(s.clone()),
Some(_) => {
return Err(invalid_params(
"Fetch.failRequest: 'errorReason' must be a CDP ErrorReason string",
))
}
};
bridge_send(
bridge,
BridgeCommand::FetchResolve {
target_id: tid,
interception_id,
action: FetchResolveAction::Fail { error_reason },
},
)
}
"fulfillRequest" => {
let interception_id = interception_id(params, "Fetch.fulfillRequest")?;
let response_code = match params.as_ref().and_then(|p| p.get("responseCode")) {
Some(Value::Number(n)) => match n.as_i64() {
Some(c) => c,
None => {
return Err(invalid_params(
"Fetch.fulfillRequest: 'responseCode' must be an integer HTTP status",
))
}
},
Some(_) => {
return Err(invalid_params(
"Fetch.fulfillRequest: 'responseCode' must be an integer HTTP status",
))
}
None => {
return Err(invalid_params(
"Fetch.fulfillRequest: missing required 'responseCode'",
))
}
};
let status_message = match params.as_ref().and_then(|p| p.get("responsePhrase")) {
None | Some(Value::Null) => None,
Some(Value::String(s)) => Some(s.clone()),
Some(_) => {
return Err(invalid_params(
"Fetch.fulfillRequest: 'responsePhrase' must be a string",
))
}
};
let response_headers = match params.as_ref().and_then(|p| p.get("responseHeaders")) {
None | Some(Value::Null) => None,
Some(h @ Value::Object(_)) => {
if h.as_object().is_some_and(|o| o.values().all(Value::is_string)) {
Some(h.clone())
} else {
return Err(invalid_params(
"Fetch.fulfillRequest: 'responseHeaders' values must be strings",
));
}
}
Some(_) => {
return Err(invalid_params(
"Fetch.fulfillRequest: 'responseHeaders' must be an object of {name: value}",
))
}
};
let body = match params.as_ref().and_then(|p| p.get("body")) {
None | Some(Value::Null) => None,
Some(Value::String(s)) => {
if s.len() > FULFILL_BODY_MAX_BASE64_CHARS {
return Err(invalid_params(format!(
"Fetch.fulfillRequest: body exceeds the 8 MiB cap (base64 length {} > {}); the browser layer re-checks the decoded size (design ยง7)",
s.len(),
FULFILL_BODY_MAX_BASE64_CHARS
)));
}
Some(s.clone())
}
Some(_) => {
return Err(invalid_params(
"Fetch.fulfillRequest: 'body' must be a base64 string",
))
}
};
bridge_send(
bridge,
BridgeCommand::FetchResolve {
target_id: tid,
interception_id,
action: FetchResolveAction::Fulfill {
response_code,
status_message,
response_headers,
body,
},
},
)
}
"getRequestPostData" => Err(not_supported(
"Fetch.getRequestPostData",
"the servo embedder request face carries no request body (WebResourceRequest has method/url/headers only) โ a vendor body carrier is phase-2 (design ยง7)",
)),
"continueWithAuth" => Err(not_supported(
"Fetch.continueWithAuth",
"the auth interception facet is not wired: Fetch.enable with handleAuthRequests=true is rejected the same way and no auth challenge is ever paused (design ยง7)",
)),
"takeResponseBodyAsStream" => Err(not_supported(
"Fetch.takeResponseBodyAsStream",
"stream handles are not part of the P1 interception face โ a fulfillRequest client already holds the body it sent; capturing the network response body of a paused request is phase-2 (design ยง7)",
)),
"continueWithResponse" => Err(not_supported(
"Fetch.continueWithResponse",
"phase-2 (a renamed fulfillRequest) โ use Fetch.fulfillRequest, which is the real synthesized-response path (design ยง7)",
)),
_ => Err(CdpError {
code: -32601,
message: format!("'Fetch.{}' wasn't found", command),
}),
}
}
fn handle_profiler(
command: &str,
target_id: &str,
params: &Option<Value>,
bridge: Option<&BridgeSender>,
) -> HandlerResult {
let tid = target_id.to_string();
match command {
"enable" | "disable" => ok_empty(),
"start" => bridge_send(bridge, BridgeCommand::ProfilerStart { target_id: tid }),
"stop" => bridge_send(bridge, BridgeCommand::ProfilerStop { target_id: tid }),
"setSamplingInterval" => {
let interval = params
.as_ref()
.and_then(|p| p.get("interval"))
.and_then(|v| v.as_u64())
.unwrap_or(1000) as u32;
bridge_send(
bridge,
BridgeCommand::ProfilerSetSamplingInterval {
target_id: tid,
interval,
},
)
}
_ => Err(CdpError {
code: -32601,
message: format!("'Profiler.{}' wasn't found", command),
}),
}
}
fn handle_heap_profiler(
command: &str,
target_id: &str,
bridge: Option<&BridgeSender>,
) -> HandlerResult {
let tid = target_id.to_string();
match command {
"enable" | "disable" => ok_empty(),
"takeHeapSnapshot" => {
bridge_send(
bridge,
BridgeCommand::HeapProfilerTakeSnapshot { target_id: tid },
)
}
"startTrackingHeapObjects" => {
bridge_send(
bridge,
BridgeCommand::HeapProfilerStartTracking { target_id: tid },
)
}
"stopTrackingHeapObjects" => {
bridge_send(
bridge,
BridgeCommand::HeapProfilerStopTracking { target_id: tid },
)
}
"collectGarbage" => {
bridge_send(
bridge,
BridgeCommand::HeapProfilerCollectGarbage { target_id: tid },
)
}
_ => Err(CdpError {
code: -32601,
message: format!("'HeapProfiler.{}' wasn't found", command),
}),
}
}
fn handle_memory(command: &str, target_id: &str, bridge: Option<&BridgeSender>) -> HandlerResult {
let tid = target_id.to_string();
match command {
"getDOMCounters" => {
bridge_send(
bridge,
BridgeCommand::MemoryGetDOMCounters { target_id: tid },
)
}
"prepareForLeakDetection" => ok_empty(),
"forciblyPurgeJavaScriptMemory" => {
bridge_send(bridge, BridgeCommand::MemoryPurgeJS { target_id: tid })
}
_ => Err(CdpError {
code: -32601,
message: format!("'Memory.{}' wasn't found", command),
}),
}
}
fn handle_performance(
command: &str,
target_id: &str,
bridge: Option<&BridgeSender>,
) -> HandlerResult {
let tid = target_id.to_string();
match command {
"enable" | "disable" => ok_empty(),
"getMetrics" => {
bridge_send(
bridge,
BridgeCommand::PerformanceGetMetrics { target_id: tid },
)
}
_ => Err(CdpError {
code: -32601,
message: format!("'Performance.{}' wasn't found", command),
}),
}
}
fn handle_system_info(command: &str) -> HandlerResult {
match command {
"getInfo" => {
let os_name = std::env::consts::OS;
let arch = std::env::consts::ARCH;
let pid = std::process::id();
Ok(serde_json::json!({
"gpu": {
"vendorString": "Bao",
"deviceString": "Software Renderer"
},
"modelName": "Bao",
"modelVersion": env!("CARGO_PKG_VERSION"),
"commandLine": "",
"platform": os_name,
"product": "Bao",
"cpu": {
"arch": arch,
"processors": num_cpus()
},
"osName": os_name,
"osVersion": "",
"pid": pid
}))
}
"getProcessInfo" => {
let pid = std::process::id();
Ok(serde_json::json!({
"processInfo": [{
"id": pid,
"type": "browser",
"name": "Bao"
}]
}))
}
_ => Err(CdpError {
code: -32601,
message: format!("'SystemInfo.{}' wasn't found", command),
}),
}
}
fn num_cpus() -> u32 {
std::thread::available_parallelism()
.map(|n| n.get() as u32)
.unwrap_or(1)
}
fn handle_browser(command: &str) -> HandlerResult {
match command {
"getVersion" => Ok(serde_json::json!({
"protocolVersion": "1.3",
"product": "Bao/0.1.0",
"revision": env!("CARGO_PKG_VERSION"),
"userAgent": format!("Bao/{}", env!("CARGO_PKG_VERSION")),
"jsVersion": "SpiderMonkey",
})),
"setDownloadBehavior" => ok_empty(),
_ => Err(CdpError {
code: -32601,
message: format!("'Browser.{}' wasn't found", command),
}),
}
}
fn handle_service_worker(
command: &str,
target_id: &str,
params: &Option<Value>,
bridge: Option<&BridgeSender>,
) -> HandlerResult {
let tid = target_id.to_string();
match command {
"enable" | "disable" => ok_empty(),
"deliverPushMessage" => {
let origin = params_str(params, "origin");
let registration_id = params_str(params, "registrationId");
Ok(serde_json::json!({
"origin": origin,
"registrationId": registration_id,
"delivered": true
}))
}
"dispatchPeriodicSyncEvent" | "dispatchSyncEvent" => ok_empty(),
"getAllRegistrations" => {
if bridge.is_some() {
bridge_send(
bridge,
BridgeCommand::ListServiceWorkerRegistrations { target_id: tid },
)
} else {
Ok(serde_json::json!({ "registrations": [] }))
}
}
"getRegistration" => {
let registration_id = params_str(params, "registrationId");
if bridge.is_some() && !registration_id.is_empty() {
bridge_send(
bridge,
BridgeCommand::GetServiceWorkerRegistrationInfo {
target_id: tid,
registration_id,
},
)
} else {
Ok(serde_json::json!({ "registration": null }))
}
}
"stopWorker" => {
let registration_id = params_str(params, "registrationId");
if bridge.is_some() && !registration_id.is_empty() {
bridge_send(
bridge,
BridgeCommand::StopServiceWorker {
target_id: tid,
registration_id,
},
)?;
}
ok_empty()
}
"unregister" => {
let registration_id = params_str(params, "registrationId");
if bridge.is_some() && !registration_id.is_empty() {
bridge_send(
bridge,
BridgeCommand::TerminateServiceWorker {
target_id: tid,
registration_id,
},
)?;
}
ok_empty()
}
"updateRegistration" => ok_empty(),
_ => Err(CdpError {
code: -32601,
message: format!("'ServiceWorker.{}' wasn't found", command),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CdpEvent;
use serde_json::json;
fn cdp_msg(id: i64, method: &str, params: Option<Value>) -> CdpMessage {
CdpMessage {
id: Some(id),
method: method.into(),
params,
session_id: None,
}
}
fn dispatch_no_bridge(id: i64, method: &str, params: Option<Value>) -> CdpResponse {
let msg = cdp_msg(id, method, params);
let params = msg.params.clone();
handle_command(msg, "t1", ¶ms, None)
}
#[test]
fn parse_message_valid_json() {
let msg = parse_message(r#"{"id":1,"method":"Page.enable","params":{"url":"http://x"}}"#)
.unwrap();
assert_eq!(msg.id, Some(1));
assert_eq!(msg.method, "Page.enable");
assert_eq!(msg.params, Some(json!({"url": "http://x"})));
assert_eq!(msg.session_id, None);
}
#[test]
fn parse_message_invalid_json() {
assert!(parse_message("{not json}").is_none());
}
#[test]
fn parse_message_missing_method() {
assert!(parse_message(r#"{"id":1}"#).is_none());
}
#[test]
fn parse_message_with_session_id() {
let raw = r#"{"id":5,"method":"Runtime.evaluate","sessionId":"abc123"}"#;
let msg = parse_message(raw).expect("should parse valid JSON with session_id");
assert_eq!(msg.id, Some(5));
assert_eq!(msg.method, "Runtime.evaluate");
assert_eq!(msg.session_id, Some("abc123".to_string()));
}
#[test]
fn parse_message_null_params() {
let msg = parse_message(r#"{"id":2,"method":"Page.enable","params":null}"#).unwrap();
assert_eq!(msg.id, Some(2));
assert_eq!(msg.params, None);
}
#[test]
fn serialize_response_with_result() {
let resp = CdpResponse {
id: Some(1),
result: Some(json!({"key": "val"})),
error: None,
};
let s = serialize_response(&resp);
let parsed: Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["id"], 1);
assert_eq!(parsed["result"]["key"], "val");
assert!(parsed.get("error").is_none());
}
#[test]
fn serialize_response_with_error() {
let resp = CdpResponse {
id: Some(2),
result: None,
error: Some(CdpError {
code: -32601,
message: "not found".into(),
}),
};
let s = serialize_response(&resp);
let parsed: Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["id"], 2);
assert!(parsed.get("result").is_none());
assert_eq!(parsed["error"]["code"], -32601);
assert_eq!(parsed["error"]["message"], "not found");
}
#[test]
fn handle_command_unknown_domain() {
let resp = dispatch_no_bridge(1, "Foo.bar", None);
assert!(resp.result.is_none());
let err = resp.error.unwrap();
assert_eq!(err.code, -32601);
}
#[test]
fn handle_command_target_get_targets() {
let resp = dispatch_no_bridge(2, "Target.getTargets", None);
assert!(resp.error.is_none());
let result = resp.result.unwrap();
assert!(result.get("targetInfos").unwrap().as_array().unwrap().len() > 0);
assert_eq!(result["targetInfos"][0]["targetId"], "t1");
}
fn context_responder(
rx: crate::servo_bridge::BridgeReceiver,
) -> std::thread::JoinHandle<()> {
std::thread::spawn(move || loop {
let handled = rx.try_process(|cmd| {
crate::servo_bridge::BridgeResponse {
result: match cmd {
BridgeCommand::CreateBrowserContext => {
Ok(json!({"browserContextId": "bao-context-1"}))
}
BridgeCommand::ListBrowserContexts => {
Ok(json!({"browserContextIds": ["bao-context-1"]}))
}
BridgeCommand::DisposeBrowserContext { browser_context_id } => {
if browser_context_id == "bao-context-1" {
Ok(json!({}))
} else {
Err(crate::servo_bridge::BridgeError::domain(format!(
"Failed to find browser context: {browser_context_id}"
)))
}
}
BridgeCommand::CreateTargetInContext { browser_context_id, .. } => {
if browser_context_id == "bao-context-1" {
Ok(json!({
"targetId": "7",
"browserContextId": browser_context_id
}))
} else {
Err(crate::servo_bridge::BridgeError::domain(format!(
"Failed to find browser context: {browser_context_id}"
)))
}
}
BridgeCommand::GetTargetContextId { target_id } => {
Ok(json!({"browserContextId": format!("ctx-of-{target_id}")}))
}
BridgeCommand::GetTitle { .. } => Ok(json!("T")),
BridgeCommand::GetUrl { .. } => Ok(json!("https://example.com")),
BridgeCommand::ListTargets => Ok(json!([
{"id": "1", "title": "P", "url": "about:blank",
"browserContextId": "bao-context-1"}
])),
_ => Ok(json!({})),
},
}
});
if !handled {
std::thread::sleep(std::time::Duration::from_millis(1));
}
})
}
fn dispatch_with_bridge(
id: i64,
method: &str,
params: Option<Value>,
bridge: &crate::servo_bridge::BridgeSender,
) -> CdpResponse {
let msg = cdp_msg(id, method, params);
let params = msg.params.clone();
handle_command(msg, "t1", ¶ms, Some(bridge))
}
#[test]
fn create_browser_context_returns_bridge_minted_id() {
let (tx, rx) = crate::servo_bridge::bridge_channel(std::time::Duration::from_secs(2));
let _keeper = context_responder(rx);
let resp = dispatch_with_bridge(1, "Target.createBrowserContext", None, &tx);
assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
let id = resp.result.unwrap()["browserContextId"]
.as_str()
.unwrap()
.to_string();
assert_eq!(id, "bao-context-1");
assert_ne!(id, DEFAULT_BROWSER_CONTEXT_ID);
}
#[test]
fn dispose_browser_context_unknown_id_surfaces_domain_error() {
let (tx, rx) = crate::servo_bridge::bridge_channel(std::time::Duration::from_secs(2));
let _keeper = context_responder(rx);
let resp = dispatch_with_bridge(
1,
"Target.disposeBrowserContext",
Some(json!({"browserContextId": "nope"})),
&tx,
);
let err = resp.error.expect("unknown context must fail");
assert_eq!(err.code, ERR_NOT_SUPPORTED);
assert!(err.message.contains("Failed to find browser context"));
}
#[test]
fn get_browser_contexts_returns_registry_listing() {
let (tx, rx) = crate::servo_bridge::bridge_channel(std::time::Duration::from_secs(2));
let _keeper = context_responder(rx);
let resp = dispatch_with_bridge(1, "Target.getBrowserContexts", None, &tx);
assert!(resp.error.is_none());
let ids = resp.result.unwrap()["browserContextIds"]
.as_array()
.unwrap()
.clone();
assert_eq!(ids, vec![json!("bao-context-1")]);
}
#[test]
fn create_target_with_context_routes_context_aware_creation() {
let (tx, rx) = crate::servo_bridge::bridge_channel(std::time::Duration::from_secs(2));
let _keeper = context_responder(rx);
let resp = dispatch_with_bridge(
1,
"Target.createTarget",
Some(json!({"url": "about:blank", "browserContextId": "bao-context-1"})),
&tx,
);
assert!(resp.error.is_none());
let r = resp.result.unwrap();
assert_eq!(r["targetId"], "7");
assert_eq!(r["browserContextId"], "bao-context-1");
let err = dispatch_with_bridge(
2,
"Target.createTarget",
Some(json!({"url": "about:blank", "browserContextId": "nope"})),
&tx,
)
.error
.expect("unknown context must fail");
assert_eq!(err.code, ERR_NOT_SUPPORTED);
assert!(err.message.contains("Failed to find browser context"));
}
#[test]
fn get_targets_attribution_comes_from_listing_entries() {
let (tx, rx) = crate::servo_bridge::bridge_channel(std::time::Duration::from_secs(2));
let _keeper = context_responder(rx);
let resp = dispatch_with_bridge(1, "Target.getTargets", None, &tx);
assert!(resp.error.is_none());
let infos = resp.result.unwrap()["targetInfos"].as_array().unwrap().clone();
let page = infos
.iter()
.find(|i| i["type"] == "page")
.expect("page entry present");
assert_eq!(page["browserContextId"], "bao-context-1");
}
#[test]
fn handle_command_ok_empty_result_matrix() {
let cases: Vec<(i64, &str, Option<Value>)> = vec![
(5, "Target.setAutoAttach", None),
(6, "Page.enable", None),
(8, "Runtime.enable", None),
(12, "Network.enable", None),
(14, "CSS.enable", None),
(
16,
"Emulation.setDeviceMetricsOverride",
Some(json!({"width": 800, "height": 600, "deviceScaleFactor": 2})),
),
(18, "Overlay.enable", None),
(19, "Debugger.enable", None),
(21, "Log.enable", None),
(7, "Target.setDiscoverTargets", None),
(1, "ServiceWorker.enable", None),
(2, "ServiceWorker.disable", None),
];
for (id, method, params) in cases {
let resp = dispatch_no_bridge(id, method, params);
assert!(resp.error.is_none(), "{method} (id {id}): unexpected error");
assert_eq!(
resp.result.unwrap(),
json!({}),
"{method} (id {id}): expected empty result"
);
}
}
#[test]
fn handle_command_ok_only_matrix() {
let cases: Vec<(i64, &str, Option<Value>)> = vec![
(20, "Page.disable", None),
(21, "Runtime.disable", None),
(24, "Runtime.evaluateAsync", None),
(25, "Runtime.runScript", None),
(26, "Runtime.releaseObject", None),
(27, "Runtime.releaseObjectGroup", None),
(28, "Runtime.compileScript", None),
(30, "DOM.enable", None),
(31, "DOM.disable", None),
(43, "Network.disable", None),
(45, "Network.setCacheDisabled", None),
(47, "Network.emulateNetworkConditions", None),
(49, "Network.deleteCookies", None),
(54, "CSS.disable", None),
(59, "Emulation.clearDeviceMetricsOverride", None),
(61, "Emulation.setTouchEmulationEnabled", None),
(62, "Emulation.setScriptExecutionDisabled", None),
(63, "Emulation.setFocusEmulationEnabled", None),
(64, "Emulation.setCPUThrottlingRate", None),
(65, "Emulation.setDefaultBackgroundColorOverride", None),
(69, "Input.insertText", Some(json!({"text": ""}))),
(70, "Input.setIgnoreInputEvents", None),
(71, "Input.setInterceptDrags", None),
(76, "Overlay.setPausedInDebuggerMessage", None),
(78, "Debugger.disable", None),
(79, "Debugger.removeBreakpoint", None),
(80, "Debugger.pause", None),
(81, "Debugger.resume", None),
(82, "Debugger.stepOver", None),
(83, "Debugger.stepInto", None),
(84, "Debugger.stepOut", None),
(85, "Debugger.setSkipAllPauses", None),
(86, "Debugger.setBreakpointsActive", None),
(90, "Debugger.setPauseOnExceptions", None),
(92, "Log.disable", None),
(93, "Log.clear", None),
(94, "Log.startViolationsReport", None),
(95, "Log.stopViolationsReport", None),
(
5,
"ServiceWorker.stopWorker",
Some(json!({"registrationId": "sw-reg-1"})),
),
(
6,
"ServiceWorker.unregister",
Some(json!({"registrationId": "sw-reg-1"})),
),
(8, "ServiceWorker.dispatchPeriodicSyncEvent", None),
];
for (id, method, params) in cases {
let resp = dispatch_no_bridge(id, method, params);
assert!(resp.error.is_none(), "{method} (id {id}): unexpected error");
}
}
#[test]
fn handle_command_unknown_command_matrix() {
let cases: &[(i64, &str)] = &[
(42, "DOM.nonExistent"),
(53, "Network.bogus"),
(58, "CSS.bogus"),
(66, "Emulation.bogus"),
(72, "Input.bogus"),
(77, "Overlay.bogus"),
(91, "Debugger.bogus"),
(96, "Log.bogus"),
(106, "Fetch.bogus"),
(9, "ServiceWorker.nonExistent"),
];
for (id, method) in cases {
let resp = dispatch_no_bridge(*id, method, None);
assert!(
resp.result.is_none(),
"{method} (id {id}): expected no result"
);
assert_eq!(
resp.error.unwrap().code,
-32601,
"{method} (id {id}): expected method-not-found"
);
}
}
#[test]
fn handle_command_explicit_error_full_matrix() {
let cases: Vec<(i64, &str, Option<Value>, i64, &str)> = vec![
(
3,
"Target.createTarget",
Some(json!({"url": "https://example.com"})),
-32603,
"no servo bridge connected",
),
(
4,
"Target.closeTarget",
None,
-32603,
"no servo bridge connected",
),
(
22,
"Fetch.enable",
Some(json!({"patterns": [{"urlPattern": "*"}]})),
-32603,
"no servo bridge connected",
),
(
23,
"Fetch.continueRequest",
Some(json!({"interceptionId": "interception-1"})),
-32603,
"no servo bridge connected",
),
(
5,
"Target.attachToTarget",
Some(json!({"targetId": "t1", "flatten": true})),
-32000,
"WS session registry",
),
(
6,
"Target.detachFromTarget",
None,
-32000,
"WS session registry",
),
(
98,
"Fetch.continueWithResponse",
Some(json!({"requestId": "r1"})),
-32000,
"phase-2",
),
(
99,
"Fetch.failRequest",
Some(json!({"interceptionId": "i2", "errorReason": "Aborted"})),
-32603,
"no servo bridge connected",
),
(
100,
"Fetch.fulfillRequest",
Some(
json!({"interceptionId": "i3", "responseCode": 404, "body": "dGVzdA=="}),
),
-32603,
"no servo bridge connected",
),
(
101,
"Fetch.getRequestPostData",
Some(json!({"requestId": "r4"})),
-32000,
"no request body",
),
(
102,
"Fetch.continueWithAuth",
Some(json!({"requestId": "r5"})),
-32000,
"auth interception facet is not wired",
),
(
103,
"Fetch.takeResponseBodyAsStream",
Some(json!({"requestId": "r6"})),
-32000,
"stream handles",
),
(104, "Fetch.enable", None, -32603, "no servo bridge connected"),
(
105,
"Fetch.enable",
Some(json!({"patterns": [{"urlPattern": "*"}, {"urlPattern": "https://*"}]})),
-32603,
"no servo bridge connected",
),
];
for (id, method, params, code, msg) in cases {
let resp = dispatch_no_bridge(id, method, params);
assert!(
resp.result.is_none(),
"{method} (id {id}): expected no result"
);
let err = resp.error.unwrap();
assert_eq!(err.code, code, "{method} (id {id}): wrong error code");
assert!(
err.message.contains(msg),
"{method} (id {id}): message lacks {msg:?}: {err:?}"
);
}
}
#[test]
fn handle_command_explicit_error_message_matrix() {
let cases: Vec<(i64, &str, Option<Value>, i64, &str)> = vec![
(7, "Page.getLayoutMetrics", None, -32603, "no servo bridge"),
(9, "Page.navigate", None, -32603, "no servo bridge"),
(
10,
"Page.navigate",
Some(json!({"url": "https://example.com"})),
-32603,
"no servo bridge",
),
(11, "Page.reload", None, -32603, "no servo bridge"),
(12, "Page.getFrameTree", None, -32603, "no servo bridge"),
(13, "Page.getNavigationHistory", None, -32000, "not supported"),
(14, "Page.captureScreenshot", None, -32603, "no servo bridge"),
(
16,
"Page.removeScriptToEvaluateOnNewDocument",
Some(json!({"identifier": "3"})),
-32603,
"no servo bridge",
),
(17, "Page.setContent", None, -32602, "html"),
(
18,
"Target.createBrowserContext",
None,
-32603,
"no servo bridge connected",
),
(
19,
"Target.disposeBrowserContext",
Some(json!({"browserContextId": "bao-context-1"})),
-32603,
"no servo bridge connected",
),
(
20,
"Target.getBrowserContexts",
None,
-32603,
"no servo bridge connected",
),
(
21,
"Target.disposeBrowserContext",
None,
-32602,
"requires a non-empty browserContextId param",
),
];
for (id, method, params, code, msg) in cases {
let resp = dispatch_no_bridge(id, method, params);
let err = resp
.error
.unwrap_or_else(|| panic!("{method} (id {id}): explicit error required"));
assert_eq!(err.code, code, "{method} (id {id}): wrong error code");
assert!(
err.message.contains(msg),
"{method} (id {id}): message lacks {msg:?}: {err:?}"
);
}
}
#[test]
fn handle_command_explicit_error_code_only_matrix() {
let cases: Vec<(i64, &str, Option<Value>)> = vec![
(18, "Page.close", None),
(19, "Page.bringToFront", None),
(39, "DOM.getOuterHTML", None),
(44, "Network.getResponseBody", None),
(46, "Network.setExtraHTTPHeaders", None),
];
for (id, method, params) in cases {
let resp = dispatch_no_bridge(id, method, params);
let err = resp
.error
.unwrap_or_else(|| panic!("{method} (id {id}): explicit error required"));
assert_eq!(err.code, -32603, "{method} (id {id}): wrong error code");
}
}
#[test]
fn handle_command_runtime_evaluate_no_bridge() {
let resp = dispatch_no_bridge(9, "Runtime.evaluate", Some(json!({"expression": ""})));
assert!(resp.error.is_none());
let result = resp.result.unwrap();
assert_eq!(result["result"]["type"], "undefined");
}
#[test]
fn handle_command_dom_get_document() {
let resp = dispatch_no_bridge(10, "DOM.getDocument", None);
let err = resp.error.unwrap_or_else(|| panic!("explicit error required"));
assert_eq!(err.code, -32603);
assert!(err.message.contains("no servo bridge"));
}
#[test]
fn handle_command_dom_query_selector() {
let resp = dispatch_no_bridge(11, "DOM.querySelector", Some(json!({"selector": "div"})));
let err = resp.error.unwrap_or_else(|| panic!("explicit error required"));
assert_eq!(err.code, -32603);
}
#[test]
fn handle_command_network_get_cookies() {
let resp = dispatch_no_bridge(13, "Network.getCookies", None);
assert!(resp.error.is_none());
let result = resp.result.unwrap();
assert_eq!(result["cookies"], json!([]));
}
#[test]
fn handle_command_css_get_computed_style() {
let resp = dispatch_no_bridge(15, "CSS.getComputedStyleForNode", None);
let err = resp.error.unwrap_or_else(|| panic!("explicit error required"));
assert_eq!(err.code, -32602);
assert!(err.message.contains("nodeId"));
}
#[test]
fn handle_command_debugger_set_breakpoint_by_url() {
let resp = dispatch_no_bridge(20, "Debugger.setBreakpointByUrl", None);
let err = resp.error.unwrap_or_else(|| panic!("explicit error required"));
assert_eq!(err.code, -32603);
}
#[test]
fn cdp_error_clone_and_debug() {
let err = CdpError {
code: -32601,
message: "not found".into(),
};
let cloned = err.clone();
assert_eq!(cloned.code, err.code);
assert_eq!(cloned.message, err.message);
let debug_str = format!("{:?}", err);
assert!(debug_str.contains("-32601"));
assert!(debug_str.contains("not found"));
}
#[test]
fn cdp_event_serialize() {
let ev = CdpEvent {
method: "Page.loadEventFired".into(),
params: Some(json!({"timestamp": 12345})),
};
let s = serialize_event(&ev);
let parsed: Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["method"], "Page.loadEventFired");
assert_eq!(parsed["params"]["timestamp"], 12345);
}
#[test]
fn parse_message_unicode_method() {
let msg = parse_message(r#"{"id":99,"method":"Page.ไฝ ๅฅฝไธ็"}"#).unwrap();
assert_eq!(msg.id, Some(99));
assert_eq!(msg.method, "Page.ไฝ ๅฅฝไธ็");
}
#[test]
fn parse_message_id_zero() {
let msg = parse_message(r#"{"id":0,"method":"Page.enable"}"#).unwrap();
assert_eq!(msg.id, Some(0));
assert_eq!(msg.method, "Page.enable");
}
#[test]
fn parse_message_id_max() {
let msg = parse_message(r#"{"id":9223372036854775807,"method":"Page.enable"}"#).unwrap();
assert_eq!(msg.id, Some(i64::MAX));
}
#[test]
fn parse_message_negative_id() {
let msg = parse_message(r#"{"id":-1,"method":"Page.enable"}"#).unwrap();
assert_eq!(msg.id, Some(-1));
}
#[test]
fn parse_message_id_min() {
let msg = parse_message(r#"{"id":-9223372036854775808,"method":"Page.enable"}"#).unwrap();
assert_eq!(msg.id, Some(i64::MIN));
}
#[test]
fn parse_message_empty_method() {
let msg = parse_message(r#"{"id":1,"method":""}"#).unwrap();
assert_eq!(msg.method, "");
}
#[test]
fn parse_message_method_no_dot() {
let msg = parse_message(r#"{"id":1,"method":"NoDomain"}"#).unwrap();
assert_eq!(msg.method, "NoDomain");
}
#[test]
fn parse_message_method_multiple_dots() {
let msg = parse_message(r#"{"id":1,"method":"Page.navigate.to"}"#).unwrap();
assert_eq!(msg.method, "Page.navigate.to");
let parts: Vec<&str> = msg.method.splitn(2, '.').collect();
assert_eq!(parts[0], "Page");
assert_eq!(parts[1], "navigate.to");
}
#[test]
fn parse_message_empty_string() {
assert!(parse_message("").is_none());
}
#[test]
fn parse_message_whitespace_only() {
assert!(parse_message(" ").is_none());
}
#[test]
fn parse_message_extra_fields() {
let msg = parse_message(r#"{"id":1,"method":"Page.enable","extra":"ignored"}"#);
assert!(msg.is_some());
assert_eq!(msg.unwrap().method, "Page.enable");
}
#[test]
fn parse_message_params_object() {
let msg =
parse_message(r#"{"id":1,"method":"Page.navigate","params":{"url":"http://x.com"}}"#)
.unwrap();
assert!(msg.params.is_some());
assert_eq!(msg.params.unwrap()["url"], "http://x.com");
}
#[test]
fn parse_message_params_array() {
let msg = parse_message(r#"{"id":1,"method":"Test.cmd","params":[1,2,3]}"#).unwrap();
assert!(msg.params.is_some());
assert!(msg.params.unwrap().is_array());
}
#[test]
fn parse_message_params_string() {
let msg = parse_message(r#"{"id":1,"method":"Test.cmd","params":"hello"}"#).unwrap();
assert!(msg.params.is_some());
assert!(msg.params.unwrap().is_string());
}
#[test]
fn parse_message_params_number() {
let msg = parse_message(r#"{"id":1,"method":"Test.cmd","params":42}"#).unwrap();
assert!(msg.params.is_some());
assert!(msg.params.unwrap().is_number());
}
#[test]
fn parse_message_params_boolean() {
let msg = parse_message(r#"{"id":1,"method":"Test.cmd","params":true}"#).unwrap();
assert!(msg.params.is_some());
assert!(msg.params.unwrap().is_boolean());
}
#[test]
fn parse_message_long_session_id() {
let long_session = "A".repeat(10000);
let raw = format!(
r#"{{"id":1,"method":"Page.enable","sessionId":"{}"}}"#,
long_session
);
let msg = parse_message(&raw).unwrap();
assert_eq!(msg.session_id.unwrap().len(), 10000);
}
#[test]
fn parse_message_empty_session_id() {
let msg = parse_message(r#"{"id":1,"method":"Page.enable","sessionId":""}"#).unwrap();
assert_eq!(msg.session_id, Some("".to_string()));
}
#[test]
fn serialize_response_null_result() {
let resp = CdpResponse {
id: Some(1),
result: Some(Value::Null),
error: None,
};
let s = serialize_response(&resp);
let parsed: Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["id"], 1);
assert_eq!(parsed["result"], Value::Null);
}
#[test]
fn serialize_response_empty_object_result() {
let resp = CdpResponse {
id: Some(2),
result: Some(json!({})),
error: None,
};
let s = serialize_response(&resp);
let parsed: Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["result"], json!({}));
}
#[test]
fn serialize_response_nested_result() {
let resp = CdpResponse {
id: Some(3),
result: Some(json!({"root": {"nodeId": 1, "children": [{"nodeId": 2}]}})),
error: None,
};
let s = serialize_response(&resp);
let parsed: Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["result"]["root"]["nodeId"], 1);
assert_eq!(parsed["result"]["root"]["children"][0]["nodeId"], 2);
}
#[test]
fn serialize_response_id_zero() {
let resp = CdpResponse {
id: Some(0),
result: Some(json!({"ok": true})),
error: None,
};
let s = serialize_response(&resp);
let parsed: Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["id"], 0);
}
#[test]
fn serialize_response_negative_id() {
let resp = CdpResponse {
id: Some(-42),
result: Some(json!({})),
error: None,
};
let s = serialize_response(&resp);
let parsed: Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["id"], -42);
}
#[test]
fn serialize_response_max_id() {
let resp = CdpResponse {
id: Some(i64::MAX),
result: Some(json!({})),
error: None,
};
let s = serialize_response(&resp);
let parsed: Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["id"], i64::MAX);
}
#[test]
fn serialize_response_array_result() {
let resp = CdpResponse {
id: Some(5),
result: Some(json!([1, 2, 3])),
error: None,
};
let s = serialize_response(&resp);
let parsed: Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["result"], json!([1, 2, 3]));
}
#[test]
fn serialize_response_string_result() {
let resp = CdpResponse {
id: Some(6),
result: Some(json!("hello world")),
error: None,
};
let s = serialize_response(&resp);
let parsed: Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["result"], "hello world");
}
#[test]
fn cdp_error_code_parse_error() {
let resp = CdpResponse {
id: Some(1),
result: None,
error: Some(CdpError {
code: -32700,
message: "Parse error".into(),
}),
};
let s = serialize_response(&resp);
let parsed: Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["error"]["code"], -32700);
assert_eq!(parsed["error"]["message"], "Parse error");
}
#[test]
fn cdp_error_code_invalid_request() {
let err = CdpError {
code: -32600,
message: "Invalid Request".into(),
};
assert_eq!(err.code, -32600);
let s = serde_json::to_string(&err).unwrap();
assert!(s.contains("-32600"));
}
#[test]
fn cdp_error_code_method_not_found() {
let err = CdpError {
code: -32601,
message: "Method not found".into(),
};
assert_eq!(err.code, -32601);
}
#[test]
fn cdp_error_code_invalid_params() {
let err = CdpError {
code: -32602,
message: "Invalid params".into(),
};
assert_eq!(err.code, -32602);
}
#[test]
fn cdp_error_code_internal_error() {
let err = CdpError {
code: -32603,
message: "Internal error".into(),
};
assert_eq!(err.code, -32603);
}
#[test]
fn cdp_error_custom_code() {
let err = CdpError {
code: -32000,
message: "Server error".into(),
};
let s = serde_json::to_string(&err).unwrap();
assert!(s.contains("-32000"));
assert!(s.contains("Server error"));
}
#[test]
fn cdp_error_empty_message() {
let err = CdpError {
code: -32601,
message: String::new(),
};
let s = serde_json::to_string(&err).unwrap();
assert!(s.contains("-32601"));
}
#[test]
fn cdp_error_unicode_message() {
let err = CdpError {
code: -32601,
message: "ๆนๆณๆชๆพๅฐ".into(),
};
let s = serde_json::to_string(&err).unwrap();
assert!(s.contains("ๆนๆณๆชๆพๅฐ"));
}
#[test]
fn cdp_event_no_params() {
let ev = CdpEvent {
method: "Page.domContentEventFired".into(),
params: None,
};
let s = serialize_event(&ev);
let parsed: Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["method"], "Page.domContentEventFired");
assert!(
parsed.get("params").is_none(),
"params should be skipped when None"
);
}
#[test]
fn cdp_event_large_data() {
let large_string = "X".repeat(100_000);
let ev = CdpEvent {
method: "Network.dataReceived".into(),
params: Some(
json!({ "dataLength": large_string.len(), "encodedDataLength": large_string.len() }),
),
};
let s = serialize_event(&ev);
let parsed: Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["params"]["dataLength"], 100_000);
}
#[test]
fn cdp_event_nested_params() {
let ev = CdpEvent {
method: "DOM.attributeModified".into(),
params: Some(json!({
"nodeId": 1,
"name": "class",
"value": "container active",
"metadata": {
"source": "user",
"timestamp": 1234567890
}
})),
};
let s = serialize_event(&ev);
let parsed: Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["params"]["nodeId"], 1);
assert_eq!(parsed["params"]["metadata"]["source"], "user");
}
#[test]
fn cdp_event_null_params() {
let ev = CdpEvent {
method: "Page.frameResized".into(),
params: Some(Value::Null),
};
let s = serialize_event(&ev);
let parsed: Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["params"], Value::Null);
}
#[test]
fn cdp_event_empty_method() {
let ev = CdpEvent {
method: String::new(),
params: None,
};
let s = serialize_event(&ev);
let parsed: Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["method"], "");
}
#[test]
fn handle_command_no_dot_method() {
let resp = dispatch_no_bridge(1, "NoDomain", None);
assert!(resp.result.is_none());
let err = resp.error.unwrap();
assert_eq!(err.code, -32601);
assert!(err.message.contains("NoDomain"));
}
#[test]
fn handle_command_empty_method() {
let resp = dispatch_no_bridge(2, "", None);
assert!(resp.result.is_none());
let err = resp.error.unwrap();
assert_eq!(err.code, -32601);
}
#[test]
fn handle_command_known_domain_unknown_command() {
let resp = dispatch_no_bridge(3, "Page.nonExistentCommand", None);
assert!(resp.result.is_none());
let err = resp.error.unwrap();
assert_eq!(err.code, -32601);
assert!(err.message.contains("Page.nonExistentCommand"));
}
#[test]
fn handle_command_target_get_target_info() {
let resp = dispatch_no_bridge(4, "Target.getTargetInfo", None);
assert!(resp.error.is_none());
let result = resp.result.unwrap();
let info = result.get("targetInfo").unwrap();
assert_eq!(info["targetId"], "t1");
assert_eq!(info["type"], "page");
assert_eq!(info["attached"], true);
}
#[test]
fn handle_command_target_get_target_targets() {
let resp = dispatch_no_bridge(8, "Target.getTargetTargets", None);
assert!(resp.error.is_none());
let result = resp.result.unwrap();
assert!(result.get("targetInfos").unwrap().as_array().unwrap().len() > 0);
}
#[test]
fn handle_command_page_add_script_empty_source() {
let resp = dispatch_no_bridge(15, "Page.addScriptToEvaluateOnNewDocument", None);
let err = resp.error.expect("no bridge = no registration possible");
assert_eq!(err.code, -32603);
assert!(err.message.contains("no servo bridge"), "{err:?}");
}
#[test]
fn handle_command_runtime_call_function_on() {
let resp = dispatch_no_bridge(22, "Runtime.callFunctionOn", None);
assert!(resp.error.is_none());
let result = resp.result.unwrap();
assert_eq!(result["result"]["type"], "undefined");
}
#[test]
fn handle_command_runtime_get_properties() {
let resp = dispatch_no_bridge(23, "Runtime.getProperties", None);
assert!(resp.error.is_none());
let result = resp.result.unwrap();
assert_eq!(result["result"], json!([]));
}
#[test]
fn handle_command_runtime_unknown_command() {
let resp = dispatch_no_bridge(29, "Runtime.unknownMethod", None);
assert!(resp.result.is_none());
let err = resp.error.unwrap();
assert_eq!(err.code, -32601);
assert!(err.message.contains("Runtime.unknownMethod"));
}
#[test]
fn handle_command_dom_describe_node() {
let resp = dispatch_no_bridge(32, "DOM.describeNode", None);
let err = resp.error.unwrap_or_else(|| panic!("explicit error required"));
assert_eq!(err.code, -32602);
assert!(err.message.contains("nodeId or objectId"));
}
#[test]
fn handle_command_dom_get_box_model() {
let resp = dispatch_no_bridge(33, "DOM.getBoxModel", None);
let err = resp.error.unwrap_or_else(|| panic!("explicit error required"));
assert_eq!(err.code, -32602);
assert!(err.message.contains("nodeId or objectId"));
}
#[test]
fn handle_command_dom_resolve_node() {
let resp = dispatch_no_bridge(40, "DOM.resolveNode", None);
let err = resp.error.unwrap_or_else(|| panic!("explicit error required"));
assert_eq!(err.code, -32602);
assert!(err.message.contains("nodeId, objectId or backendNodeId"));
}
#[test]
fn handle_command_dom_push_nodes_by_backend_ids() {
let resp = dispatch_no_bridge(41, "DOM.pushNodesByBackendIdsToFrontend", None);
let err = resp.error.unwrap_or_else(|| panic!("explicit error required"));
assert_eq!(err.code, -32602);
assert!(err.message.contains("backendNodeIds"));
}
#[test]
fn handle_command_network_get_all_cookies() {
let resp = dispatch_no_bridge(48, "Network.getAllCookies", None);
assert!(resp.error.is_none());
let result = resp.result.unwrap();
assert_eq!(result["cookies"], json!([]));
}
#[test]
fn handle_command_network_set_cookie() {
let resp = dispatch_no_bridge(50, "Network.setCookie", None);
assert!(resp.error.is_none());
assert_eq!(resp.result, Some(json!({ "success": true })));
}
#[test]
fn handle_command_css_get_matched_styles() {
let resp = dispatch_no_bridge(55, "CSS.getMatchedStylesForNode", None);
let err = resp.error.unwrap_or_else(|| panic!("explicit error required"));
assert_eq!(err.code, -32602);
assert!(err.message.contains("nodeId"));
}
#[test]
fn handle_command_css_get_inline_styles() {
let resp = dispatch_no_bridge(56, "CSS.getInlineStylesForNode", None);
let err = resp.error.unwrap_or_else(|| panic!("explicit error required"));
assert_eq!(err.code, -32602);
assert!(err.message.contains("nodeId"));
}
#[test]
fn handle_command_css_dom_query_methods_no_bridge_explicit_error() {
let node = Some(json!({ "nodeId": 1 }));
for (method, params, code) in [
("CSS.getComputedStyleForNode", node.clone(), -32603),
("CSS.getMatchedStylesForNode", node.clone(), -32603),
("CSS.getInlineStylesForNode", node.clone(), -32603),
("DOM.getFlattenedDocument", None, -32603),
("DOM.getNodeForOwner", None, -32602),
] {
let resp = dispatch_no_bridge(80, method, params);
let err = resp
.error
.unwrap_or_else(|| panic!("{method}: explicit error required"));
assert_eq!(err.code, code, "{method}: wrong error code");
assert!(resp.result.is_none(), "{method}: error must not carry result");
}
}
#[test]
fn handle_command_css_set_style_texts() {
let resp = dispatch_no_bridge(57, "CSS.setStyleTexts", None);
let err = resp.error.unwrap_or_else(|| panic!("explicit error required"));
assert_eq!(err.code, -32602);
assert!(err.message.contains("edits"));
}
#[test]
fn handle_command_debugger_evaluate_on_call_frame() {
let resp = dispatch_no_bridge(87, "Debugger.evaluateOnCallFrame", None);
assert!(resp.error.is_none());
let result = resp.result.unwrap();
assert_eq!(result["result"]["type"], "undefined");
}
#[test]
fn handle_command_debugger_get_possible_breakpoints() {
let resp = dispatch_no_bridge(88, "Debugger.getPossibleBreakpoints", None);
assert!(resp.error.is_none());
let result = resp.result.unwrap();
assert_eq!(result["locations"], json!([]));
}
#[test]
fn handle_command_debugger_get_script_source() {
let resp = dispatch_no_bridge(89, "Debugger.getScriptSource", None);
assert!(resp.error.is_none());
let result = resp.result.unwrap();
assert_eq!(result["scriptSource"], "");
}
#[test]
fn params_str_nested_key_returns_empty() {
let params = Some(json!({"outer": {"inner": "value"}}));
let result = params_str(¶ms, "outer.inner");
assert_eq!(result, ""); }
#[test]
fn params_str_numeric_value_returns_empty() {
let params = Some(json!({"count": 42}));
let result = params_str(¶ms, "count");
assert_eq!(result, ""); }
#[test]
fn params_str_boolean_value_returns_empty() {
let params = Some(json!({"flag": true}));
let result = params_str(¶ms, "flag");
assert_eq!(result, ""); }
#[test]
fn params_str_null_value_returns_empty() {
let params = Some(json!({"key": null}));
let result = params_str(¶ms, "key");
assert_eq!(result, ""); }
#[test]
fn params_str_missing_key_returns_empty() {
let params = Some(json!({"other": "value"}));
let result = params_str(¶ms, "key");
assert_eq!(result, "");
}
#[test]
fn params_str_none_returns_empty() {
let result = params_str(&None, "key");
assert_eq!(result, "");
}
#[test]
fn params_str_empty_string_value() {
let params = Some(json!({"key": ""}));
let result = params_str(¶ms, "key");
assert_eq!(result, "");
}
#[test]
fn handle_command_bridge_required_no_bridge_returns_internal_error() {
let result = bridge_send(
None,
BridgeCommand::GetTitle {
target_id: "test-target".into(),
},
);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.code, -32603);
assert!(err.message.contains("no servo bridge connected"));
}
#[test]
fn bridge_send_maps_error_kind_to_cdp_code() {
use crate::servo_bridge::BridgeError;
let (bridge, receiver) = bridge_channel(std::time::Duration::from_secs(2));
let responder = std::thread::spawn(move || {
receiver.recv_and_process(std::time::Duration::from_secs(2), |cmd| BridgeResponse {
result: Err(match cmd {
BridgeCommand::FetchResolve { .. } => {
BridgeError::domain(
"unknown interceptionId: i-1 (never paused or already resolved)",
)
}
_ => BridgeError::no_bridge("bridge response timeout"),
}),
})
});
let msg = cdp_msg(
90,
"Fetch.continueRequest",
Some(json!({"interceptionId": "i-1"})),
);
let params = msg.params.clone();
let resp = handle_command(msg, "t1", ¶ms, Some(&bridge));
let err = resp.error.expect("unknown interceptionId must fail explicitly");
assert_eq!(err.code, ERR_NOT_SUPPORTED, "domain class โ -32000: {err:?}");
assert!(
err.message.contains("unknown interceptionId"),
"message verbatim: {err:?}"
);
let msg = cdp_msg(91, "Page.navigate", Some(json!({"url": "https://example.com/"})));
let params = msg.params.clone();
let resp = handle_command(msg, "t1", ¶ms, Some(&bridge));
let err = resp.error.expect("transport failure must fail explicitly");
assert_eq!(err.code, -32603, "transport class โ -32603: {err:?}");
responder.join().unwrap();
}
#[test]
fn cdp_message_debug_format() {
let msg = cdp_msg(1, "Page.enable", None);
let debug = format!("{:?}", msg);
assert!(debug.contains("CdpMessage"));
assert!(debug.contains("Page.enable"));
}
#[test]
fn cdp_message_clone() {
let msg = CdpMessage {
id: Some(1),
method: "Page.enable".into(),
params: Some(json!({"k": "v"})),
session_id: Some("s1".into()),
};
let cloned = msg.clone();
assert_eq!(cloned.id, msg.id);
assert_eq!(cloned.method, msg.method);
assert_eq!(cloned.params, msg.params);
assert_eq!(cloned.session_id, msg.session_id);
}
#[test]
fn service_worker_get_all_registrations_no_bridge() {
let resp = dispatch_no_bridge(3, "ServiceWorker.getAllRegistrations", None);
assert!(resp.error.is_none());
let result = resp.result.unwrap();
assert_eq!(result["registrations"], json!([]));
}
#[test]
fn service_worker_get_registration_no_bridge() {
let resp = dispatch_no_bridge(
4,
"ServiceWorker.getRegistration",
Some(json!({"registrationId": "sw-reg-1"})),
);
assert!(resp.error.is_none());
let result = resp.result.unwrap();
assert_eq!(result["registration"], Value::Null);
}
#[test]
fn service_worker_deliver_push_message() {
let resp = dispatch_no_bridge(
7,
"ServiceWorker.deliverPushMessage",
Some(json!({"origin": "https://example.com", "registrationId": "sw-reg-1"})),
);
assert!(resp.error.is_none());
let result = resp.result.unwrap();
assert_eq!(result["origin"], "https://example.com");
assert_eq!(result["delivered"], true);
}
#[test]
fn target_get_targets_no_bridge_includes_page() {
let resp = dispatch_no_bridge(10, "Target.getTargets", None);
assert!(resp.error.is_none());
let result = resp.result.unwrap();
let infos = result["targetInfos"].as_array().unwrap();
assert!(infos.len() >= 1, "should at least have the page target");
assert_eq!(infos[0]["targetId"], "t1");
}
#[test]
fn overlay_highlight_node_no_bridge() {
let resp = dispatch_no_bridge(
80,
"Overlay.highlightNode",
Some(json!({"nodeId": 5})),
);
let err = resp.error.unwrap_or_else(|| panic!("explicit error required"));
assert_eq!(err.code, -32603);
assert!(err.message.contains("no servo bridge connected"));
}
#[test]
fn overlay_highlight_node_missing_node_ref() {
let resp = dispatch_no_bridge(81, "Overlay.highlightNode", None);
let err = resp.error.unwrap_or_else(|| panic!("explicit error required"));
assert_eq!(err.code, -32602);
assert!(err.message.contains("nodeId, objectId or backendNodeId"));
}
#[test]
fn overlay_set_inspect_mode_not_supported() {
let resp = dispatch_no_bridge(82, "Overlay.setInspectMode", None);
let err = resp.error.unwrap_or_else(|| panic!("explicit error required"));
assert_eq!(err.code, ERR_NOT_SUPPORTED);
assert!(err.message.contains("Overlay.setInspectMode"));
assert!(err.message.contains("nodeHighlightRequested"));
}
#[test]
fn overlay_highlight_node_backend_node_id_accepted() {
let resp = dispatch_no_bridge(
83,
"Overlay.highlightNode",
Some(json!({"backendNodeId": 7, "highlightSpec": {"borderColor": {"r": 255}} })),
);
let err = resp.error.unwrap_or_else(|| panic!("explicit error required"));
assert_eq!(err.code, -32603);
assert!(err.message.contains("no servo bridge connected"));
}
#[test]
fn overlay_hide_highlight_no_bridge() {
let resp = dispatch_no_bridge(84, "Overlay.hideHighlight", None);
let err = resp.error.unwrap_or_else(|| panic!("explicit error required"));
assert_eq!(err.code, -32603);
assert!(err.message.contains("no servo bridge connected"));
}
use crate::servo_bridge::{bridge_channel, BridgeResponse, FetchRequestStage};
fn capture_bridge_command(drive: impl FnOnce(&BridgeSender)) -> BridgeCommand {
let (bridge, receiver) = bridge_channel(std::time::Duration::from_secs(2));
let captured: std::sync::Arc<std::sync::Mutex<Option<BridgeCommand>>> =
std::sync::Arc::default();
let sink = std::sync::Arc::clone(&captured);
let responder = std::thread::spawn(move || {
receiver.recv_and_process(std::time::Duration::from_secs(2), |cmd| {
*sink.lock().unwrap() = Some(cmd);
BridgeResponse {
result: Ok(json!({})),
}
});
});
drive(&bridge);
let cmd = captured
.lock()
.unwrap()
.clone()
.expect("handler should have sent exactly one bridge command");
responder.join().unwrap();
cmd
}
fn expect_dispatch_error(id: i64, method: &str, params: Option<Value>, code: i64, fragment: &str) {
let resp = dispatch_no_bridge(id, method, params);
let err = resp
.error
.unwrap_or_else(|| panic!("{method} (id {id}): explicit error required"));
assert_eq!(err.code, code, "{method} (id {id}): wrong error code");
assert!(
err.message.contains(fragment),
"{method} (id {id}): message lacks {fragment:?}: {err:?}"
);
}
#[test]
fn fetch_enable_sends_normalized_patterns_to_bridge() {
let cmd = capture_bridge_command(|bridge| {
let msg = cdp_msg(
1,
"Fetch.enable",
Some(json!({
"patterns": [
{"urlPattern": "https://example.com/*", "requestStage": "Response"},
{"resourceType": "Document"}
]
})),
);
let params = msg.params.clone();
let resp = handle_command(msg, "t1", ¶ms, Some(bridge));
assert!(
resp.error.is_none(),
"valid patterns should arm the slot: {:?}",
resp.error
);
});
match cmd {
BridgeCommand::FetchEnable {
target_id,
patterns,
legacy_network_events,
} => {
assert_eq!(target_id, "t1");
assert!(!legacy_network_events, "Fetch domain emits requestPaused");
assert_eq!(patterns.len(), 2);
assert_eq!(
patterns[0].url_pattern.as_deref(),
Some("https://example.com/*")
);
assert_eq!(patterns[0].resource_type, None);
assert_eq!(patterns[0].request_stage, FetchRequestStage::Response);
assert_eq!(patterns[1].url_pattern, None);
assert_eq!(patterns[1].resource_type.as_deref(), Some("Document"));
assert_eq!(patterns[1].request_stage, FetchRequestStage::Request);
}
other => panic!("expected FetchEnable, got {other:?}"),
}
}
#[test]
fn fetch_enable_without_patterns_normalizes_catch_all() {
let cmd = capture_bridge_command(|bridge| {
let msg = cdp_msg(2, "Fetch.enable", None);
let params = msg.params.clone();
let resp = handle_command(msg, "t1", ¶ms, Some(bridge));
assert!(resp.error.is_none(), "{:?}", resp.error);
});
match cmd {
BridgeCommand::FetchEnable { patterns, .. } => {
assert_eq!(patterns.len(), 1);
assert_eq!(patterns[0].url_pattern, None);
assert_eq!(patterns[0].resource_type, None);
assert_eq!(patterns[0].request_stage, FetchRequestStage::Request);
}
other => panic!("expected FetchEnable, got {other:?}"),
}
}
#[test]
fn fetch_enable_handle_auth_requests_is_explicit_error() {
expect_dispatch_error(
3,
"Fetch.enable",
Some(json!({"handleAuthRequests": true})),
-32602,
"handleAuthRequests=true is not supported",
);
}
#[test]
fn fetch_enable_malformed_patterns_are_32602() {
let cases: Vec<(&str, Value)> = vec![
("array", json!({"patterns": "*"})),
("object", json!({"patterns": ["*"]})),
("string", json!({"patterns": [{"urlPattern": 5}]})),
("non-empty", json!({"patterns": [{"resourceType": ""}]})),
("Request", json!({"patterns": [{"requestStage": "Headers"}]})),
];
for (i, (fragment, params)) in cases.into_iter().enumerate() {
expect_dispatch_error(
10 + i as i64,
"Fetch.enable",
Some(params),
-32602,
fragment,
);
}
}
#[test]
fn fetch_disable_without_bridge_is_32603() {
expect_dispatch_error(20, "Fetch.disable", None, -32603, "no servo bridge connected");
}
#[test]
fn fetch_continue_request_modification_fields_are_32602() {
for (i, field) in ["url", "method", "postData", "headers"].into_iter().enumerate() {
let params = json!({"interceptionId": "i1", (field): "x"});
expect_dispatch_error(
30 + i as i64,
"Fetch.continueRequest",
Some(params),
-32602,
"request modification is not supported",
);
}
}
#[test]
fn fetch_resolution_missing_interception_id_is_32602() {
for (i, method) in ["Fetch.continueRequest", "Fetch.failRequest", "Fetch.fulfillRequest"]
.iter()
.enumerate()
{
expect_dispatch_error(
40 + i as i64,
method,
Some(json!({"requestId": "not-the-resolution-key"})),
-32602,
"missing required 'interceptionId'",
);
}
}
#[test]
fn fetch_fulfill_request_param_validation_is_32602() {
let cases: Vec<(&str, Value)> = vec![
("missing required 'responseCode'", json!({"interceptionId": "i"})),
(
"'responseCode' must be an integer",
json!({"interceptionId": "i", "responseCode": "404"}),
),
(
"'responseHeaders' must be an object",
json!({"interceptionId": "i", "responseCode": 200, "responseHeaders": []}),
),
(
"'responseHeaders' values must be strings",
json!({"interceptionId": "i", "responseCode": 200, "responseHeaders": {"a": 1}}),
),
(
"'body' must be a base64 string",
json!({"interceptionId": "i", "responseCode": 200, "body": 42}),
),
];
for (i, (fragment, params)) in cases.into_iter().enumerate() {
expect_dispatch_error(
50 + i as i64,
"Fetch.fulfillRequest",
Some(params),
-32602,
fragment,
);
}
}
#[test]
fn fetch_fulfill_request_oversized_body_is_explicit_error() {
let oversized = "A".repeat(8 * 1024 * 1024 * 2);
expect_dispatch_error(
60,
"Fetch.fulfillRequest",
Some(json!({"interceptionId": "i", "responseCode": 200, "body": oversized})),
-32602,
"exceeds the 8 MiB cap",
);
}
#[test]
fn fetch_resolution_commands_carry_bridge_action_shapes() {
let cmd = capture_bridge_command(|bridge| {
let msg =
cdp_msg(70, "Fetch.continueRequest", Some(json!({"interceptionId": "i-7"})));
let params = msg.params.clone();
handle_command(msg, "t1", ¶ms, Some(bridge));
});
match cmd {
BridgeCommand::FetchResolve {
target_id,
interception_id,
action: FetchResolveAction::Continue,
} => {
assert_eq!(target_id, "t1");
assert_eq!(interception_id, "i-7");
}
other => panic!("expected FetchResolve(Continue), got {other:?}"),
}
let cmd = capture_bridge_command(|bridge| {
let msg = cdp_msg(
71,
"Fetch.failRequest",
Some(json!({"interceptionId": "i-8", "errorReason": "Failed"})),
);
let params = msg.params.clone();
handle_command(msg, "t1", ¶ms, Some(bridge));
});
match cmd {
BridgeCommand::FetchResolve {
action: FetchResolveAction::Fail { error_reason },
..
} => assert_eq!(error_reason.as_deref(), Some("Failed")),
other => panic!("expected FetchResolve(Fail), got {other:?}"),
}
let cmd = capture_bridge_command(|bridge| {
let msg = cdp_msg(
72,
"Fetch.fulfillRequest",
Some(json!({
"interceptionId": "i-9",
"responseCode": 200,
"responsePhrase": "OK",
"responseHeaders": {"content-type": "text/html"},
"body": "dGVzdA=="
})),
);
let params = msg.params.clone();
handle_command(msg, "t1", ¶ms, Some(bridge));
});
match cmd {
BridgeCommand::FetchResolve {
target_id,
interception_id,
action: FetchResolveAction::Fulfill {
response_code,
status_message,
response_headers,
body,
},
} => {
assert_eq!(target_id, "t1");
assert_eq!(interception_id, "i-9");
assert_eq!(response_code, 200);
assert_eq!(status_message.as_deref(), Some("OK"));
assert_eq!(
response_headers.unwrap()["content-type"],
json!("text/html")
);
assert_eq!(body.as_deref(), Some("dGVzdA=="));
}
other => panic!("expected FetchResolve(Fulfill), got {other:?}"),
}
}
#[test]
fn legacy_network_set_request_interception_maps_to_fetch_bridge() {
let cmd = capture_bridge_command(|bridge| {
let msg = cdp_msg(
80,
"Network.setRequestInterception",
Some(json!({"patterns": [{"urlPattern": "*", "interceptionStage": "Request"}]})),
);
let params = msg.params.clone();
let resp = handle_command(msg, "t1", ¶ms, Some(bridge));
assert!(resp.error.is_none(), "{:?}", resp.error);
});
match cmd {
BridgeCommand::FetchEnable {
patterns,
legacy_network_events,
..
} => {
assert!(legacy_network_events, "legacy face emits requestIntercepted");
assert_eq!(patterns.len(), 1);
assert_eq!(patterns[0].url_pattern.as_deref(), Some("*"));
assert_eq!(patterns[0].request_stage, FetchRequestStage::Request);
}
other => panic!("expected FetchEnable, got {other:?}"),
}
}
#[test]
fn legacy_network_set_request_interception_rejects_headers_received_stage() {
expect_dispatch_error(
81,
"Network.setRequestInterception",
Some(json!({"patterns": [{"urlPattern": "*", "interceptionStage": "HeadersReceived"}]})),
-32602,
"request-time only",
);
}
#[test]
fn legacy_network_continue_intercepted_request_maps_to_fetch_bridge() {
let cmd = capture_bridge_command(|bridge| {
let msg = cdp_msg(
82,
"Network.continueInterceptedRequest",
Some(json!({"interceptionId": "i-10"})),
);
let params = msg.params.clone();
handle_command(msg, "t1", ¶ms, Some(bridge));
});
match cmd {
BridgeCommand::FetchResolve {
action: FetchResolveAction::Continue,
..
} => {}
other => panic!("expected FetchResolve(Continue), got {other:?}"),
}
let cmd = capture_bridge_command(|bridge| {
let msg = cdp_msg(
83,
"Network.continueInterceptedRequest",
Some(json!({"interceptionId": "i-11", "errorReason": "Aborted"})),
);
let params = msg.params.clone();
handle_command(msg, "t1", ¶ms, Some(bridge));
});
match cmd {
BridgeCommand::FetchResolve {
action: FetchResolveAction::Fail { error_reason },
..
} => assert_eq!(error_reason.as_deref(), Some("Aborted")),
other => panic!("expected FetchResolve(Fail), got {other:?}"),
}
}
#[test]
fn legacy_network_continue_intercepted_request_boundaries() {
expect_dispatch_error(
84,
"Network.continueInterceptedRequest",
Some(json!({"interceptionId": "i", "rawResponse": "SFRUUC8xLjEgMjAw"})),
-32000,
"raw HTTP wire-format response reassembly is phase-2",
);
expect_dispatch_error(
85,
"Network.continueInterceptedRequest",
Some(json!({"interceptionId": "i", "url": "https://example.com/"})),
-32602,
"request modification is not supported",
);
expect_dispatch_error(
86,
"Network.continueInterceptedRequest",
None,
-32602,
"missing required 'interceptionId'",
);
}
#[test]
fn network_emulate_network_conditions_stays_accepted_noop() {
let resp = dispatch_no_bridge(87, "Network.emulateNetworkConditions", None);
assert!(resp.error.is_none());
assert_eq!(resp.result.unwrap(), json!({}));
}
#[test]
fn fetch_fail_request_non_string_error_reason_is_32602() {
expect_dispatch_error(
88,
"Fetch.failRequest",
Some(json!({"interceptionId": "i", "errorReason": 7})),
-32602,
"'errorReason' must be a CDP ErrorReason string",
);
}
}