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:
SGavrl authored and GitHub committed 2026-07-14 13:57:45 +00:00
1 parent d0b6fc4ffd
commit 99ece0e12d
5 files changed
+147 -23

No files matched your search

+23 -10
View File
@@ -1652,19 +1652,32 @@ impl Page {
/// Register a passive callback fired for every JS `fetch()`/XHR (and
/// navigation) request, once the 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) {
if let Ok(mut v) = self.http_client.on_request.try_write() {
v.push(cb);
}
/// Register a passive request observer, returning a stable id. Pass the id
/// to `off_request` to detach it (issue #408). Note the observer lives on
/// the context's shared http client, so it also sees requests from sibling
/// pages in the same context; detach it when the capturing phase ends.
pub fn on_request(&mut self, cb: RequestCallback) -> u64 {
self.http_client.register_on_request(cb)
}
/// Register a passive callback fired with every JS `fetch()`/XHR (and
/// navigation) response, including its body. Non-blocking. The main path for
/// crawlers that need to capture API response payloads.
pub fn on_response(&mut self, cb: ResponseCallback) {
if let Ok(mut v) = self.http_client.on_response.try_write() {
v.push(cb);
}
/// navigation) response, including its body. Non-blocking. Returns a stable
/// id for `off_response`. The main path for crawlers that need to capture
/// API response payloads. Shared across the context like `on_request`.
pub fn on_response(&mut self, cb: ResponseCallback) -> u64 {
self.http_client.register_on_response(cb)
}
/// Detach a request observer registered with `on_request`. Returns true if
/// one was removed.
pub fn off_request(&mut self, id: u64) -> bool {
self.http_client.remove_on_request(id)
}
/// Detach a response observer registered with `on_response`. Returns true if
/// one was removed.
pub fn off_response(&mut self, id: u64) -> bool {
self.http_client.remove_on_response(id)
}
pub async fn process_pending_navigation(&mut self) -> Result<bool, PageError> {