Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
97 changes: 96 additions & 1 deletion src/runtime/server/RequestContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1506,6 +1506,7 @@ where
}
self.response_weakref.deref();

self.clear_request_body_stream_drain_handler(global_this);
self.request_body_readable_stream_ref.deinit();

// Releases the ref taken in `set_cookies` (via `CookieMapRef::drop`).
Expand Down Expand Up @@ -2374,6 +2375,10 @@ where
self.request_body_buf = Vec::new();

if let Some(resp) = self.resp.take() {
if self.flags.request_body_paused() {
self.flags.set_request_body_paused(false);
resp.resume_();
}
Comment thread
robobun marked this conversation as resolved.
if self.flags.is_waiting_for_request_body() {
self.flags.set_is_waiting_for_request_body(false);
resp.clear_on_data();
Expand Down Expand Up @@ -3901,6 +3906,7 @@ where
if this.request_body_streamed_len > server.config().max_request_body_size {
this.resp.expect("infallible: resp bound").clear_on_data();
this.flags.set_is_waiting_for_request_body(false);
this.resume_request_body_socket();

let _exit = vm.enter_event_loop_scope();

Expand All @@ -3911,6 +3917,9 @@ where

readable.value.ensure_still_alive();
if let Some(bytes) = readable.ptr.bytes() {
let source = bytes.parent_const();
source.drain_handler.set(None);
source.drain_ctx.set(None);
let mut err = Body::ValueError::Message(BunString::static_(
"Request body exceeded maxRequestBodySize",
));
Expand Down Expand Up @@ -3951,7 +3960,14 @@ where
);
// TODO: properly propagate exception upwards
let _ = bytes.on_data(WebCore::streams::Result::Temporary(borrowed));

// What `on_data` buffered; `on_stream_drained` resumes once it empties.
let buffered = bytes.buffer.get().len().saturating_sub(bytes.offset.get());
if buffered >= REQUEST_BODY_HIGH_WATER_MARK {
this.pause_request_body_socket();
}
Comment thread
robobun marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.
} else {
this.resume_request_body_socket();
// Moved out so the Strong (and its underlying GC handle) is
// released at scope exit via `Drop` on `strong::Optional`.
let _strong = core::mem::take(&mut this.request_body_readable_stream_ref);
Expand All @@ -3966,6 +3982,9 @@ where
let bytes = bun_ptr::BackRef::from(
NonNull::new(bytes_ptr).expect("Source::Bytes payload is non-null"),
);
let source = bytes.parent_const();
source.drain_handler.set(None);
source.drain_ctx.set(None);
// TODO: properly propagate exception upwards
let _ = bytes.on_data(WebCore::streams::Result::TemporaryAndDone(borrowed));
}
Expand Down Expand Up @@ -4055,9 +4074,65 @@ where
);
}
this.request_body_buf.extend_from_slice(chunk);

// Pre-stream backpressure; resumed by `on_stream_drained` / `on_start_buffering`.
if !this.flags.request_body_buffer_all()
&& this.request_body_buf.len() >= REQUEST_BODY_HIGH_WATER_MARK
{
this.pause_request_body_socket();
}
Comment thread
robobun marked this conversation as resolved.
}
}

fn pause_request_body_socket(&mut self) {
if self.flags.request_body_paused() {
return;
}
let Some(resp) = self.resp else {
return;
};
ctx_log!("pauseRequestBodySocket");
self.flags.set_request_body_paused(true);
resp.pause();
}
Comment thread
robobun marked this conversation as resolved.

fn resume_request_body_socket(&mut self) {
if !self.flags.request_body_paused() {
return;
}
ctx_log!("resumeRequestBodySocket");
self.flags.set_request_body_paused(false);
if let Some(resp) = self.resp {
resp.resume_();
}
}

/// Detach the body ByteStream's `drain_handler` (the stream can outlive this ctx in JS).
fn clear_request_body_stream_drain_handler(&self, global_this: &JSGlobalObject) {
let Some(readable) = self.request_body_readable_stream_ref.get(global_this) else {
return;
};
if let Some(bytes) = readable.ptr.bytes() {
let source = bytes.parent_const();
source.drain_handler.set(None);
source.drain_ctx.set(None);
}
}

