fix(net,js): route op_fetch_url + ES module loader through proxy_url (#139) (#151)

Fixes the remaining two paths from #139: JS-level fetch()/XHR
(op_fetch_url) and dynamic ES module imports (ObscuraModuleLoader) both
built their reqwest clients with `Client::builder().build()` — no
proxy — so every JS-initiated network call silently bypassed
BrowserContext.proxy_url. (The third path, StealthHttpClient, was
already fixed in main: page.rs:61 uses with_proxy.)

Changes:

* obscura-net: expose ObscuraHttpClient::proxy_url() so callers outside
  the crate can route their own reqwest requests through the same
  upstream proxy. The field was already there — just lacked an accessor.

* obscura-js/module_loader: add `proxy_url: Option<String>` and
  `with_proxy(base_url, proxy_url)`. ObscuraModuleLoader::new keeps the
  pre-#139 direct-connection behaviour. load() builds a reqwest client
  per import, threading the proxy and bubbling up "Invalid module proxy"
  on bad URLs.

* obscura-js/ops.rs: replace the process-wide
  `static SHARED_HTTP_CLIENT: OnceLock<reqwest::Client>` (which could
  never be reconfigured after first call) with a per-request
  `build_request_client(proxy_url)` that pulls the configured proxy out
  of SharedState.http_client.proxy_url() each call. Per-request build
  cost is ~1ms vs the network round-trip, and we no longer have a
  global singleton that lies about its config.

* obscura-js/runtime: add ObscuraJsRuntime::with_base_url_and_proxy.
  with_base_url delegates to it with None so existing tests / callers
  are unaffected.

* obscura-browser/page.rs: thread context.proxy_url through to
  with_base_url_and_proxy when constructing the JS runtime.
This commit is contained in:
Ben Younes authored and GitHub committed 2026-05-14 08:25:55 +00:00
1 parent a750797d15
commit f3c783f6f5
5 files changed
+139 -16

No files matched your search

+65 -1
View File
@@ -33,10 +33,17 @@ impl ObscuraJsRuntime {
}
pub fn with_base_url(base_url: &str) -> Self {
Self::with_base_url_and_proxy(base_url, None)
}
/// Construct a runtime whose ES-module loader routes dynamic imports
/// through `proxy_url` (#139). `None` is equivalent to `with_base_url`
/// (direct connection).
pub fn with_base_url_and_proxy(base_url: &str, proxy_url: Option<String>) -> Self {
let state = Rc::new(RefCell::new(ObscuraState::new()));
let state_clone = state.clone();
let module_loader = Rc::new(ObscuraModuleLoader::new(base_url));
let module_loader = Rc::new(ObscuraModuleLoader::with_proxy(base_url, proxy_url));
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![build_extension()],
@@ -1589,4 +1596,61 @@ mod tests {
assert_eq!(inf, serde_json::Value::Null);
}
// Issue #139 — proxy_url must thread through to both the ES-module
// loader (module_loader.rs) and op_fetch_url's reqwest client
// (ops.rs::build_request_client). Pre-fix both built clients with
// `Client::builder().build()` — no proxy — so JS fetch/XHR and
// dynamic imports silently bypassed BrowserContext.proxy_url.
//
// Phase 5.5 RED check: each test references a symbol that does NOT
// exist on main (proxy_url() accessor, with_proxy ctor,
// with_base_url_and_proxy ctor), so the tests fail to compile without
// the prod fix.
#[test]
fn http_client_round_trips_proxy_url() {
use obscura_net::{CookieJar, ObscuraHttpClient};
let jar = std::sync::Arc::new(CookieJar::new());
let configured =
ObscuraHttpClient::with_options(jar.clone(), Some("http://proxy.test:8080"));
assert_eq!(
configured.proxy_url(),
Some("http://proxy.test:8080"),
"proxy_url() must expose the value passed to with_options"
);
let direct = ObscuraHttpClient::with_options(jar, None);
assert_eq!(
direct.proxy_url(),
None,
"proxy_url() must return None when no proxy was configured"
);
}
#[test]
fn module_loader_stores_proxy_for_dynamic_imports() {
use crate::module_loader::ObscuraModuleLoader;
let loader = ObscuraModuleLoader::with_proxy(
"https://example.com/",
Some("http://proxy.test:8080".to_string()),
);
assert_eq!(loader.proxy_url.as_deref(), Some("http://proxy.test:8080"));
assert_eq!(loader.base_url, "https://example.com/");
// Default constructor must keep the historical "no proxy" behaviour.
let direct = ObscuraModuleLoader::new("https://example.com/");
assert_eq!(direct.proxy_url, None);
}
#[test]
fn runtime_with_base_url_and_proxy_constructs_successfully() {
// Sanity-check the public ctor that page.rs uses to thread proxy
// through to the module loader. Direct (None) and proxied paths
// must both initialise the JS environment.
let _direct = ObscuraJsRuntime::with_base_url_and_proxy("https://example.com/", None);
let _proxied = ObscuraJsRuntime::with_base_url_and_proxy(
"https://example.com/",
Some("http://proxy.test:8080".to_string()),
);
}
}