Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
253d892
fetch: reserve Content-Length on the buffered-body handoff so arrayBu…
robobun Jul 31, 2026
995e917
test: import node:net at module scope
robobun Jul 31, 2026
4ca3599
tighten comments
robobun Jul 31, 2026
54ab69a
gate the Content-Length reserve on BufferAll mode
robobun Jul 31, 2026
a8b1e0a
gate the Content-Length reserve on on_start_buffering, not BufferAll …
robobun Jul 31, 2026
51103b4
clear is_buffering_body when a ByteStream attaches
robobun Jul 31, 2026
b8c5b61
AsyncHTTP: drop response_buffer field + init/init_sync param
robobun Jul 31, 2026
5302034
InternalState: replace body_out_str with owned decoded_body
robobun Jul 31, 2026
62fb870
s3: S3HttpSimpleTask reads response_buffer, extends in http_callback
robobun Jul 31, 2026
df2cb07
FetchTasklet: drop response_buffer; read result.body as &[u8]
robobun Jul 31, 2026
df3e7fd
http/lib: own decoded_body, deliver &[u8] in progress callback
robobun Jul 31, 2026
2f17b0f
NetworkTask.notify: read result.body slice, accumulate locally
robobun Jul 31, 2026
df0accc
s3/download_stream: result.body is &[u8]; drop response_buffer field
robobun Jul 31, 2026
314db32
RemoteImageDownload: extend response_buffer from result.body; drop in…
robobun Jul 31, 2026
3cf915d
s3/client.rs: drop response_buffer arg from AsyncHTTP::init calls
robobun Jul 31, 2026
0881b6d
http: send_sync takes &mut MutableString; channel appends body
robobun Jul 31, 2026
4bb2ac0
s3 download_stream: accumulate body on every callback
robobun Jul 31, 2026
10b0708
http: lift decoded_body to stack before terminal callback
robobun Jul 31, 2026
67b4da8
send_sync: set response_buffer before boxing, drop unsafe write
robobun Jul 31, 2026
ff882dc
NetworkTask::notify: reset response_buffer on new-attempt metadata
robobun Jul 31, 2026
636c841
FetchTasklet: clear is_buffering_body under mutex on stream attach
robobun Jul 31, 2026
5170259
test(fetch): trim long-redirect loop under debug/ASAN
robobun Jul 31, 2026
2f5f428
abort-signal-leak: scale iterations on debug to fit 5s budget
robobun Jul 31, 2026
c3f621e
fetch-leak fixture: cache the 2MB string for URLSearchParams
robobun Jul 31, 2026
23aaf8e
fetch-tcp-stress: scale iterations on debug/ASAN to fit 30s budget
robobun Jul 31, 2026
8214d20
fetch-leak: cut ITERATIONS to 20 on debug/ASAN for URLSearchParams
robobun Jul 31, 2026
7157194
revert unrelated test-timing tweaks (scope creep; pre-existing on main)
robobun Jul 31, 2026
cf1f11f
FetchTasklet: release scheduled_response_buffer capacity above 512K o…
robobun Jul 31, 2026
c6d6a5a
s3/download_stream: handle_oom on write; name DECODED_BODY_RETAIN_CAP
robobun Jul 31, 2026
48b9d2a
http: carry owned body_owned Vec on terminal callback for zero-copy h…
robobun Jul 31, 2026
7ba280e
s3/simple_request: add SAFETY comment for detach_lifetime (clippy)
robobun Jul 31, 2026
637d54b
FetchTasklet: adopt body_owned before reserving to avoid transient 2x…
robobun Jul 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 19 additions & 35 deletions src/http/AsyncHTTP.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ bun_core::declare_scope!(AsyncHTTP, visible);
pub struct AsyncHTTP<'a> {
pub response: Option<picohttp::Response<'static>>,
pub request_headers: headers::EntryList,
// Caller-owned response buffer (raw pointer, lifetime-erased); never freed here.
pub response_buffer: *mut MutableString,
pub request_body: HTTPRequestBody<'a>,
pub(crate) method: Method,
pub url: URL<'a>,
Expand Down Expand Up @@ -302,7 +300,6 @@ impl<'a> AsyncHTTP<'a> {
self.elapsed = src.elapsed;
self.err = src.err;
self.response = src.response;
self.response_buffer = src.response_buffer;
self.client.url = src.client.url.clone();
self.client.flags = src.client.flags;
self.client.remaining_redirect_count = src.client.remaining_redirect_count;
Expand All @@ -321,12 +318,9 @@ impl<'a> AsyncHTTP<'a> {
// ──────────────────────────────────────────────────────────────────────────

struct Preconnect {
// Self-referential — `async_http.response_buffer` borrows
// `self.response_buffer`. `Option` so we can write the field after the heap
// address is fixed (late-init); `None` is never observed after `preconnect()`
// populates it.
// `Option` so we can write the field after the heap address is fixed
// (late-init); `None` is never observed after `preconnect()` populates it.
Comment thread
robobun marked this conversation as resolved.
async_http: Option<AsyncHTTP<'static>>,
response_buffer: MutableString,
url: URL<'static>,
is_url_owned: bool,
}
Expand All @@ -336,7 +330,6 @@ impl Preconnect {
// SAFETY: `this` was produced by `heap::alloc` in `preconnect()` and is
// uniquely owned here; `async_http` was fully written before scheduling.
unsafe {
(*this).response_buffer = MutableString::default();
(*this)
.async_http
.as_mut()
Expand All @@ -348,7 +341,7 @@ impl Preconnect {
free_owned_href((*this).url.href);
}
// Reclaim and drop the heap allocation (runs Drop on `async_http`
// — which in turn drops `HTTPClient` — and on `response_buffer`).
// — which in turn drops `HTTPClient`).
drop(bun_core::heap::take(this));
}
}
Expand All @@ -374,23 +367,20 @@ pub fn preconnect(url: URL<'static>, is_url_owned: bool) {

let this: *mut Preconnect = bun_core::heap::into_raw(Box::new(Preconnect {
async_http: None,
response_buffer: MutableString::default(),
url,
is_url_owned,
}));

// SAFETY: `this` is a freshly Box-allocated, uniquely-owned pointer; we
// in-place write `async_http` before any read and before it can be observed
// by another thread. The address of `response_buffer` is stable (heap).
// by another thread.
unsafe {
let response_buffer: *mut MutableString = core::ptr::addr_of_mut!((*this).response_buffer);
let url = (*this).url.clone();
let async_http = (*this).async_http.insert(AsyncHTTP::init(
Method::GET,
url,
headers::EntryList::default(),
b"",
response_buffer,
b"",
HTTPClientResultCallback::new::<Preconnect>(this, Preconnect::on_result),
FetchRedirect::Manual,
Expand All @@ -412,7 +402,6 @@ impl<'a> AsyncHTTP<'a> {
url: URL<'a>,
headers: headers::EntryList,
headers_buf: &'a [u8],
response_buffer: *mut MutableString,
request_body: &'a [u8],
callback: HTTPClientResultCallback,
redirect_type: FetchRedirect,
Expand Down Expand Up @@ -449,7 +438,6 @@ impl<'a> AsyncHTTP<'a> {
let mut this = AsyncHTTP {
response: None,
request_headers: headers,
response_buffer,
request_body: HTTPRequestBody::Bytes(request_body),
method,
url,
Expand Down Expand Up @@ -517,7 +505,6 @@ impl<'a> AsyncHTTP<'a> {
url: URL<'a>,
headers: headers::EntryList,
headers_buf: &'a [u8],
response_buffer: *mut MutableString,
request_body: &'a [u8],
http_proxy: Option<URL<'a>>,
hostname: Option<&'a [u8]>,
Expand All @@ -528,7 +515,6 @@ impl<'a> AsyncHTTP<'a> {
url,
headers,
headers_buf,
response_buffer,
request_body,
noop_callback(),
redirect_type,
Expand Down Expand Up @@ -556,13 +542,15 @@ impl<'a> AsyncHTTP<'a> {
pub(crate) struct SingleHTTPChannel {
slot: bun_threading::Guarded<Option<HTTPClientResult<'static>>>,
cv: bun_threading::Condvar,
response_buffer: *mut MutableString,
}

impl SingleHTTPChannel {
pub(crate) fn init() -> SingleHTTPChannel {
SingleHTTPChannel {
slot: bun_threading::Guarded::new(None),
cv: bun_threading::Condvar::new(),
response_buffer: core::ptr::null_mut(),
}
}
fn write_item(&self, item: HTTPClientResult<'static>) {
Expand All @@ -584,7 +572,7 @@ impl SingleHTTPChannel {
fn send_sync_callback(
this: *mut SingleHTTPChannel,
async_http: *mut AsyncHTTP<'static>,
result: HTTPClientResult<'_>,
mut result: HTTPClientResult<'_>,
) {
// `init_sync` leaves every streaming/progress signal unset, so the only
// callback is the terminal one; writing on `has_more` would hand
Expand All @@ -606,25 +594,29 @@ fn send_sync_callback(
real.response = None;
real.err = async_http.err;
real.elapsed = async_http.elapsed;
real.response_buffer = async_http.response_buffer;
}
// SAFETY: `this` is the leaked `SingleHTTPChannel` from `send_sync` and is
// alive for the process lifetime; `result` borrows the HTTP-thread copy's
// response buffer, which is the caller's buffer — outlives the read in
// `send_sync`.
// SAFETY: `this` is the heap `SingleHTTPChannel` from `send_sync`;
// `response_buffer` is the caller's `&mut MutableString` which outlives
// `read_item`.
unsafe {
result.body_into(&mut (*(*this).response_buffer).list);
(*this).write_item(result.detach_lifetime());
}
}

impl<'a> AsyncHTTP<'a> {
pub fn send_sync(&mut self) -> crate::Result<crate::HTTPResponseMetadata> {
pub fn send_sync(
&mut self,
response_buffer: &mut MutableString,
) -> crate::Result<crate::HTTPResponseMetadata> {
crate::http_thread::init(&Default::default());

// Note: `Box::leak` is forbidden (PORTING.md §Forbidden);
// allocate via `heap::alloc` and reclaim once
// the single sync callback has fired and we've read the result.
let ctx = bun_core::heap::into_raw_nn(Box::new(SingleHTTPChannel::init()));
let mut ch = SingleHTTPChannel::init();
ch.response_buffer = &raw mut *response_buffer;
let ctx = bun_core::heap::into_raw_nn(Box::new(ch));
self.result_callback =
HTTPClientResultCallback::new::<SingleHTTPChannel>(ctx.as_ptr(), send_sync_callback);

Expand Down Expand Up @@ -851,20 +843,12 @@ impl<'a> AsyncHTTP<'a> {

self.elapsed = http_thread_timer_read();

// `response_buffer` was set in `init()` to a caller-owned MutableString
// that outlives this request — the very buffer `start()` records as
// `state.body_out_str`. Route through the shared `body_out` accessor
// (one centralised unsafe).
let response_buffer = crate::body_out::as_mut(
NonNull::new(self.response_buffer).expect("response_buffer set in init"),
);

// Note: `HTTPRequestBody` is not `Clone` (the `Stream` arm holds an
// intrusive refcount). Move owned
// payloads into the client and leave a detached placeholder so Drop on
// `self.request_body` is a no-op.
let body = core::mem::replace(&mut self.request_body, HTTPRequestBody::Bytes(b""));
self.client.start(body, response_buffer);
self.client.start(body);
}
}

Expand Down
Loading
Loading