/// # Safety
/// `ctx` must be a `*mut RequestContext` previously registered as the body
/// `on_stream_drained` context.
Comment thread
robobun marked this conversation as resolved.
pub(crate) fn on_request_body_stream_drained_callback(ctx: Option<*mut c_void>) {
let Some(ctx) = ctx else { return };
// SAFETY: caller upholds the fn-level contract — `ctx` is the
// `*mut RequestContext` registered as the body callback context.
let this = unsafe { bun_ptr::callback_ctx::<Self>(ctx) };
if this.is_aborted_or_ended() {
return;
}
Comment thread
claude[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
this.resume_request_body_socket();
}

pub fn on_start_streaming_request_body(&mut self) -> WebCore::DrainResult {
ctx_log!("onStartStreamingRequestBody");
if self.is_aborted_or_ended() {
Expand Down Expand Up @@ -4089,6 +4164,9 @@ where
pub fn on_start_buffering(&mut self) {
if let Some(server) = self.server {
ctx_log!("onStartBuffering");
// `.text()`/`.json()` want the whole body; disable pre-stream backpressure.
self.flags.set_request_body_buffer_all(true);
self.resume_request_body_socket();
// TODO: check if is someone calling onStartBuffering other than onStartBufferingCallback
// if is not, this should be removed and only keep protect + setAbortHandler
// HTTP/3 (RFC 9114): Content-Length is optional; the body is
Expand Down Expand Up @@ -4183,6 +4261,9 @@ where

const MAX_REQUEST_BODY_PREALLOCATE_LENGTH: usize = 1024 * 256;

/// Pause socket reads at this many unconsumed request-body bytes (two 512 KB uWS recv buffers).
const REQUEST_BODY_HIGH_WATER_MARK: usize = 1024 * 1024;

// Trap host fn for the `(false, _, true)` arms of `exported_host_fns`. Those
// `RequestContext` monomorphs (plain-HTTP/3) are type-reachable via the
// blanket H3 impls but never serve requests at runtime — HTTP/3 always
Expand Down Expand Up @@ -4395,7 +4476,7 @@ pub struct SendfileContext {
// `is_web_browser_navigation` / `has_finalized` accessors on the const params.
bitflags::bitflags! {
#[derive(Default, Clone, Copy)]
pub struct FlagsBits: u16 {
pub struct FlagsBits: u32 {
const HAS_MARKED_COMPLETE = 1 << 0;
const HAS_MARKED_PENDING = 1 << 1;
const HAS_ABORT_HANDLER = 1 << 2;
Expand All @@ -4416,6 +4497,10 @@ bitflags::bitflags! {
const ABORTED = 1 << 13;
const HAS_FINALIZED = 1 << 14;
const IS_ERROR_PROMISE_PENDING = 1 << 15;
/// Socket reads are paused because the request-body buffer is over its high-water mark.
const REQUEST_BODY_PAUSED = 1 << 16;
/// `on_start_buffering` fired (`.text()` etc.); skip pre-stream backpressure.
const REQUEST_BODY_BUFFER_ALL = 1 << 17;
}
}

Expand Down Expand Up @@ -4495,6 +4580,16 @@ impl<const DEBUG_MODE: bool> Flags<DEBUG_MODE> {
set_is_error_promise_pending,
IS_ERROR_PROMISE_PENDING
);
flag_accessor!(
request_body_paused,
set_request_body_paused,
REQUEST_BODY_PAUSED
);
flag_accessor!(
request_body_buffer_all,
set_request_body_buffer_all,
REQUEST_BODY_BUFFER_ALL
);

#[inline]
pub fn is_web_browser_navigation(self) -> bool {
Expand Down
3 changes: 3 additions & 0 deletions src/runtime/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,9 @@ impl<const SSL: bool, const DEBUG: bool> NewServer<SSL, DEBUG> {
on_readable_stream_available: Some(
ServerRequestContext::<SSL, DEBUG>::on_request_body_readable_stream_available,
),
on_stream_drained: Some(
ServerRequestContext::<SSL, DEBUG>::on_request_body_stream_drained_callback,
),
..Default::default()
});
}
Expand Down
6 changes: 6 additions & 0 deletions src/runtime/server/server_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ pub(super) trait RequestCtxOps: RequestCtx {
global_this: &JSGlobalObject,
readable: WebCore::ReadableStream,
);
fn on_request_body_stream_drained_callback(this: Option<*mut c_void>);
}

impl<ThisServer, const SSL: bool, const DBG: bool, const H3: bool> RequestCtxOps
Expand Down Expand Up @@ -275,6 +276,10 @@ where
) {
Self::on_request_body_readable_stream_available(this, global_this, readable)
}
#[inline]
fn on_request_body_stream_drained_callback(this: Option<*mut c_void>) {
Self::on_request_body_stream_drained_callback(this)
}
}

// NOTE: local request/response trait so generic `Ctx::Req` / `Ctx::Resp`
Expand Down Expand Up @@ -3187,6 +3192,7 @@ where
on_readable_stream_available: Some(
Ctx::on_request_body_readable_stream_available,
),
on_stream_drained: Some(Ctx::on_request_body_stream_drained_callback),
..Default::default()
});
}
Expand Down
Loading
Loading