feat(net): make passive request/response callbacks detachable (#416)
on_request/on_response stored callbacks in append-only Vecs with no way to remove one, so a crawler that installed a response-body capture hook for one phase could never detach it: it fired for the shared client's whole lifetime, re-registering across navigations piled up duplicates, and (because the client is shared per context) it saw sibling pages' requests too. Tag each callback with a stable id. on_request/on_response now return the id and off_request/off_response (remove_on_request/remove_on_response on the client) detach a specific one. Storage is Vec<(u64, Callback)>; all fire sites destructure the tuple. Closes #408.
This commit is contained in:
1 parent
d0b6fc4ffd
commit
99ece0e12d
5 files changed
+147
-23
No files matched your search
@@ -114,16 +114,31 @@ impl Page {
|
||||
/// Register a passive callback fired for every request the page makes
|
||||
/// (navigation and JS `fetch()`/XHR), once its method/headers/body are known
|
||||
/// and before it is sent. Non-blocking; use `enable_interception` to mutate
|
||||
/// or block.
|
||||
pub fn on_request(&mut self, cb: RequestCallback) {
|
||||
self.inner.on_request(cb);
|
||||
/// or block. Returns a stable id; pass it to `off_request` to detach.
|
||||
pub fn on_request(&mut self, cb: RequestCallback) -> u64 {
|
||||
self.inner.on_request(cb)
|
||||
}
|
||||
|
||||
/// Register a passive callback fired with every response the page receives
|
||||
/// (navigation and JS `fetch()`/XHR), including its body. Non-blocking. The
|
||||
/// main path for capturing API response payloads from SPAs.
|
||||
pub fn on_response(&mut self, cb: ResponseCallback) {
|
||||
self.inner.on_response(cb);
|
||||
/// main path for capturing API response payloads from SPAs. Returns a stable
|
||||
/// id; pass it to `off_response` to detach.
|
||||
pub fn on_response(&mut self, cb: ResponseCallback) -> u64 {
|
||||
self.inner.on_response(cb)
|
||||
}
|
||||
|
||||
/// Detach a request callback previously registered with `on_request`.
|
||||
/// Returns true if a callback with that id was removed. Callbacks live on
|
||||
/// the context's shared HTTP client, so detach them when a page or capture
|
||||
/// phase ends to avoid firing for sibling pages (issue #408).
|
||||
pub fn off_request(&mut self, id: u64) -> bool {
|
||||
self.inner.off_request(id)
|
||||
}
|
||||
|
||||
/// Detach a response callback previously registered with `on_response`.
|
||||
/// Returns true if a callback with that id was removed.
|
||||
pub fn off_response(&mut self, id: u64) -> bool {
|
||||
self.inner.off_response(id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -165,3 +165,49 @@ async fn page_rewrites_request_url_via_interception() {
|
||||
body
|
||||
);
|
||||
}
|
||||
|
||||
// Issue #408: a callback registered with on_response must be detachable via the
|
||||
// returned id, so a crawler can stop capturing after a phase. Before the fix the
|
||||
// vecs were append-only and a callback fired for the client's whole lifetime.
|
||||
#[tokio::test]
|
||||
async fn on_response_callback_can_be_detached() {
|
||||
std::env::set_var("OBSCURA_ALLOW_PRIVATE_NETWORK", "1");
|
||||
let base = spawn_echo_server();
|
||||
|
||||
let browser = Browser::new().unwrap();
|
||||
let mut page = browser.new_page().await.unwrap();
|
||||
|
||||
let hits = Arc::new(AtomicU32::new(0));
|
||||
let h = hits.clone();
|
||||
let id = page.on_response(Arc::new(move |info, _resp| {
|
||||
if info.resource_type == ResourceType::Fetch {
|
||||
h.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
}));
|
||||
|
||||
// First navigation: the callback fires for the JS fetch('/api').
|
||||
page.goto(&base).await.unwrap();
|
||||
for _ in 0..20 {
|
||||
page.settle(200).await;
|
||||
if hits.load(Ordering::SeqCst) >= 1 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let after_first = hits.load(Ordering::SeqCst);
|
||||
assert!(after_first >= 1, "on_response should fire while attached");
|
||||
|
||||
// Detach it; off_response must report success and the id must be gone.
|
||||
assert!(page.off_response(id), "off_response must remove the callback");
|
||||
assert!(!page.off_response(id), "removing an already-removed id returns false");
|
||||
|
||||
// Second navigation: the detached callback must not fire again.
|
||||
page.goto(&base).await.unwrap();
|
||||
for _ in 0..10 {
|
||||
page.settle(200).await;
|
||||
}
|
||||
assert_eq!(
|
||||
hits.load(Ordering::SeqCst),
|
||||
after_first,
|
||||
"detached on_response callback must not fire after off_response"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user