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 <script> (CDP
  Page.addScriptToEvaluateOnNewDocument). The field already ran the scripts;
  this only adds the method.
- enable_interception: returns a receiver for every JS fetch()/XHR request;
  resolve each through its resolver with InterceptResolution::{Continue,
  Fulfill, Fail} to pass, mock, or block (CDP Fetch.enable equivalent).
- on_request / on_response: passive callbacks. on_response is the main path for
  capturing API response payloads from SPAs.

on_request/on_response previously fired only for navigation; op_fetch_url now
invokes them for JS fetch()/XHR too (both reqwest and stealth paths), gated on
a non-empty callback vector so the common no-callback case pays nothing.

Re-export InterceptedRequest, InterceptResolution, RequestInfo, ResourceType,
RequestCallback, ResponseCallback, Response from the obscura crate.

Integration test tests/interception.rs exercises all three against a local echo
server.
This commit is contained in:
SGavrl committed 2026-06-24 21:36:01 +00:00
1 parent 12b927f476
commit 1f5066fc30
8 files changed
+261 -7

No files matched your search

+41 -1
View File
@@ -3,7 +3,7 @@ use std::sync::Arc;
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
use obscura_dom::{parse_html, DomTree};
use obscura_js::runtime::ObscuraJsRuntime;
use obscura_net::{ObscuraHttpClient, ObscuraNetError, Response};
use obscura_net::{ObscuraHttpClient, ObscuraNetError, RequestCallback, Response, ResponseCallback};
use url::Url;
use crate::context::BrowserContext;
@@ -1526,6 +1526,46 @@ impl Page {
self.preload_scripts = scripts;
}
/// Append a script that runs in the page before any of the page's own
/// `<script>` tags, matching CDP `Page.addScriptToEvaluateOnNewDocument`.
/// Takes effect on the next navigation (`goto` / `navigate*`).
pub fn add_preload_script(&mut self, script: &str) {
self.preload_scripts.push(script.to_string());
}
/// Enable CDP-Fetch-style interception of JS-initiated `fetch()`/XHR.
/// Returns a receiver yielding every such request; resolve each through its
/// `resolver` with `InterceptResolution::{Continue, Fulfill, Fail}` to pass,
/// mock, or block it. Works in stealth and non-stealth. Mirrors how the CDP
/// server wires the channel (`obscura-cdp/src/server.rs`).
pub fn enable_interception(
&mut self,
) -> tokio::sync::mpsc::UnboundedReceiver<obscura_js::ops::InterceptedRequest> {
let (tx, rx) =
tokio::sync::mpsc::unbounded_channel::<obscura_js::ops::InterceptedRequest>();
self.set_intercept_tx(tx);
self.enable_intercept(true);
rx
}
/// 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 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);
}
}
pub async fn process_pending_navigation(&mut self) -> Result<bool, PageError> {
if let Some((url, method, body)) = self.take_pending_navigation() {
self.navigate_with_wait_post(