From 1f5066fc30ffd37f447d0787c3893322cdf083ae Mon Sep 17 00:00:00 2001 From: SGavrl <277843151+SGavrl@users.noreply.github.com> Date: Wed, 24 Jun 2026 21:36:01 +0000 Subject: [PATCH] api: add request/response interception API to Page (#306) Expose the interception infrastructure that already existed internally but was private, so embedders can intercept, observe, mock, or block JS fetch()/XHR without forking or unsafe casts. On obscura::Page: - add_preload_script: inject JS before any page ".to_string()) + }; + let resp = format!( + "HTTP/1.1 200 OK\r\nContent-Type: {}\r\nContent-Length: {}\r\nAccess-Control-Allow-Origin: *\r\nConnection: close\r\n\r\n{}", + ct, + body.len(), + body + ); + let _ = s.write_all(resp.as_bytes()); + let _ = s.shutdown(std::net::Shutdown::Both); + } + }); + format!("http://{}", addr) +} + +#[tokio::test] +async fn page_intercepts_and_observes_js_fetch() { + 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(); + + // Passive on_request counter (fires for navigation + the JS fetch). + let req_count = Arc::new(AtomicU32::new(0)); + let rc = req_count.clone(); + page.on_request(Arc::new(move |_info| { + rc.fetch_add(1, Ordering::SeqCst); + })); + + // Passive on_response: capture the /api (Fetch) body. + let captured = Arc::new(Mutex::new(String::new())); + let cap = captured.clone(); + page.on_response(Arc::new(move |info, resp| { + if info.resource_type == ResourceType::Fetch { + *cap.lock().unwrap() = String::from_utf8_lossy(&resp.body).into_owned(); + } + })); + + // Channel-based interception: resolve every request Continue so the fetch + // is not blocked. + let mut rx = page.enable_interception(); + tokio::spawn(async move { + while let Some(req) = rx.recv().await { + if req + .resolver + .send(InterceptResolution::Continue { + url: None, + method: None, + headers: None, + body: None, + }) + .is_err() + { + break; + } + } + }); + + page.goto(&base).await.unwrap(); + // Pump the event loop so the inline fetch('/api') resolves. + for _ in 0..20 { + page.settle(500).await; + if captured.lock().unwrap().contains("hello") { + break; + } + } + + assert!( + req_count.load(Ordering::SeqCst) >= 1, + "on_request never fired for navigation or fetch" + ); + let body = captured.lock().unwrap().clone(); + assert!( + body.contains("hello"), + "on_response did not capture the fetch response body: {:?}", + body + ); +}