use ::std::cell::RefCell;
use ::std::collections::{HashMap, VecDeque};
use ::std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicU8, Ordering as AtomicOrdering};
use ::std::sync::{Arc, Mutex};
use bao_engine::context::RawValueRootGuard;
use bun_core::ZBox;
use bun_core::backpressure::BackpressureValve;
use mozjs::glue::JS_GetReservedSlot;
use mozjs::jsapi::*;
use mozjs::jsval::{DoubleValue, 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>,
store_aborted: ::std::option::Option<core::ptr::NonNull<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 const UNOBSERVED_BODY_HIGH_WATER_MARK: usize = 256 * 1024;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[repr(u8)]
pub enum StreamPhase {
Pending = 0,
HeadersArrived = 1,
Streaming = 2,
Parked = 3,
Done = 4,
Canceled = 5,
}
impl StreamPhase {
fn from_u8(v: u8) -> StreamPhase {
match v {
1 => StreamPhase::HeadersArrived,
2 => StreamPhase::Streaming,
3 => StreamPhase::Parked,
4 => StreamPhase::Done,
5 => StreamPhase::Canceled,
_ => StreamPhase::Pending,
}
}
}
pub(crate) struct StreamHead {
pub status_code: u32,
pub status_text: String,
pub headers: Vec<(String, String)>,
}
#[derive(Clone)]
pub(crate) enum StreamFail {
Aborted,
Other(String),
}
pub(crate) struct StreamShared {
pub head: Option<StreamHead>,
pub staging: VecDeque<Vec<u8>>,
pub closed: bool,
pub fail: Option<StreamFail>,
}
pub(crate) struct StreamingState {
pub signals_store: bun_http::signals::Store,
pub async_http_id: AtomicU32,
pub shared: Mutex<StreamShared>,
pub phase: AtomicU8,
pub valve: BackpressureValve,
pub finalize_pending: AtomicBool,
pub promise_settled: bool,
pub transport_released: bool,
pub stream_released: AtomicBool,
pub pending_pull: Option<RawValueRootGuard>,
pub keepalive_held: bool,
pub source_id: u64,
}
impl StreamingState {
fn new(aborted_wired: bool) -> StreamingState {
let mut store = bun_http::signals::Store::default();
if aborted_wired {
store.aborted = AtomicBool::new(false);
}
StreamingState {
signals_store: store,
async_http_id: AtomicU32::new(0),
shared: Mutex::new(StreamShared {
head: None,
staging: VecDeque::new(),
closed: false,
fail: None,
}),
phase: AtomicU8::new(StreamPhase::Pending as u8),
valve: BackpressureValve::park_at(UNOBSERVED_BODY_HIGH_WATER_MARK),
finalize_pending: AtomicBool::new(false),
promise_settled: false,
transport_released: false,
stream_released: AtomicBool::new(false),
pending_pull: None,
keepalive_held: false,
source_id: 0,
}
}
}
thread_local! {
static STREAM_REGISTRY: RefCell<HashMap<u64, *mut PendingFetch>> =
RefCell::new(HashMap::new());
}
static NEXT_STREAM_ID: AtomicU64 = AtomicU64::new(1);
pub fn stream_phase(this: *mut PendingFetch) -> StreamPhase {
if this.is_null() {
return StreamPhase::Pending;
}
let phase = unsafe { &*this }
.streaming
.as_ref()
.map(|s| s.phase.load(AtomicOrdering::Acquire))
.unwrap_or(StreamPhase::Pending as u8);
StreamPhase::from_u8(phase)
}
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,
pub method: bun_http::Method,
mini_loop_ptr: *const bun_event_loop::MiniEventLoop::MiniEventLoop<'static>,
concurrent_task: bun_event_loop::AnyTaskWithExtraContext::AnyTaskWithExtraContext,
has_schedule_callback: AtomicBool,
refcount: AtomicU32,
streaming: Option<Box<StreamingState>>,
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,
false,
)
}
}
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,
false,
)
}
}
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,
false,
)
}
}
pub unsafe fn start_fetch_streaming(
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,
true,
)
}
}
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,
false,
)
}
}
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>,
streaming: bool,
) {
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,
method,
mini_loop_ptr: ::std::ptr::null(), concurrent_task: bun_event_loop::AnyTaskWithExtraContext::AnyTaskWithExtraContext::default(
),
has_schedule_callback: AtomicBool::new(false),
refcount: AtomicU32::new(if streaming { 2 } else { 1 }),
streaming: if streaming {
Some(Box::new(StreamingState::new(abort.is_some())))
} else {
None
},
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: ::std::option::Option<bun_http::Signals> = if streaming {
let store_ptr: *mut bun_http::signals::Store = unsafe {
core::ptr::addr_of_mut!(
(*(*pending_ptr).streaming.as_mut().unwrap()).signals_store
)
};
Some(bun_http::signals::Signals {
header_progress: Some(unsafe {
core::ptr::NonNull::new_unchecked(core::ptr::addr_of_mut!((*store_ptr).header_progress))
}),
response_body_streaming: Some(unsafe {
core::ptr::NonNull::new_unchecked(core::ptr::addr_of_mut!(
(*store_ptr).response_body_streaming
))
}),
aborted: Some(unsafe {
core::ptr::NonNull::new_unchecked(core::ptr::addr_of_mut!((*store_ptr).aborted))
}),
..Default::default()
})
} else {
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,
)));
{
let async_http_id = unsafe { (*async_http_box).async_http_id };
if streaming {
let state = unsafe { (*pending_ptr).streaming.as_mut().unwrap() };
state
.async_http_id
.store(async_http_id, AtomicOrdering::Release);
unsafe {
(*async_http_box).enable_response_body_streaming();
(*async_http_box).signal_header_progress();
}
}
if let Some(req) = &abort {
let store_aborted = if streaming {
unsafe {
::std::option::Option::Some(core::ptr::NonNull::new_unchecked(
core::ptr::addr_of_mut!(
(*(*pending_ptr).streaming.as_mut().unwrap()).signals_store.aborted
),
))
}
} else {
::std::option::Option::None
};
ABORT_REGISTRY.with(|r| {
r.borrow_mut().insert(
req.id,
AbortEntry {
flag: Arc::clone(&req.flag),
store_aborted,
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();
if streaming {
unsafe {
(*pending_ptr).streaming.as_mut().unwrap().keepalive_held = true;
}
}
}
}
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);
if let ::std::option::Option::Some(store_aborted) = entry.store_aborted {
unsafe { store_aborted.as_ref().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 unsafe { &*this }.streaming.is_some() {
on_http_done_streaming(this, async_http_box, result, result_is_terminal);
return;
}
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 result_is_terminal {
reclaim_async_http_boxes(async_http_box);
}
}
fn reclaim_async_http_boxes(async_http_box: *mut bun_http::AsyncHTTP<'static>) {
if async_http_box.is_null() {
return;
}
let real = unsafe { (*async_http_box).real.take() };
let Some(real_ptr) = real else {
return;
};
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 on_http_done_streaming(
this: *mut PendingFetch,
async_http_box: *mut bun_http::AsyncHTTP<'static>,
mut result: bun_http::HTTPClientResult<'_>,
terminal: bool,
) {
let Some(state) = (unsafe { &*this }).streaming.as_ref() else {
return;
};
let mut wake = false;
if let Some(fail) = result.fail {
let fail_str = format!("{:?}", fail);
let stream_fail = if fail_str.contains("Aborted") {
StreamFail::Aborted
} else {
StreamFail::Other(fail_str)
};
if let Ok(mut g) = state.shared.lock() {
g.fail = Some(stream_fail);
g.closed = true;
}
wake = true;
} else {
let mut staged_bytes = 0usize;
{
let mut g = state.shared.lock().unwrap();
if g.head.is_none() {
if let Some(metadata) = result.metadata.as_ref() {
g.head = Some(StreamHead {
status_code: metadata.response.status_code,
status_text: ::std::str::from_utf8(metadata.response.status)
.unwrap_or("")
.to_string(),
headers: metadata
.response
.headers
.list
.iter()
.map(|h| {
(
::std::str::from_utf8(h.name()).unwrap_or("").to_string(),
::std::str::from_utf8(h.value()).unwrap_or("").to_string(),
)
})
.collect(),
});
wake = true;
}
}
if let Some(body) = result.body.as_deref() {
let bytes = body.list.as_slice();
if !bytes.is_empty() {
staged_bytes = bytes.len();
state.valve.note_produced(bytes.len());
g.staging.push_back(bytes.to_vec());
}
}
if terminal {
g.closed = true;
}
}
if let Some(body) = result.body.as_deref_mut() {
body.list.clear();
}
if !terminal && staged_bytes > 0 && !state.valve.is_parked() {
let id = state.async_http_id.load(AtomicOrdering::Acquire);
if id != 0 {
bun_http::http_thread_mut().schedule_response_body_drain(id);
}
}
wake = wake || staged_bytes > 0 || terminal;
}
if wake {
unsafe { schedule_tasklet_wake(this) };
}
if terminal {
reclaim_async_http_boxes(async_http_box);
}
}
unsafe fn schedule_tasklet_wake(this: *mut PendingFetch) {
if unsafe { &*this }
.has_schedule_callback
.compare_exchange(false, true, AtomicOrdering::AcqRel, AtomicOrdering::Acquire)
.is_err()
{
return;
}
let loop_ptr = unsafe { &*this }.mini_loop_ptr;
if loop_ptr.is_null() {
return;
}
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),
)
};
}
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);
if unsafe { &*this }.streaming.is_some() {
unsafe { process_stream_event(this) };
return;
}
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 null_body =
response_body_is_null(resp.status_code as u16, pending.method);
let resp_obj = build_response_js(cx, &resp, null_body);
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);
}
}
}
}
unsafe {
deref_tasklet(this);
}
mozjs_sys::jsapi::js::RunJobs(cx);
}
unsafe fn deref_tasklet(this: *mut PendingFetch) {
let prev = unsafe { &*this }.refcount.fetch_sub(1, AtomicOrdering::AcqRel);
debug_assert!(prev >= 1, "FetchTasklet refcount underflow");
if prev == 1 {
unsafe { free_tasklet(this) };
}
}
unsafe fn free_tasklet(this: *mut PendingFetch) {
let streaming = unsafe { (*this).streaming.is_some() };
let keepalive_held = unsafe {
(*this)
.streaming
.as_ref()
.is_some_and(|s| s.keepalive_held)
};
{
let ctx = crate::timers::with_event_loop(|loop_| {
bun_event_loop::MiniEventLoop::MiniEventLoop::as_event_loop_ctx(loop_)
});
if ctx.is_js() && (!streaming || keepalive_held) {
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(source_id) = unsafe {
(*this)
.streaming
.as_ref()
.map(|s| s.source_id)
.filter(|&id| id != 0)
} {
STREAM_REGISTRY.with(|r| {
r.borrow_mut().remove(&source_id);
});
}
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));
}
}
unsafe fn process_stream_event(this: *mut PendingFetch) {
let cx = unsafe { (*this).cx };
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
let finalize = unsafe {
(*this)
.streaming
.as_ref()
.expect("streaming dispatch on buffered fetch")
.finalize_pending
.load(AtomicOrdering::Acquire)
};
if finalize {
cancel_stream_core(this);
let transport_already_released = unsafe {
(*this).streaming.as_ref().unwrap().transport_released
};
release_stream_ref_once(this);
if transport_already_released {
mozjs_sys::jsapi::js::RunJobs(cx);
return;
}
}
let settle_now = unsafe {
let state = (*this).streaming.as_ref().unwrap();
!state.promise_settled && {
let g = state.shared.lock().unwrap();
g.head.is_some() || g.fail.is_some() || g.closed
}
};
if settle_now {
let (head, fail) = unsafe {
let state = (*this).streaming.as_ref().unwrap();
let mut g = state.shared.lock().unwrap();
(g.head.take(), g.fail.clone())
};
let pending = unsafe { &*this };
let promise_val = pending
.promise_root
.as_ref()
.map_or(pending.promise_val, |g| g.get(0));
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;
let aborted = unsafe {
let state = (*this).streaming.as_ref().unwrap();
state.signals_store.aborted.load(AtomicOrdering::Acquire)
|| pending
.abort_flag
.as_ref()
.is_some_and(|f| f.load(AtomicOrdering::Acquire))
};
if let Some(fail) = fail.as_ref().filter(|_| !aborted) {
reject_with_network_error(cx, promise_h, &match fail {
StreamFail::Aborted => "error.Aborted".to_string(),
StreamFail::Other(msg) => msg.clone(),
});
} else if aborted {
reject_promise_with_abort_error(cx, promise_h);
} else if let Some(head) = head.as_ref() {
let null_body =
response_body_is_null(head.status_code as u16, pending.method);
match unsafe { build_streaming_response_js(cx, this, head, !null_body) } {
::std::option::Option::Some(resp_obj) if !resp_obj.is_null() => {
rooted!(&in(realm_cx) let resp_val = ObjectValue(resp_obj));
JS::ResolvePromise(cx, promise_h, resp_val.handle().into());
if null_body {
cancel_stream_core(this);
} else {
set_phase(this, StreamPhase::HeadersArrived);
}
}
_ => {
reject_with_message(cx, promise_h, "http: failed to build streaming Response");
cancel_stream_core(this);
}
}
} else {
reject_with_message(
cx,
promise_h,
"fetch: streaming response completed without metadata",
);
}
}
unsafe {
(*this).streaming.as_mut().unwrap().promise_settled = true;
}
unsafe { deref_tasklet(this) };
}
let fail = unsafe {
(*this)
.streaming
.as_ref()
.unwrap()
.shared
.lock()
.unwrap()
.fail
.clone()
};
if let Some(fail) = fail {
unsafe { reject_parked_pull(this, |cx, pull_h| {
match &fail {
StreamFail::Aborted => reject_promise_with_abort_error(cx, pull_h),
StreamFail::Other(msg) => reject_with_network_error(cx, pull_h, msg),
}
}) };
let phase = current_phase(this);
if phase != StreamPhase::Canceled {
set_phase(this, StreamPhase::Canceled);
}
}
let deliver = unsafe {
let state = (*this).streaming.as_ref().unwrap();
state.pending_pull.is_some() && {
let g = state.shared.lock().unwrap();
g.staging.front().is_some()
}
};
if deliver {
let chunk = unsafe {
let state = (*this).streaming.as_ref().unwrap();
let mut g = state.shared.lock().unwrap();
match g.staging.pop_front() {
::std::option::Option::Some(c) => {
state.valve.note_consumed(c.len());
::std::option::Option::Some(c)
}
::std::option::Option::None => ::std::option::Option::None,
}
};
if let ::std::option::Option::Some(chunk) = chunk {
unsafe { resolve_parked_pull_with_chunk(this, chunk) };
if current_phase(this) == StreamPhase::HeadersArrived {
set_phase(this, StreamPhase::Streaming);
}
}
}
let (closed, staged_bytes) = unsafe {
let state = (*this).streaming.as_ref().unwrap();
let g = state.shared.lock().unwrap();
(g.closed, state.valve.level())
};
if closed {
if staged_bytes == 0 {
unsafe { resolve_parked_pull_with_done(this) };
if current_phase(this) == StreamPhase::Streaming
|| current_phase(this) == StreamPhase::HeadersArrived
|| current_phase(this) == StreamPhase::Parked
{
set_phase(this, StreamPhase::Done);
}
}
}
if !closed
&& unsafe {
let state = (*this).streaming.as_ref().unwrap();
state.valve.above_high(staged_bytes)
&& state.pending_pull.is_none()
&& !state.valve.is_parked()
}
&& matches!(
current_phase(this),
StreamPhase::HeadersArrived | StreamPhase::Streaming
)
{
park_stream(this);
}
unsafe {
let terminal_observed =
closed || (*this).streaming.as_ref().unwrap().shared.lock().unwrap().fail.is_some();
if terminal_observed {
release_transport_ref_once(this);
}
}
mozjs_sys::jsapi::js::RunJobs(cx);
}
fn current_phase(this: *mut PendingFetch) -> StreamPhase {
unsafe { &*this }
.streaming
.as_ref()
.map(|s| StreamPhase::from_u8(s.phase.load(AtomicOrdering::Acquire)))
.unwrap_or(StreamPhase::Pending)
}
fn set_phase(this: *mut PendingFetch, phase: StreamPhase) {
if let Some(state) = unsafe { &*this }.streaming.as_ref() {
state.phase.store(phase as u8, AtomicOrdering::Release);
}
}
fn park_stream(this: *mut PendingFetch) {
let Some(state) = (unsafe { &*this }).streaming.as_ref() else {
return;
};
state.valve.latch_park();
set_phase(this, StreamPhase::Parked);
PENDING.with(|p| {
let mut guard = p.borrow_mut();
if let Some(pos) = guard.iter().position(|&ptr| ptr == this) {
guard.swap_remove(pos);
}
});
if state.keepalive_held {
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();
}
unsafe {
(*this).streaming.as_mut().unwrap().keepalive_held = false;
}
}
bun_http::HTTPThread::schedule_transport_pause_from_any_thread(
state.async_http_id.load(AtomicOrdering::Acquire),
bun_http::http_thread::TransportPauseKind::Pause,
);
}
fn unpark_stream(this: *mut PendingFetch) {
let Some(state) = (unsafe { &*this }).streaming.as_ref() else {
return;
};
let closed = state.shared.lock().map_or(true, |g| g.closed);
if closed {
return;
}
PENDING.with(|p| {
p.borrow_mut().push(this);
});
if !state.keepalive_held {
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();
}
unsafe {
(*this).streaming.as_mut().unwrap().keepalive_held = true;
}
}
if current_phase(this) == StreamPhase::Parked {
set_phase(this, StreamPhase::Streaming);
}
bun_http::HTTPThread::schedule_transport_pause_from_any_thread(
state.async_http_id.load(AtomicOrdering::Acquire),
bun_http::http_thread::TransportPauseKind::Resume,
);
schedule_response_body_drain_from_any_thread(
state.async_http_id.load(AtomicOrdering::Acquire),
);
}
fn schedule_response_body_drain_from_any_thread(async_http_id: u32) {
if async_http_id == 0 {
return;
}
bun_http::http_thread::init(&Default::default());
let ht = unsafe { (*bun_http::HTTP_THREAD.get_unchecked()).as_mut_ptr() };
unsafe {
{
let _guard = (*ht).queued_response_body_drains_lock.lock_guard();
(*ht)
.queued_response_body_drains
.push(bun_http::http_thread::DrainMessage { async_http_id });
}
(*ht).wakeup();
}
}
fn release_transport_ref_once(this: *mut PendingFetch) {
let Some(state) = (unsafe { &mut *this }).streaming.as_mut() else {
return;
};
if state.transport_released {
return;
}
state.transport_released = true;
unsafe { deref_tasklet(this) };
}
fn release_stream_ref_once(this: *mut PendingFetch) {
let Some(state) = (unsafe { &*this }).streaming.as_ref() else {
return;
};
if state.stream_released.swap(true, AtomicOrdering::AcqRel) {
return;
}
unsafe { deref_tasklet(this) };
}
fn cancel_stream_core(this: *mut PendingFetch) {
let Some(state) = (unsafe { &*this }).streaming.as_ref() else {
return;
};
if matches!(current_phase(this), StreamPhase::Done | StreamPhase::Canceled) {
return;
}
set_phase(this, StreamPhase::Canceled);
let closed = state.shared.lock().map_or(true, |g| g.closed);
if !closed {
state
.signals_store
.aborted
.store(true, AtomicOrdering::Release);
let id = state.async_http_id.load(AtomicOrdering::Acquire);
if id != 0 {
bun_http::HTTPThread::schedule_shutdown_from_any_thread(id);
}
}
}
unsafe fn reject_parked_pull<F>(this: *mut PendingFetch, reject: F)
where
F: FnOnce(*mut JSContext, Handle<*mut JSObject>),
{
let Some(root) = (unsafe { &mut *this })
.streaming
.as_mut()
.and_then(|s| s.pending_pull.take())
else {
return;
};
let cx = unsafe { (*this).cx };
let pull_val = root.get(0);
if pull_val.is_object() {
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 pull_obj = pull_val.to_object());
{
let _realm = AutoRealm::new_from_handle(cx_ref, pull_obj.handle());
reject(cx, pull_obj.handle().into());
}
}
drop(root);
mozjs_sys::jsapi::js::RunJobs(cx);
}
unsafe fn resolve_parked_pull_with_chunk(this: *mut PendingFetch, chunk: Vec<u8>) {
let Some(root) = (unsafe { &mut *this })
.streaming
.as_mut()
.and_then(|s| s.pending_pull.take())
else {
return;
};
let cx = unsafe { (*this).cx };
let pull_val = root.get(0);
if pull_val.is_object() {
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 pull_obj = pull_val.to_object());
{
let _realm = AutoRealm::new_from_handle(cx_ref, pull_obj.handle());
unsafe { resolve_pull_result(cx, pull_obj.handle().into(), ::std::option::Option::Some(chunk)) };
}
}
drop(root);
mozjs_sys::jsapi::js::RunJobs(cx);
}
unsafe fn resolve_parked_pull_with_done(this: *mut PendingFetch) {
let Some(root) = (unsafe { &mut *this })
.streaming
.as_mut()
.and_then(|s| s.pending_pull.take())
else {
return;
};
let cx = unsafe { (*this).cx };
let pull_val = root.get(0);
if pull_val.is_object() {
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 pull_obj = pull_val.to_object());
{
let _realm = AutoRealm::new_from_handle(cx_ref, pull_obj.handle());
unsafe { resolve_pull_result(cx, pull_obj.handle().into(), ::std::option::Option::None) };
}
}
drop(root);
mozjs_sys::jsapi::js::RunJobs(cx);
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn resolve_pull_result(
cx: *mut JSContext,
promise_h: Handle<*mut JSObject>,
chunk: ::std::option::Option<Vec<u8>>,
) {
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 result_obj = JS_NewPlainObject(cx));
if result_obj.is_null() {
return;
}
match chunk {
::std::option::Option::Some(bytes) => {
rooted!(&in(cx_ref) let arr = JS_NewUint8Array(cx, bytes.len()));
if !arr.is_null() && !bytes.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(arr.get(), &mut ta_len, &mut shared, &mut data);
if !unwrapped.is_null() && !data.is_null() && ta_len >= bytes.len() {
::std::ptr::copy_nonoverlapping(bytes.as_ptr(), data, bytes.len());
}
rooted!(&in(cx_ref) let val_v = ObjectValue(arr.get()));
JS_DefineProperty(
cx,
result_obj.handle().into(),
c"value".as_ptr(),
val_v.handle().into(),
JSPROP_ENUMERATE as u32,
);
} else {
rooted!(&in(cx_ref) let arr0 = JS_NewUint8Array(cx, 0));
if !arr0.is_null() {
rooted!(&in(cx_ref) let val_v = ObjectValue(arr0.get()));
JS_DefineProperty(
cx,
result_obj.handle().into(),
c"value".as_ptr(),
val_v.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
rooted!(&in(cx_ref) let done_v = mozjs::jsval::BooleanValue(false));
JS_DefineProperty(
cx,
result_obj.handle().into(),
c"done".as_ptr(),
done_v.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
::std::option::Option::None => {
rooted!(&in(cx_ref) let done_v = mozjs::jsval::BooleanValue(true));
JS_DefineProperty(
cx,
result_obj.handle().into(),
c"done".as_ptr(),
done_v.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
rooted!(&in(cx_ref) let result_v = ObjectValue(result_obj.get()));
JS::ResolvePromise(cx, promise_h, result_v.handle().into());
}
}
const SLOT_STREAM_ID: u32 = 0;
fn stream_id_from_slot(slot: JSVal) -> u64 {
if slot.is_double() {
slot.to_double() as u64
} else {
0
}
}
unsafe fn stream_source_id(obj: mozjs::rust::Handle<'_, *mut JSObject>) -> u64 {
unsafe {
let mut slot = UndefinedValue();
JS_GetReservedSlot(obj.get(), SLOT_STREAM_ID, &mut slot);
stream_id_from_slot(slot)
}
}
unsafe fn finalizing_stream_source_id(obj: *mut JSObject) -> u64 {
unsafe {
let mut slot = UndefinedValue();
JS_GetReservedSlot(obj, SLOT_STREAM_ID, &mut slot);
stream_id_from_slot(slot)
}
}
unsafe extern "C" fn stream_source_finalize(
_gcx: *mut mozjs_sys::jsapi::JS::GCContext,
obj: *mut JSObject,
) {
unsafe {
let id = finalizing_stream_source_id(obj);
if id == 0 {
return;
}
let Some(this) = STREAM_REGISTRY.with(|r| r.borrow().get(&id).copied()) else {
return; };
let Some(state) = (*this).streaming.as_ref() else {
return;
};
state.finalize_pending.store(true, AtomicOrdering::Release);
schedule_tasklet_wake(this);
}
}
static STREAM_SOURCE_CLASS_OPS: JSClassOps = JSClassOps {
addProperty: None,
delProperty: None,
enumerate: None,
newEnumerate: None,
resolve: None,
mayResolve: None,
finalize: Some(stream_source_finalize),
call: None,
construct: None,
trace: None,
};
const STREAM_SOURCE_CLASS: JSClass = JSClass {
name: c"BaoFetchStreamSource".as_ptr(),
flags: (1 << JSCLASS_RESERVED_SLOTS_SHIFT) as u32,
cOps: &STREAM_SOURCE_CLASS_OPS as *const JSClassOps as *mut JSClassOps,
spec: ::std::ptr::null(),
ext: ::std::ptr::null(),
oOps: ::std::ptr::null(),
};
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn build_stream_source_object(cx: *mut JSContext, this: *mut PendingFetch) -> *mut JSObject {
unsafe {
let id = NEXT_STREAM_ID.fetch_add(1, AtomicOrdering::Relaxed);
STREAM_REGISTRY.with(|r| {
r.borrow_mut().insert(id, this);
});
(*this).streaming.as_mut().unwrap().source_id = id;
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_NewObject(cx, &STREAM_SOURCE_CLASS));
if obj.is_null() {
STREAM_REGISTRY.with(|r| {
r.borrow_mut().remove(&id);
});
(*this).streaming.as_mut().unwrap().source_id = 0;
return ::std::ptr::null_mut();
}
(*this).refcount.fetch_add(1, AtomicOrdering::AcqRel);
rooted!(&in(cx_ref) let id_val = DoubleValue(id as f64));
JS_SetReservedSlot(obj.get(), SLOT_STREAM_ID, &id_val.get());
obj.get()
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn build_streaming_response_js(
cx: *mut JSContext,
this: *mut PendingFetch,
head: &StreamHead,
attach_source: bool,
) -> ::std::option::Option<*mut JSObject> {
unsafe {
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::option::Option::None;
}
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::option::Option::None;
}
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 init_obj = JS_NewPlainObject(cx));
if init_obj.is_null() {
return ::std::option::Option::None;
}
let init_h = init_obj.handle().into();
rooted!(&in(cx_ref) let status_val = mozjs::jsval::Int32Value(head.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(head.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, head.headers.len()));
if headers_arr.is_null() {
return ::std::option::Option::None;
}
let hdrs_h = headers_arr.handle().into();
for (i, (k, v)) in head.headers.iter().enumerate() {
rooted!(&in(cx_ref) let pair =
mozjs_sys::jsapi::JS::NewArrayObject1(cx, 2usize));
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 = [UndefinedValue(), 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::option::Option::None;
}
rooted!(&in(cx_ref) let resp_root = resp_val);
rooted!(&in(cx_ref) let resp_obj_root = resp_root.get().to_object());
if !attach_source {
return ::std::option::Option::Some(resp_root.get().to_object());
}
let source_obj = build_stream_source_object(cx, this);
if source_obj.is_null() {
return ::std::option::Option::None;
}
rooted!(&in(cx_ref) let source_val = ObjectValue(source_obj));
if !JS_DefineProperty(
cx,
resp_obj_root.handle().into(),
c"_bodyStreamSource".as_ptr(),
source_val.handle().into(),
(JSPROP_PERMANENT | JSPROP_READONLY) as u32,
) {
let id = (*this).streaming.as_mut().unwrap().source_id;
STREAM_REGISTRY.with(|r| {
r.borrow_mut().remove(&id);
});
(*this).streaming.as_mut().unwrap().source_id = 0;
(*this).refcount.fetch_sub(1, AtomicOrdering::AcqRel);
return ::std::option::Option::None;
}
::std::option::Option::Some(resp_root.get().to_object())
}
}
#[allow(non_snake_case)]
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn fetch_body_pull_native(
cx: *mut JSContext,
argc: u32,
vp: *mut JSVal,
) -> bool {
unsafe {
let args = CallArgs::from_vp(vp, argc);
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 null_global = ::std::ptr::null_mut::<JSObject>());
let promise = mozjs_sys::jsapi::JS::NewPromiseObject(cx, null_global.handle().into());
if promise.is_null() {
args.rval().set(UndefinedValue());
return true;
}
args.rval().set(ObjectValue(promise));
rooted!(&in(cx_ref) let promise_root = promise);
let promise_h = promise_root.handle().into();
let src_val = *args.get(0).ptr;
if !src_val.is_object() {
resolve_pull_result(cx, promise_h, ::std::option::Option::None);
return true;
}
rooted!(&in(cx_ref) let src_obj = src_val.to_object());
let id = stream_source_id(src_obj.handle());
if id == 0 {
resolve_pull_result(cx, promise_h, ::std::option::Option::None);
return true;
}
let Some(this) = STREAM_REGISTRY.with(|r| r.borrow().get(&id).copied()) else {
resolve_pull_result(cx, promise_h, ::std::option::Option::None);
return true;
};
let Some(state) = (*this).streaming.as_ref() else {
resolve_pull_result(cx, promise_h, ::std::option::Option::None);
return true;
};
if state.finalize_pending.load(AtomicOrdering::Acquire)
|| matches!(current_phase(this), StreamPhase::Canceled)
{
reject_promise_with_abort_error(cx, promise_h);
return true;
}
if state.valve.take_parked() {
unpark_stream(this);
}
enum PullAction {
Fail(StreamFail),
Chunk(Vec<u8>),
Done,
Park,
}
let action = {
let mut g = state.shared.lock().unwrap();
if let ::std::option::Option::Some(fail) = g.fail.clone() {
PullAction::Fail(fail)
} else if let ::std::option::Option::Some(front) = g.staging.pop_front() {
state.valve.note_consumed(front.len());
PullAction::Chunk(front)
} else if g.closed {
PullAction::Done
} else {
PullAction::Park
}
};
match action {
PullAction::Fail(fail) => match &fail {
StreamFail::Aborted => reject_promise_with_abort_error(cx, promise_h),
StreamFail::Other(msg) => reject_with_network_error(cx, promise_h, msg),
},
PullAction::Chunk(bytes) => {
resolve_pull_result(cx, promise_h, ::std::option::Option::Some(bytes));
}
PullAction::Done => {
resolve_pull_result(cx, promise_h, ::std::option::Option::None);
}
PullAction::Park => {
let pull_val = ObjectValue(promise_root.get());
let root = RawValueRootGuard::new(
cx,
::std::slice::from_ref(&pull_val),
c"FetchStream.pull",
);
if let ::std::option::Option::Some(root) = root {
if let ::std::option::Option::Some(old) =
(*this).streaming.as_mut().unwrap().pending_pull.replace(root)
{
drop(old);
}
}
}
}
true
}
}
#[allow(non_snake_case)]
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn fetch_body_cancel_native(
cx: *mut JSContext,
argc: u32,
vp: *mut JSVal,
) -> bool {
unsafe {
let args = CallArgs::from_vp(vp, argc);
args.rval().set(UndefinedValue());
let src_val = *args.get(0).ptr;
if !src_val.is_object() {
return true;
}
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 src_obj = src_val.to_object());
let id = stream_source_id(src_obj.handle());
if id == 0 {
return true;
}
let Some(this) = STREAM_REGISTRY.with(|r| r.borrow().get(&id).copied()) else {
return true;
};
if (*this).streaming.is_none() {
return true;
}
reject_parked_pull(this, |cx, pull_h| {
reject_promise_with_abort_error(cx, pull_h)
});
cancel_stream_core(this);
release_stream_ref_once(this);
true
}
}
pub unsafe fn install_fetch_stream_natives(
cx: &mut mozjs::context::JSContext,
global: mozjs::rust::Handle<*mut JSObject>,
) {
unsafe {
mozjs::rust::wrappers2::JS_DefineFunction(
cx,
global,
c"__baoFetchBodyPull".as_ptr(),
::std::option::Option::Some(fetch_body_pull_native),
1,
0,
);
mozjs::rust::wrappers2::JS_DefineFunction(
cx,
global,
c"__baoFetchBodyCancel".as_ptr(),
::std::option::Option::Some(fetch_body_cancel_native),
1,
0,
);
}
}
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()
}
fn response_body_is_null(status_code: u16, method: bun_http::Method) -> bool {
matches!(status_code, 204 | 205 | 304) || method == bun_http::Method::HEAD
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn build_response_js(
cx: *mut JSContext,
resp: &StealthSyncResult,
null_body: bool,
) -> *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 = if null_body {
::std::ptr::null_mut::<JSObject>()
} else {
JS_NewUint8Array(cx, resp.body.len())
});
if !null_body && body_arr.is_null() {
return ::std::ptr::null_mut();
}
if !null_body && !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 = [
if null_body {
mozjs::jsval::NullValue()
} else {
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,
method: bun_http::Method::GET,
mini_loop_ptr: ::std::ptr::null(),
concurrent_task: Default::default(),
has_schedule_callback: AtomicBool::new(false),
refcount: AtomicU32::new(1),
streaming: None,
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"),
}
}
}