fix(cdp): route navigation on exact method, not substring (#372)

The cdp_processor decided whether to take the spawn-and-defer navigation path
with `text.contains("Page.navigate")`, which also matches
`Page.navigateToHistoryEntry`. goBack / goForward (both Puppeteer and
Playwright drive Page.navigateToHistoryEntry) were misrouted into the raw
navigate path, which reads an absent `url` param and fails with
"Invalid URL: relative URL without a base", so history navigation never
reached its real handler. Any frame merely embedding the literal
"Page.navigate" (e.g. a Runtime.evaluate expression) was misrouted the same
way.

Match on the parsed CDP method instead, so only an exact Page.navigate takes
the navigation path.

Fixes #363.
This commit is contained in:
Andrey Mnatsakanov authored and GitHub committed 2026-07-10 20:23:53 +00:00
1 parent 4bc69c5209
commit daa0d62fbd
1 file changed
+44 -1
+44 -1
View File
@@ -432,7 +432,7 @@ async fn cdp_processor(
// messages via the `process_with_interception` select loop;
// unrelated requests get deferred only briefly and are drained
// as soon as the nav settles.
let is_navigation = cdp_msg.text.contains("Page.navigate");
let is_navigation = is_navigate_method(&cdp_msg.text);
if is_navigation {
process_with_interception(
@@ -457,6 +457,18 @@ async fn cdp_processor(
ctx.default_context.save_cookies();
}
// Whether a raw CDP frame is exactly a `Page.navigate` call, and so should take
// the spawn-and-defer navigation path. Matching on the parsed method rather than
// a `contains("Page.navigate")` substring avoids catching
// `Page.navigateToHistoryEntry` (goBack / goForward), which has no `url` param
// and belongs to its own handler, or any other frame that merely embeds the
// literal text (e.g. a `Runtime.evaluate` expression). See issue #363.
fn is_navigate_method(text: &str) -> bool {
serde_json::from_str::<CdpRequest>(text)
.map(|req| req.method == "Page.navigate")
.unwrap_or(false)
}
fn handle_fetch_resolution(
text: &str,
_ctx: &mut CdpContext,
@@ -994,3 +1006,34 @@ async fn handle_connection_ws(
send_task.abort();
Ok(())
}
#[cfg(test)]
mod tests {
use super::is_navigate_method;
// Issue #363: only an exact Page.navigate may take the spawn-and-defer
// navigation path. A substring match also caught Page.navigateToHistoryEntry
// (goBack / goForward), which has no `url` param, so it was misrouted into
// the raw-navigate path and failed with "Invalid URL" instead of reaching
// its real handler.
#[test]
fn only_exact_page_navigate_routes_as_navigation() {
assert!(is_navigate_method(
r#"{"id":1,"method":"Page.navigate","params":{"url":"https://example.com"}}"#
));
assert!(!is_navigate_method(
r#"{"id":2,"method":"Page.navigateToHistoryEntry","params":{"entryId":0}}"#
));
}
// A Runtime.evaluate whose expression merely contains the literal
// "Page.navigate" must not be misrouted, and malformed input is not a
// navigation.
#[test]
fn unrelated_methods_do_not_route_as_navigation() {
assert!(!is_navigate_method(
r#"{"id":3,"method":"Runtime.evaluate","params":{"expression":"'Page.navigate'"}}"#
));
assert!(!is_navigate_method("not json"));
}
}