fix(net): scope passive callbacks to the registering page (#414)

The on_request/on_response Vec lived on the ObscuraHttpClient shared by
every page in the context, so a callback registered on one page fired for
all pages (#408 cross-page bleed) and outlived its page.

Callbacks now live in a per-page CallbackRegistry (Arc shared with the
page's JS runtime state). Page-driven fetch paths (navigation, POST,
scripts, stylesheets, robots.txt, module loads, JS fetch/XHR in both
plain and stealth modes) pass the registry through; the HTTP client
itself is callback-free.

Adds callbacks_do_not_bleed_across_pages: page A's callback stays silent
for page B's requests and stops firing after page A is dropped.
This commit is contained in:
Jim Zhang @ wonderhoi authored and GitHub committed 2026-07-16 12:43:13 +02:00
1 parent ccfa5fb87b
commit f5dc97b131
8 files changed
+249 -108

No files matched your search

+11 -2
View File
@@ -153,6 +153,12 @@ impl ObscuraJsRuntime {
self.state.borrow_mut().http_client = Some(client);
}
/// Install the owning page's passive on_request/on_response callback
/// registry so scripted fetch()/XHR observation is page-scoped (issue #408).
pub fn set_callbacks(&self, callbacks: std::sync::Arc<obscura_net::CallbackRegistry>) {
self.state.borrow_mut().callbacks = Some(callbacks);
}
/// Install the stealth (wreq) HTTP client so scripted fetch()/XHR is routed
/// through it in stealth mode (see op_fetch_url / stealth_fetch_all).
#[cfg(feature = "stealth")]
@@ -631,9 +637,12 @@ impl ObscuraJsRuntime {
// Fetch the module source. The old impl registered an empty string
// and called it loaded, so every Vite / Next module bundle "loaded"
// in 1ms with zero code and the SPA never mounted (issue #205).
let client = self.state.borrow().http_client.clone();
let (client, callbacks) = {
let st = self.state.borrow();
(st.http_client.clone(), st.callbacks.clone())
};
let source_code = match client {
Some(c) => match c.fetch(&specifier).await {
Some(c) => match c.fetch_with_callbacks(&specifier, callbacks.as_deref()).await {
Ok(resp) => obscura_net::decode_non_html(&resp.body, resp.content_type()),
Err(e) => {
tracing::warn!("Module fetch failed ({}): {}", url, e);