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

+28 -12
View File
@@ -1,7 +1,7 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::{Arc, OnceLock};
use std::sync::Arc;
use deno_core::op2;
use deno_core::OpState;
@@ -281,14 +281,25 @@ fn op_console_msg(state: &OpState, #[string] level: &str, #[string] msg: &str) {
}
}
static SHARED_HTTP_CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
fn get_shared_client() -> &'static reqwest::Client {
SHARED_HTTP_CLIENT.get_or_init(|| {
reqwest::Client::builder()
.build()
.expect("failed to build shared reqwest::Client")
})
// op_fetch_url backs JS-level `fetch()` and XHR. Pre-#139 it used a
// process-wide `OnceLock<reqwest::Client>` initialised with no proxy, so
// every JS network call bypassed the configured upstream proxy. We now
// build a client per request, threading whatever `proxy_url` the page's
// ObscuraHttpClient was configured with.
//
// The per-request build cost is negligible (≪1ms) compared with the actual
// network round-trip; the simplification is worth not having to invalidate
// a cache when the proxy is reconfigured between fetches.
fn build_request_client(proxy_url: Option<&str>) -> Result<reqwest::Client, String> {
let mut builder = reqwest::Client::builder();
if let Some(proxy) = proxy_url {
let p = reqwest::Proxy::all(proxy)
.map_err(|e| format!("Invalid op_fetch_url proxy '{}': {}", proxy, e))?;
builder = builder.proxy(p);
}
builder
.build()
.map_err(|e| format!("failed to build reqwest::Client: {}", e))
}
#[op2(async)]
@@ -317,7 +328,7 @@ async fn op_fetch_url(
}
}
let (cookie_jar, in_flight, intercept_tx) = {
let (cookie_jar, in_flight, intercept_tx, proxy_url) = {
let state_borrow = state.borrow();
let gs = state_borrow.borrow::<SharedState>().clone();
let mut gs = gs.borrow_mut();
@@ -334,6 +345,10 @@ async fn op_fetch_url(
}
let jar = gs.cookie_jar.clone();
let in_flight = gs.http_client.as_ref().map(|c| c.in_flight.clone());
// #139: thread the configured proxy through to the per-request
// reqwest::Client. Without this, op_fetch_url silently bypasses
// BrowserContext.proxy_url for every JS fetch() / XHR call.
let proxy_url = gs.http_client.as_ref().and_then(|c| c.proxy_url().map(|s| s.to_string()));
tracing::debug!("op_fetch_url: intercept_enabled={}, has_tx={}", gs.intercept_enabled, gs.intercept_tx.is_some());
let itx = if gs.intercept_enabled {
gs.intercept_counter += 1;
@@ -341,7 +356,7 @@ async fn op_fetch_url(
} else {
None
};
(jar, in_flight, itx)
(jar, in_flight, itx, proxy_url)
};
if let Some((tx, request_id)) = intercept_tx {
@@ -385,7 +400,8 @@ async fn op_fetch_url(
}
}
let client = get_shared_client();
let client = build_request_client(proxy_url.as_deref())
.map_err(deno_error::JsErrorBox::generic)?;
let request_origin = url::Url::parse(&url)
.ok()