use ::std::cell::RefCell;
use ::std::collections::HashMap;
use ::std::sync::atomic::{AtomicBool, AtomicU32, Ordering as AtomicOrdering};
use ::std::sync::{Arc, Mutex};
use bao_engine::context::RawValueRootGuard;
use bun_core::ZBox;
use mozjs::jsapi::*;
use mozjs::jsval::{JSVal, ObjectValue, StringValue, UndefinedValue};
use mozjs::realm::AutoRealm;
use mozjs::rooted;
use crate::stealth_http::{StealthSyncResult, stealth_http_request};
type FetchOutcome = ::std::result::Result<StealthSyncResult, String>;
#[derive(Clone, Copy)]
pub enum ResolveKind {
Response,
TlsSocket { host_idx: usize },
}
thread_local! {
static HOST_STRINGS: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
}
static NEXT_ABORT_ID: AtomicU32 = AtomicU32::new(1);
pub struct AbortRequest {
pub id: u32,
pub flag: Arc<AtomicBool>,
}
#[derive(Clone)]
struct AbortEntry {
flag: Arc<AtomicBool>,
async_http_id: u32,
}
#[derive(Default)]
pub struct FetchTlsInit {
pub ca_certs_der: Box<[Box<[u8]>]>,
pub reject_unauthorized: Option<bool>,
pub servername: Option<String>,
}
thread_local! {
static ABORT_REGISTRY: RefCell<HashMap<u32, AbortEntry>> = RefCell::new(HashMap::new());
}
pub fn new_abort_id() -> u32 {
NEXT_ABORT_ID.fetch_add(1, AtomicOrdering::Relaxed)
}
pub struct PendingFetch {
pub cx: *mut JSContext,
pub promise_root: Option<RawValueRootGuard>,
pub promise_val: JSVal,
pub outcome: Arc<Mutex<Option<FetchOutcome>>>,
pub kind: ResolveKind,
mini_loop_ptr: *const bun_event_loop::MiniEventLoop::MiniEventLoop<'static>,
concurrent_task: bun_event_loop::AnyTaskWithExtraContext::AnyTaskWithExtraContext,
has_schedule_callback: AtomicBool,
url_owned: Option<*mut [u8]>,
body_owned: Option<*mut [u8]>,
headers_owned: Option<*mut [u8]>,
abort_flag: Option<Arc<AtomicBool>>,
abort_id: Option<u32>,
}
unsafe impl Send for PendingFetch {}
thread_local! {
static PENDING: RefCell<Vec<*mut PendingFetch>> = const { RefCell::new(Vec::new()) };
}
pub fn has_pending() -> bool {
PENDING.with(|p| !p.borrow().is_empty())
}
pub unsafe fn start(
cx: *mut JSContext,
promise_val: JSVal,
profile: Option<crate::stealth_http::StealthProfile>,
method: bun_http::Method,
url: String,
headers: Vec<(String, String)>,
body: Option<Vec<u8>>,
) {
unsafe {
start_with_kind(
cx,
promise_val,
profile,
method,
url,
headers,
body,
ResolveKind::Response,
None,
None,
)
}
}
pub unsafe fn start_with_signal(
cx: *mut JSContext,
promise_val: JSVal,
profile: Option<crate::stealth_http::StealthProfile>,
method: bun_http::Method,
url: String,
headers: Vec<(String, String)>,
body: Option<Vec<u8>>,
abort: AbortRequest,
) {
unsafe {
start_with_kind(
cx,
promise_val,
profile,
method,
url,
headers,
body,
ResolveKind::Response,
Some(abort),
None,
)
}
}
pub unsafe fn start_fetch(
cx: *mut JSContext,
promise_val: JSVal,
profile: Option<crate::stealth_http::StealthProfile>,
method: bun_http::Method,
url: String,
headers: Vec<(String, String)>,
body: Option<Vec<u8>>,
abort: Option<AbortRequest>,
tls: Option<FetchTlsInit>,
) {
unsafe {
start_with_kind(
cx,
promise_val,
profile,
method,
url,
headers,
body,
ResolveKind::Response,
abort,
tls,
)
}
}
pub unsafe fn start_tls_probe(cx: *mut JSContext, promise_val: JSVal, host: String, port: u16) {
let test_url = format!("https://{}:{}", host, port);
let host_idx = HOST_STRINGS.with(|h| {
let mut g = h.borrow_mut();
let idx = g.len();
g.push(host);
idx
});
unsafe {
start_with_kind(
cx,
promise_val,
None,
bun_http::Method::HEAD,
test_url,
Vec::new(),
None,
ResolveKind::TlsSocket { host_idx },
None,
None,
)
}
}
unsafe fn start_with_kind(
cx: *mut JSContext,
promise_val: JSVal,
profile: Option<crate::stealth_http::StealthProfile>,
method: bun_http::Method,
url: String,
headers: Vec<(String, String)>,
body: Option<Vec<u8>>,
kind: ResolveKind,
abort: Option<AbortRequest>,
tls: Option<FetchTlsInit>,
) {
let promise_root = unsafe {
RawValueRootGuard::new(
cx,
::std::slice::from_ref(&promise_val),
c"FetchTasklet.promise",
)
};
let rooted_val = promise_root.as_ref().map_or(promise_val, |g| g.get(0));
let outcome: Arc<Mutex<Option<FetchOutcome>>> = Arc::new(Mutex::new(None));
let pending = Box::new(PendingFetch {
cx,
promise_root,
promise_val: rooted_val,
outcome: Arc::clone(&outcome),
kind,
mini_loop_ptr: ::std::ptr::null(), concurrent_task: bun_event_loop::AnyTaskWithExtraContext::AnyTaskWithExtraContext::default(
),
has_schedule_callback: AtomicBool::new(false),
url_owned: None, body_owned: None, headers_owned: None, abort_flag: abort.as_ref().map(|req| Arc::clone(&req.flag)),
abort_id: abort.as_ref().map(|req| req.id),
});
let pending_ptr = Box::into_raw(pending);
let _on_done_outcome = Arc::clone(&outcome);
let callback = bun_http::HTTPClientResultCallback::new(pending_ptr, on_http_done);
let url_static: &'static [u8] = Box::leak(url.into_bytes().into_boxed_slice());
let parsed_url = bun_url::URL::parse(url_static);
unsafe {
(*pending_ptr).url_owned = Some(url_static as *const [u8] as *mut [u8]);
}
let mut hb = bun_http::HeaderBuilder::default();
for (name, value) in &headers {
hb.count(name.as_bytes(), value.as_bytes());
}
if hb.allocate().is_err() {
unsafe {
if let Some(url_ptr) = (*pending_ptr).url_owned.take() {
drop(Box::from_raw(url_ptr));
}
}
let mut outcome_guard = outcome.lock().unwrap();
*outcome_guard = Some(Err("fetch: header allocation failed".into()));
drop(outcome_guard);
schedule_resolve_on_js_thread(pending_ptr);
return;
}
for (name, value) in &headers {
hb.append(name.as_bytes(), value.as_bytes());
}
let content_len = hb.content.len;
let headers_cap: Box<[u8]> = hb.content.move_to_slice();
let headers_owned_ptr: *mut [u8] = Box::into_raw(headers_cap);
let headers_buf_static: &'static [u8] = if content_len > 0 {
unsafe { ::std::slice::from_raw_parts((*headers_owned_ptr).as_ptr(), content_len) }
} else {
unsafe {
drop(Box::from_raw(headers_owned_ptr));
}
&[]
};
unsafe {
if content_len > 0 {
(*pending_ptr).headers_owned = Some(headers_owned_ptr);
}
}
let entry_list = hb.entries;
let response_buffer = Box::into_raw(Box::new(bun_core::string::MutableString::default()));
let body_slice: &'static [u8] = match body {
Some(b) if !b.is_empty() => {
let bs: &'static [u8] = Box::leak(b.into_boxed_slice());
unsafe {
(*pending_ptr).body_owned = Some(bs as *const [u8] as *mut [u8]);
}
bs
}
_ => &[],
};
let tls_props = {
let mut ssl_config = crate::stealth_http::stealth_profile_to_ssl_config(&profile);
if let Some(tls) = &tls {
if !tls.ca_certs_der.is_empty() {
ssl_config.ca_certs_der = Some(tls.ca_certs_der.clone());
}
if let Some(sn) = &tls.servername {
if !ssl_config.server_name.is_null() {
unsafe { bun_core::free_sensitive(ssl_config.server_name) };
}
ssl_config.server_name = bun_core::dupe_z(sn.as_bytes());
}
}
Some(bun_http::ssl_config::GlobalRegistry::intern(ssl_config))
};
let signals = abort.as_ref().map(|req| bun_http::Signals {
aborted: Some(unsafe {
core::ptr::NonNull::new_unchecked(Arc::as_ptr(&req.flag).cast_mut())
}),
..Default::default()
});
let options = bun_http::async_http::Options {
tls_props,
signals,
reject_unauthorized: tls.as_ref().and_then(|t| t.reject_unauthorized),
..Default::default()
};
let async_http_box: *mut bun_http::AsyncHTTP<'static> =
bun_core::heap::into_raw(Box::new(bun_http::AsyncHTTP::init(
method,
parsed_url,
entry_list,
headers_buf_static,
response_buffer,
body_slice,
callback,
bun_http::FetchRedirect::Follow,
options,
)));
if let Some(req) = &abort {
let async_http_id = unsafe { (*async_http_box).async_http_id };
ABORT_REGISTRY.with(|r| {
r.borrow_mut().insert(
req.id,
AbortEntry {
flag: Arc::clone(&req.flag),
async_http_id,
},
);
});
}
let loop_ptr: *const bun_event_loop::MiniEventLoop::MiniEventLoop<'static> =
crate::timers::with_event_loop(|loop_| loop_ as *const _);
unsafe {
(*pending_ptr).mini_loop_ptr = loop_ptr;
let _field_offset = ::std::mem::offset_of!(PendingFetch, concurrent_task);
(*pending_ptr)
.concurrent_task
.from(pending_ptr, resolve_tasklet_shim);
}
bun_http::http_thread::init(&Default::default());
let batch = bun_threading::thread_pool::Batch::from(unsafe {
core::ptr::addr_of_mut!((*async_http_box).task)
});
bun_http::HTTPThread::schedule(batch);
{
let ctx = crate::timers::with_event_loop(|loop_| {
bun_event_loop::MiniEventLoop::MiniEventLoop::as_event_loop_ctx(loop_)
});
if ctx.is_js() {
ctx.ref_concurrently();
}
}
crate::node_http::register_liveness_probe(has_pending);
PENDING.with(|p| {
p.borrow_mut().push(pending_ptr);
});
}
pub fn trigger_abort(abort_id: u32) {
let entry = ABORT_REGISTRY.with(|r| r.borrow().get(&abort_id).cloned());
let Some(entry) = entry else {
return;
};
entry.flag.store(true, AtomicOrdering::Release);
schedule_abort_shutdown(entry.async_http_id);
}
fn schedule_abort_shutdown(async_http_id: u32) {
bun_http::http_thread::init(&Default::default());
let ht = unsafe { (*bun_http::HTTP_THREAD.get_unchecked()).as_mut_ptr() };
unsafe {
{
let _guard = (*ht).queued_shutdowns_lock.lock_guard();
(*ht)
.queued_shutdowns
.push(bun_http::http_thread::ShutdownMessage { async_http_id });
}
(*ht).wakeup();
}
}
fn on_http_done(
this: *mut PendingFetch,
async_http_box: *mut bun_http::AsyncHTTP<'static>,
result: bun_http::HTTPClientResult<'_>,
) {
let result_is_terminal = !result.has_more;
if !result_is_terminal {
return;
}
let outcome: FetchOutcome = if let Some(fail) = result.fail {
Err(format!("{:?}", fail))
} else {
let status_code = result
.metadata
.as_ref()
.map(|m| m.response.status_code)
.unwrap_or(0);
let status_text: compact_str::CompactString = result
.metadata
.as_ref()
.map(|m| {
::std::str::from_utf8(m.response.status)
.unwrap_or("")
.into()
})
.unwrap_or_default();
let headers: smallvec::SmallVec<
[(compact_str::CompactString, compact_str::CompactString); 8],
> = result
.metadata
.as_ref()
.map(|m| {
m.response
.headers
.list
.iter()
.map(|h| {
(
compact_str::CompactString::from(
::std::str::from_utf8(h.name()).unwrap_or(""),
),
compact_str::CompactString::from(
::std::str::from_utf8(h.value()).unwrap_or(""),
),
)
})
.collect()
})
.unwrap_or_default();
let body_bytes: bytes::Bytes = result
.body
.map(|ms| bytes::Bytes::copy_from_slice(ms.list.as_slice()))
.unwrap_or_default();
Ok(StealthSyncResult {
status_code,
status_text,
headers,
body: body_bytes,
})
};
if let Ok(mut guard) = unsafe { &*this }.outcome.lock() {
*guard = Some(outcome);
}
if unsafe { &*this }
.has_schedule_callback
.compare_exchange(false, true, AtomicOrdering::AcqRel, AtomicOrdering::Acquire)
.is_ok()
{
let loop_ptr = unsafe { &*this }.mini_loop_ptr;
if !loop_ptr.is_null() {
let concurrent_task_ptr = unsafe { core::ptr::addr_of_mut!((*this).concurrent_task) };
let _ = unsafe {
bun_event_loop::ConcurrentWakeup::enqueue_task_concurrent_cross_thread(
loop_ptr as *mut bun_event_loop::MiniEventLoop::MiniEventLoop<'static>,
core::ptr::NonNull::new_unchecked(concurrent_task_ptr),
)
};
}
}
if !async_http_box.is_null() && result_is_terminal {
let real = unsafe { (*async_http_box).real.take() };
if let Some(real_ptr) = real {
let js_box_ptr = real_ptr.as_ptr();
let resp_buf = unsafe { (*js_box_ptr).response_buffer };
if !resp_buf.is_null() {
drop(unsafe { Box::from_raw(resp_buf) });
}
drop(unsafe { Box::from_raw(js_box_ptr) });
}
}
}
fn resolve_tasklet_shim(ctx: *mut PendingFetch, _parent: *mut ()) {
unsafe { resolve_tasklet(ctx) };
}
unsafe fn resolve_tasklet(this: *mut PendingFetch) {
unsafe { &*this }
.has_schedule_callback
.store(false, AtomicOrdering::Release);
let outcome = unsafe { &*this }
.outcome
.lock()
.ok()
.and_then(|mut slot| slot.take())
.unwrap_or_else(|| Err("fetch: result slot was empty".into()));
let aborted = unsafe { &*this }
.abort_flag
.as_ref()
.is_some_and(|f| f.load(AtomicOrdering::Acquire));
let cx = unsafe { &*this }.cx;
let kind = unsafe { &*this }.kind;
let pending = unsafe { &*this };
let promise_val = pending
.promise_root
.as_ref()
.map_or(pending.promise_val, |g| g.get(0));
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let promise_obj = promise_val.to_object());
let promise_h = promise_obj.handle().into();
{
let mut realm = AutoRealm::new_from_handle(cx_ref, promise_obj.handle());
let realm_cx: &mut mozjs::context::JSContext = &mut realm;
if aborted {
reject_promise_with_abort_error(cx, promise_h);
} else {
match (outcome, kind) {
(Ok(resp), ResolveKind::Response) => {
let resp_obj = build_response_js(cx, &resp);
if !resp_obj.is_null() {
rooted!(&in(realm_cx) let resp_val = ObjectValue(resp_obj));
JS::ResolvePromise(cx, promise_h, resp_val.handle().into());
} else {
reject_with_message(cx, promise_h, "http: failed to build Response");
}
}
(Ok(_resp), ResolveKind::TlsSocket { host_idx }) => {
let host = HOST_STRINGS
.with(|h| h.borrow().get(host_idx).cloned())
.unwrap_or_default();
let tls_obj = build_tls_socket_js(cx, &host);
if !tls_obj.is_null() {
rooted!(&in(realm_cx) let tls_val = ObjectValue(tls_obj));
JS::ResolvePromise(cx, promise_h, tls_val.handle().into());
} else {
reject_with_message(cx, promise_h, "tls: failed to build socket object");
}
HOST_STRINGS.with(|h| {
if host_idx < h.borrow().len() {
h.borrow_mut()[host_idx].clear();
}
});
}
(Err(msg), _) => {
reject_with_network_error(cx, promise_h, &msg);
}
}
}
}
{
let ctx = crate::timers::with_event_loop(|loop_| {
bun_event_loop::MiniEventLoop::MiniEventLoop::as_event_loop_ctx(loop_)
});
if ctx.is_js() {
ctx.unref_concurrently();
}
}
PENDING.with(|p| {
let mut guard = p.borrow_mut();
if let Some(pos) = guard.iter().position(|&ptr| ptr == this) {
guard.swap_remove(pos);
}
});
if let Some(abort_id) = unsafe { (*this).abort_id } {
ABORT_REGISTRY.with(|r| {
r.borrow_mut().remove(&abort_id);
});
}
unsafe {
if let Some(url_ptr) = (*this).url_owned.take() {
drop(Box::from_raw(url_ptr));
}
if let Some(body_ptr) = (*this).body_owned.take() {
drop(Box::from_raw(body_ptr));
}
if let Some(headers_ptr) = (*this).headers_owned.take() {
drop(Box::from_raw(headers_ptr));
}
}
unsafe {
drop(Box::from_raw(this));
}
mozjs_sys::jsapi::js::RunJobs(cx);
}
fn schedule_resolve_on_js_thread(pending_ptr: *mut PendingFetch) {
if unsafe { &*pending_ptr }
.has_schedule_callback
.compare_exchange(false, true, AtomicOrdering::AcqRel, AtomicOrdering::Acquire)
.is_ok()
{
let loop_ptr = unsafe { &*pending_ptr }.mini_loop_ptr;
if !loop_ptr.is_null() {
let loop_ref = unsafe {
&mut *(loop_ptr as *mut bun_event_loop::MiniEventLoop::MiniEventLoop<'static>)
};
let concurrent_task_ptr =
unsafe { core::ptr::addr_of_mut!((*pending_ptr).concurrent_task) };
loop_ref.enqueue_task_concurrent(unsafe {
core::ptr::NonNull::new_unchecked(concurrent_task_ptr)
});
}
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn build_tls_socket_js(cx: *mut JSContext, host: &str) -> *mut JSObject {
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj = JS_NewPlainObject(cx));
if obj.is_null() {
return obj.get();
}
let obj_handle = obj.handle().into();
rooted!(&in(cx_ref) let auth_val = mozjs::jsval::BooleanValue(true));
JS_DefineProperty(
cx,
obj_handle,
c"authorized".as_ptr(),
auth_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
rooted!(&in(cx_ref) let enc_val = mozjs::jsval::BooleanValue(true));
JS_DefineProperty(
cx,
obj_handle,
c"encrypted".as_ptr(),
enc_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
if !host.is_empty() {
let c_host = ZBox::from_bytes(host.as_bytes());
let host_js = JS_NewStringCopyZ(cx, c_host.as_ptr());
if !host_js.is_null() {
rooted!(&in(cx_ref) let hv = StringValue(&*host_js));
JS_DefineProperty(
cx,
obj_handle,
c"servername".as_ptr(),
hv.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
obj.get()
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn build_response_js(cx: *mut JSContext, resp: &StealthSyncResult) -> *mut JSObject {
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
let global = JS::CurrentGlobalOrNull(cx);
if global.is_null() {
return ::std::ptr::null_mut();
}
rooted!(&in(cx_ref) let global_root = global);
let mut resp_ctor_val = UndefinedValue();
JS_GetProperty(
cx,
global_root.handle().into(),
c"Response".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut resp_ctor_val,
},
);
if !resp_ctor_val.is_object() {
return ::std::ptr::null_mut();
}
rooted!(&in(cx_ref) let resp_ctor = resp_ctor_val.to_object());
rooted!(&in(cx_ref) let resp_fn = ObjectValue(resp_ctor.get()));
rooted!(&in(cx_ref) let body_arr = JS_NewUint8Array(cx, resp.body.len()));
if body_arr.is_null() {
return ::std::ptr::null_mut();
}
if !resp.body.is_empty() {
let mut ta_len: usize = 0;
let mut shared = false;
let mut data: *mut u8 = ::std::ptr::null_mut();
let unwrapped =
JS_GetObjectAsUint8Array(body_arr.get(), &mut ta_len, &mut shared, &mut data);
if unwrapped.is_null() || data.is_null() || ta_len < resp.body.len() {
return ::std::ptr::null_mut();
}
::std::ptr::copy_nonoverlapping(resp.body.as_ptr(), data, resp.body.len());
}
rooted!(&in(cx_ref) let init_obj = JS_NewPlainObject(cx));
if init_obj.is_null() {
return ::std::ptr::null_mut();
}
let init_h = init_obj.handle().into();
rooted!(&in(cx_ref) let status_val = mozjs::jsval::Int32Value(resp.status_code as i32));
JS_DefineProperty(
cx,
init_h,
c"status".as_ptr(),
status_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
let c_st = ZBox::from_bytes(resp.status_text.as_bytes());
let st_js = JS_NewStringCopyZ(cx, c_st.as_ptr());
if !st_js.is_null() {
rooted!(&in(cx_ref) let st_val = StringValue(&*st_js));
JS_DefineProperty(
cx,
init_h,
c"statusText".as_ptr(),
st_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
rooted!(&in(cx_ref) let headers_arr = mozjs_sys::jsapi::JS::NewArrayObject1(cx, resp.headers.len()));
if headers_arr.is_null() {
return ::std::ptr::null_mut();
}
let hdrs_h = headers_arr.handle().into();
for (i, (k, v)) in resp.headers.iter().enumerate() {
let pair_len = 2u32;
rooted!(&in(cx_ref) let pair = mozjs_sys::jsapi::JS::NewArrayObject1(cx, pair_len as usize));
if pair.is_null() {
continue;
}
let pair_h = pair.handle().into();
let c_k = ZBox::from_bytes(k.as_bytes());
let k_js = JS_NewStringCopyZ(cx, c_k.as_ptr());
if !k_js.is_null() {
rooted!(&in(cx_ref) let kv = StringValue(&*k_js));
JS_DefineElement(
cx,
pair_h,
0,
kv.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
let c_v = ZBox::from_bytes(v.as_bytes());
let v_js = JS_NewStringCopyZ(cx, c_v.as_ptr());
if !v_js.is_null() {
rooted!(&in(cx_ref) let vv = StringValue(&*v_js));
JS_DefineElement(
cx,
pair_h,
1,
vv.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
rooted!(&in(cx_ref) let pv = ObjectValue(pair.get()));
JS_DefineElement(
cx,
hdrs_h,
i as u32,
pv.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
rooted!(&in(cx_ref) let hv = ObjectValue(headers_arr.get()));
JS_DefineProperty(
cx,
init_h,
c"headers".as_ptr(),
hv.handle().into(),
JSPROP_ENUMERATE as u32,
);
let elems = [
ObjectValue(body_arr.get()),
ObjectValue(init_obj.get()),
];
let call_args = HandleValueArray {
length_: 2,
elements_: elems.as_ptr(),
};
rooted!(&in(cx_ref) let undef_this = ::std::ptr::null_mut::<JSObject>());
let mut resp_val = UndefinedValue();
let called = JS_CallFunctionValue(
cx,
undef_this.handle().into(),
resp_fn.handle().into(),
&call_args,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut resp_val,
},
);
if !called || !resp_val.is_object() {
return ::std::ptr::null_mut();
}
rooted!(&in(cx_ref) let resp_root = resp_val);
resp_root.get().to_object()
}
#[allow(unsafe_op_in_unsafe_fn)]
pub unsafe fn reject_promise_with_abort_error(
cx: *mut JSContext,
promise_h: Handle<*mut JSObject>,
) {
unsafe {
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let err_obj = build_abort_error_js(cx));
if !err_obj.is_null() {
rooted!(&in(cx_ref) let ev = ObjectValue(err_obj.get()));
JS::RejectPromise(cx, promise_h, ev.handle().into());
return;
}
rooted!(&in(cx_ref) let obj = JS_NewPlainObject(cx));
if obj.is_null() {
reject_with_message(cx, promise_h, "The operation was aborted");
return;
}
let c_msg = ZBox::from_bytes(b"The operation was aborted");
let msg_js = JS_NewStringCopyZ(cx, c_msg.as_ptr());
if !msg_js.is_null() {
rooted!(&in(cx_ref) let msg_val = StringValue(&*msg_js));
JS_DefineProperty(
cx,
obj.handle().into(),
c"message".as_ptr(),
msg_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
let c_name = ZBox::from_bytes(b"AbortError");
let name_js = JS_NewStringCopyZ(cx, c_name.as_ptr());
if !name_js.is_null() {
rooted!(&in(cx_ref) let name_val = StringValue(&*name_js));
JS_DefineProperty(
cx,
obj.handle().into(),
c"name".as_ptr(),
name_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
rooted!(&in(cx_ref) let ev = ObjectValue(obj.get()));
JS::RejectPromise(cx, promise_h, ev.handle().into());
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn build_abort_error_js(cx: *mut JSContext) -> *mut JSObject {
unsafe {
let global = mozjs_sys::jsapi::JS::CurrentGlobalOrNull(cx);
if global.is_null() {
return ::std::ptr::null_mut();
}
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let global_rooted = global);
let mut ctor_val = UndefinedValue();
JS_GetProperty(
cx,
global_rooted.handle().into(),
c"DOMException".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut ctor_val,
},
);
if !ctor_val.is_object() {
return ::std::ptr::null_mut();
}
rooted!(&in(cx_ref) let ctor = ctor_val);
let c_msg = ZBox::from_bytes(b"The operation was aborted");
let msg_js = JS_NewStringCopyZ(cx, c_msg.as_ptr());
if msg_js.is_null() {
return ::std::ptr::null_mut();
}
rooted!(&in(cx_ref) let msg_val = StringValue(&*msg_js));
let c_name = ZBox::from_bytes(b"AbortError");
let name_js = JS_NewStringCopyZ(cx, c_name.as_ptr());
if name_js.is_null() {
return ::std::ptr::null_mut();
}
rooted!(&in(cx_ref) let name_val = StringValue(&*name_js));
let args = [msg_val.get(), name_val.get()];
let call_args = HandleValueArray {
length_: args.len(),
elements_: args.as_ptr(),
};
let mut err_obj: *mut JSObject = ::std::ptr::null_mut();
if !mozjs_sys::jsapi::JS::Construct1(
cx,
ctor.handle().into(),
&call_args,
MutableHandle::<*mut JSObject> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut err_obj,
},
) {
return ::std::ptr::null_mut();
}
err_obj
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn reject_with_network_error(
cx: *mut JSContext,
promise_h: Handle<*mut JSObject>,
fail_msg: &str,
) {
unsafe {
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let cause_obj = JS_NewPlainObject(cx));
if !cause_obj.is_null() {
let cause_h = cause_obj.handle().into();
let (code, text) = network_error_code_and_text(fail_msg);
let c_code = ZBox::from_bytes(code.as_bytes());
let code_js = JS_NewStringCopyZ(cx, c_code.as_ptr());
if !code_js.is_null() {
rooted!(&in(cx_ref) let cv = StringValue(&*code_js));
JS_DefineProperty(cx, cause_h, c"code".as_ptr(), cv.handle().into(), JSPROP_ENUMERATE as u32);
}
let c_msg = ZBox::from_bytes(text.as_bytes());
let msg_js = JS_NewStringCopyZ(cx, c_msg.as_ptr());
if !msg_js.is_null() {
rooted!(&in(cx_ref) let mv = StringValue(&*msg_js));
JS_DefineProperty(cx, cause_h, c"message".as_ptr(), mv.handle().into(), JSPROP_ENUMERATE as u32);
}
}
let global = JS::CurrentGlobalOrNull(cx);
let mut err_val = UndefinedValue();
let mut built = false;
if !global.is_null() && !cause_obj.is_null() {
rooted!(&in(cx_ref) let global_root = global);
let mut te_val = UndefinedValue();
JS_GetProperty(
cx,
global_root.handle().into(),
c"TypeError".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut te_val,
},
);
if te_val.is_object() {
rooted!(&in(cx_ref) let te_obj = te_val.to_object());
rooted!(&in(cx_ref) let te_fn = ObjectValue(te_obj.get()));
let c_msg = ZBox::from_bytes(b"fetch failed");
let msg_js = JS_NewStringCopyZ(cx, c_msg.as_ptr());
if !msg_js.is_null() {
rooted!(&in(cx_ref) let msg_root = StringValue(&*msg_js));
let elems = [msg_root.get()];
let call_args = HandleValueArray {
length_: 1,
elements_: elems.as_ptr(),
};
rooted!(&in(cx_ref) let undef_this = ::std::ptr::null_mut::<JSObject>());
let called = JS_CallFunctionValue(
cx,
undef_this.handle().into(),
te_fn.handle().into(),
&call_args,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut err_val,
},
);
if called && err_val.is_object() {
rooted!(&in(cx_ref) let err_obj = err_val.to_object());
rooted!(&in(cx_ref) let cause_val = ObjectValue(cause_obj.get()));
JS_DefineProperty(
cx,
err_obj.handle().into(),
c"cause".as_ptr(),
cause_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
built = true;
}
}
}
}
if built {
rooted!(&in(cx_ref) let err_root = err_val);
JS::RejectPromise(cx, promise_h, err_root.handle().into());
return;
}
rooted!(&in(cx_ref) let obj = JS_NewPlainObject(cx));
if obj.is_null() {
reject_with_message(cx, promise_h, fail_msg);
return;
}
let obj_h = obj.handle().into();
let c_name = ZBox::from_bytes(b"TypeError");
let name_js = JS_NewStringCopyZ(cx, c_name.as_ptr());
if !name_js.is_null() {
rooted!(&in(cx_ref) let nv = StringValue(&*name_js));
JS_DefineProperty(cx, obj_h, c"name".as_ptr(), nv.handle().into(), JSPROP_ENUMERATE as u32);
}
let c_msg = ZBox::from_bytes(b"fetch failed");
let msg_js = JS_NewStringCopyZ(cx, c_msg.as_ptr());
if !msg_js.is_null() {
rooted!(&in(cx_ref) let mv = StringValue(&*msg_js));
JS_DefineProperty(cx, obj_h, c"message".as_ptr(), mv.handle().into(), JSPROP_ENUMERATE as u32);
}
if !cause_obj.is_null() {
rooted!(&in(cx_ref) let cause_val = ObjectValue(cause_obj.get()));
JS_DefineProperty(cx, obj_h, c"cause".as_ptr(), cause_val.handle().into(), JSPROP_ENUMERATE as u32);
}
rooted!(&in(cx_ref) let ev = ObjectValue(obj.get()));
JS::RejectPromise(cx, promise_h, ev.handle().into());
}
}
fn network_error_code_and_text(fail_msg: &str) -> (&'static str, String) {
let kind = fail_msg.rsplit("error.").next().unwrap_or(fail_msg);
match kind {
"ConnectionRefused" => ("ECONNREFUSED", "connect ECONNREFUSED".to_string()),
"Timeout" => ("ETIMEDOUT", "connect ETIMEDOUT".to_string()),
"ConnectionClosed" => ("ECONNRESET", "socket connection closed before response".to_string()),
"ConnectionReset" => ("ECONNRESET", "read ECONNRESET".to_string()),
"Aborted" | "AbortedBeforeConnecting" => {
("ABORT_ERR", "The operation was aborted".to_string())
}
"HTTP2Unsupported" => ("ERR_HTTP2_ERROR", "HTTP/2 is not supported by the server".to_string()),
_ => ("UND_ERR_FETCH_FAILED", fail_msg.to_string()),
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn reject_with_message(cx: *mut JSContext, promise_h: Handle<*mut JSObject>, msg: &str) {
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let err_obj = JS_NewPlainObject(cx));
if !err_obj.is_null() {
let c_msg = ZBox::from_bytes(msg.as_bytes());
let js_str = JS_NewStringCopyZ(cx, c_msg.as_ptr());
if !js_str.is_null() {
rooted!(&in(cx_ref) let msg_val = StringValue(&*js_str));
JS_DefineProperty(
cx,
err_obj.handle().into(),
c"message".as_ptr(),
msg_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
rooted!(&in(cx_ref) let ev = if err_obj.is_null() {
UndefinedValue()
} else {
ObjectValue(err_obj.get())
});
JS::RejectPromise(cx, promise_h, ev.handle().into());
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn has_pending_false_initially() {
assert!(!has_pending());
}
#[test]
fn pending_fetch_is_send() {
fn assert_send<T: Send>() {}
assert_send::<PendingFetch>();
}
#[test]
fn outcome_slot_roundtrip() {
let slot: Arc<Mutex<Option<FetchOutcome>>> = Arc::new(Mutex::new(None));
{
let mut g = slot.lock().unwrap();
*g = Some(Ok(stealth_result_for_test()));
}
let taken = slot.lock().unwrap().take();
assert!(taken.is_some());
match taken.unwrap() {
Ok(r) => assert_eq!(r.status_code, 200),
Err(_) => panic!("expected Ok"),
}
}
#[test]
fn has_schedule_callback_atomic_roundtrip() {
let pf = PendingFetch {
cx: ::std::ptr::null_mut(),
promise_root: None,
promise_val: UndefinedValue(),
outcome: Arc::new(Mutex::new(None)),
kind: ResolveKind::Response,
mini_loop_ptr: ::std::ptr::null(),
concurrent_task: Default::default(),
has_schedule_callback: AtomicBool::new(false),
url_owned: None,
body_owned: None,
headers_owned: None,
abort_flag: None,
abort_id: None,
};
assert!(!pf.has_schedule_callback.load(AtomicOrdering::Relaxed));
assert!(
pf.has_schedule_callback
.compare_exchange(false, true, AtomicOrdering::AcqRel, AtomicOrdering::Acquire)
.is_ok()
);
assert!(pf.has_schedule_callback.load(AtomicOrdering::Relaxed));
pf.has_schedule_callback
.store(false, AtomicOrdering::Release);
assert!(!pf.has_schedule_callback.load(AtomicOrdering::Relaxed));
}
#[test]
fn abort_registry_miss_is_silent_noop() {
trigger_abort(u32::MAX);
}
#[test]
fn abort_ids_are_monotonic_and_unique() {
let a = new_abort_id();
let b = new_abort_id();
assert_ne!(a, b, "abort ids must be unique per fetch");
}
fn stealth_result_for_test() -> StealthSyncResult {
use compact_str::CompactString;
use smallvec::smallvec;
StealthSyncResult {
status_code: 200,
status_text: CompactString::new("OK"),
headers: smallvec![],
body: bytes::Bytes::from_static(b"hello"),
}
}
}