Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 5 additions & 13 deletions src/runtime/api/html_rewriter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,10 @@ impl RewriterPipe {
original: &Response,
sync_only_noun: Option<&'static str>,
) -> JsResult<JSValue> {
// Inherit status and headers (#3334); `finalize_without_stream` reads `get_fetch_headers()`, so materialize.
let mut init = original.clone_init(global)?;
init.materialize_headers(global)?;

let pipe = bun_core::heap::alloc_nn(RewriterPipe {
global: GlobalRef::from(global),
cell: Cell::new(JSValue::ZERO),
Expand Down Expand Up @@ -1023,10 +1027,7 @@ impl RewriterPipe {
// the sink buffers into `output_buffer`, and `on_start_streaming`
// hands that over as `DrainResult::Owned`.
let result = bun_core::heap::alloc_nn(Response::init(
webcore::response::Init {
status_code: 200,
..Default::default()
},
init,
webcore::Body::new({
let mut pv = webcore::body::PendingValue::new(global);
pv.task = Some(pipe.cast::<c_void>());
Expand All @@ -1044,15 +1045,6 @@ impl RewriterPipe {
this.response
.set(Some(unsafe { RefPtr::init_ref(result.as_ptr()) }));

result_ref.set_init(
original.get_method(),
original.get_init_status_code(),
original.get_init_status_text().clone(),
);

// https://github.com/oven-sh/bun/issues/3334
result_ref.set_init_headers(original.clone_init_headers(global)?);

let response_js_value = result_ref.to_js(&this.global);

// Hand ownership of `pipe` to its `JSHTMLRewriterTransform` wrapper cell.
Expand Down
4 changes: 4 additions & 0 deletions src/runtime/server/FileRoute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ impl FileRoute {
"expected blob not to be heap-allocated"
);
*body_value = BodyValue::Blob(blob.dupe());
if !response.pending_content_type().is_empty() {
// Folds it into the header list read below.
response.get_or_create_headers(global)?;
}
let headers = headers_from(response.get_init_headers(), &blob);
let status_code = response.status_code();

Expand Down
18 changes: 14 additions & 4 deletions src/runtime/server/RequestContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3799,8 +3799,11 @@ where
blob.size()
};

let (content_type, needs_content_type, content_type_needs_free) =
get_content_type(response.get_init_headers_mut(), blob);
let (content_type, needs_content_type, content_type_needs_free) = get_content_type(
response.get_init_headers_mut(),
response.pending_content_type(),
blob,
);
// NOTE: `MimeType` owns a `Cow<'static, [u8]>`; Drop handles the owned case.
// Hold the value past all reads below, then let it drop at scope end.
let _ct_guard = scopeguard::guard(content_type_needs_free, |_needs| {
Expand Down Expand Up @@ -4788,7 +4791,12 @@ impl<const DEBUG_MODE: bool> Flags<DEBUG_MODE> {
}
}

fn get_content_type(headers: Option<&mut FetchHeaders>, blob: &AnyBlob) -> (MimeType, bool, bool) {
/// Precedence: the header, `Response::pending_content_type`, then `blob` (which may be re-derived from a stream).
fn get_content_type(
headers: Option<&mut FetchHeaders>,
pending_content_type: &[u8],
blob: &AnyBlob,
) -> (MimeType, bool, bool) {
let mut needs_content_type = true;
let mut content_type_needs_free = false;

Expand All @@ -4810,7 +4818,9 @@ fn get_content_type(headers: Option<&mut FetchHeaders>, blob: &AnyBlob) -> (Mime
}
}

if !blob.content_type().is_empty() {
if !pending_content_type.is_empty() {
bun_http_types::MimeType::by_name(pending_content_type)
} else if !blob.content_type().is_empty() {
bun_http_types::MimeType::by_name(blob.content_type())
} else if let Some(content) = bun_http_types::MimeType::sniff(blob.slice()) {
content
Expand Down
3 changes: 3 additions & 0 deletions src/runtime/server/StaticRoute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ impl StaticRoute {
&bun_core::String::ascii(text_mime.value.as_ref()),
global_this,
)?;
} else if !response.pending_content_type().is_empty() {
// Folds it into the header list read below.
response.get_or_create_headers(global_this)?;
}

let mut headers: Headers = bun_http_jsc::headers_jsc::from_fetch_headers(
Expand Down
42 changes: 16 additions & 26 deletions src/runtime/webcore/Request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1157,18 +1157,13 @@ impl Request {
}

if !fields.contains(Fields::Headers) {
if let Some(headers) = response.get_init_headers_mut() {
// The flag is set unconditionally once `getInitHeaders()` yielded a
// value, even if `cloneThis` returns null — so a later arg can't
// repopulate headers from a different source.
match headers.clone_this(global_this) {
Ok(h) => {
// SAFETY: clone_this returns a +1 ref FetchHeaders.
req.headers.set(h.map(|p| unsafe { HeadersRef::adopt(p) }));
fields.insert(Fields::Headers);
}
Err(e) => bail!(Err(e)),
match response.clone_headers(global_this) {
Ok(Some(headers)) => {
req.headers.set(Some(headers));
fields.insert(Fields::Headers);
}
Ok(None) => {}
Err(e) => bail!(Err(e)),
}
}

Expand Down Expand Up @@ -1398,22 +1393,17 @@ impl Request {

req.url.set(href);

if matches!(req.body_value(), BodyValue::Blob(_)) && req.headers.get().is_some() {
if let BodyValue::Blob(blob) = req.body_value() {
let ct: &[u8] = blob.content_type_slice();
if !ct.is_empty()
&& !req
.headers_mut()
.as_mut()
.unwrap()
.fast_has(HTTPHeaderName::ContentType)
{
// Reshaped for borrowck — split borrow of req.body and req.headers
let ct_ptr: *const [u8] = ct;
match req.headers_mut().as_mut().unwrap().put(
// Fetch: append the body's `Content-Type` unless the header list has one (allocating the list).
if let BodyValue::Blob(blob) = req.body_value() {
let content_type = blob.content_type_slice();
if !content_type.is_empty() {
let headers = req
.headers_mut()
.get_or_insert_with(HeadersRef::create_empty);
if !headers.fast_has(HTTPHeaderName::ContentType) {
match headers.put(
HTTPHeaderName::ContentType,
// SAFETY: ct_ptr borrows req.body which is not mutated here.
&BunString::ascii(unsafe { &*ct_ptr }),
&BunString::ascii(content_type),
global_this,
) {
Ok(()) => {}
Expand Down
119 changes: 67 additions & 52 deletions src/runtime/webcore/Response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use bun_core::Output;
use bun_core::{String as BunString, Utf8Bytes};
use bun_http_types::Method::Method;

use super::blob::BlobContentType;
use super::body::{Body, BodyMixin, Value as BodyValue, ValueError as BodyValueError};
use super::{FetchHeaders, ReadableStream, Request};

Expand Down Expand Up @@ -310,11 +311,12 @@ impl BodyMixin for Response {

impl Response {
pub(crate) fn init(
response_init: Init,
mut response_init: Init,
body: Body,
url: BunString,
redirected: bool,
) -> Response {
response_init.capture_content_type(body.value.get());
Response {
init: JsCell::new(response_init),
body: JsCell::new(body),
Expand All @@ -324,29 +326,9 @@ impl Response {
}
}

#[inline]
pub(crate) fn set_init(&self, method: Method, status_code: u16, status_text: BunString) {
self.init.with_mut(|init| {
init.method = method;
init.status_code = status_code;
init.status_text = status_text;
});
}

#[inline]
pub(crate) fn set_init_headers(&self, headers: Option<HeadersRef>) {
// old headers dropped (HeadersRef::Drop derefs the C++ handle)
self.init.with_mut(|init| init.headers = headers);
}

#[inline]
pub(crate) fn get_init_status_code(&self) -> u16 {
self.init.get().status_code
}

#[inline]
pub(crate) fn get_init_status_text(&self) -> &BunString {
&self.init.get().status_text
/// Deep copy of `init`, including a pending body `Content-Type`.
pub(crate) fn clone_init(&self, global: &JSGlobalObject) -> JsResult<Init> {
self.init.get().clone(global)
}

#[inline]
Expand Down Expand Up @@ -389,18 +371,22 @@ impl Response {
self.init_mut().headers.as_deref_mut()
}

/// Deep-copy this response's init headers (if any) into a fresh
/// `HeadersRef`. Centralises the `FetchHeaders::clone_this` +
/// `HeadersRef::adopt` pair so callers stay `unsafe`-free.
#[inline]
pub(crate) fn clone_init_headers(
&self,
global: &JSGlobalObject,
) -> JsResult<Option<HeadersRef>> {
match self.init_mut().headers.as_ref() {
Some(headers) => headers.clone_this(global),
None => Ok(None),
/// Deep copy of the header list for another owner; a pending `Content-Type` goes into the copy only.
pub(crate) fn clone_headers(&self, global: &JSGlobalObject) -> JsResult<Option<HeadersRef>> {
let init = self.init.get();
if let Some(headers) = init.headers.as_ref() {
return headers.clone_this(global);
}
if init.pending_content_type.is_empty() {
return Ok(None);
}
let mut headers = HeadersRef::create_empty();
headers.put(
HTTPHeaderName::ContentType,
&BunString::ascii(init.pending_content_type.as_slice()),
global,
)?;
Ok(Some(headers))
}

#[inline]
Expand Down Expand Up @@ -593,37 +579,35 @@ impl Response {
// borrows `self.init`; callers (`get_headers`, `construct_*`) do not
// hold the borrow across calls that re-enter Response host-fns.
let init = self.init_mut();
if init.headers.is_none() {
init.headers = Some(HeadersRef::create_empty());

if let BodyValue::Blob(blob) = self.body.get().value.get() {
let content_type = blob.content_type_slice();
if !content_type.is_empty() {
init.headers.as_mut().unwrap().put(
HTTPHeaderName::ContentType,
&BunString::ascii(content_type),
global_this,
)?;
}
}
}

Ok(init.headers.as_mut().unwrap())
init.materialize_headers(global_this)?;
Ok(init.headers.get_or_insert_with(HeadersRef::create_empty))
}

pub(crate) fn get_headers(this: &Self, global_this: &JSGlobalObject) -> JsResult<JSValue> {
Ok(this.get_or_create_headers(global_this)?.to_js(global_this))
}

/// See [`Init::pending_content_type`]. Empty once the header list exists.
pub(crate) fn pending_content_type(&self) -> &[u8] {
self.init.get().pending_content_type.as_slice()
}

pub(crate) fn get_content_type(&self) -> JsResult<Option<Utf8Bytes<'_>>> {
// R-2 escape hatch via `init_mut()` — `fast_get` (FFI out-param write)
// does not re-enter JS.
if let Some(headers) = self.init_mut().headers.as_mut() {
let init = self.init_mut();
if let Some(headers) = init.headers.as_mut() {
if let Some(value) = headers.fast_get(HTTPHeaderName::ContentType) {
return Ok(Some(value.to_utf8()));
}
}

if !init.pending_content_type.is_empty() {
return Ok(Some(Utf8Bytes::Borrowed(
init.pending_content_type.as_slice(),
)));
}

if let BodyValue::Blob(blob) = self.body.get().value.get() {
let content_type = blob.content_type_slice();
if !content_type.is_empty() {
Expand Down Expand Up @@ -1166,6 +1150,7 @@ impl Response {
}
}
}
init.capture_content_type(body.value.get());

// Disarm: all fallible ops have succeeded.
let body = scopeguard::ScopeGuard::into_inner(body);
Expand Down Expand Up @@ -1194,6 +1179,8 @@ impl Response {
// the fields' own drop glue releases `headers` and `status_text`.
pub struct Init {
pub(crate) headers: Option<HeadersRef>,
/// The body's `Content-Type`, owed to `headers` (allocated lazily); non-empty only while `headers` is `None`.
pub(crate) pending_content_type: BlobContentType,
pub(crate) status_code: u16,
pub(crate) status_text: BunString,
pub method: Method,
Expand All @@ -1203,6 +1190,7 @@ impl Default for Init {
fn default() -> Self {
Self {
headers: None,
pending_content_type: BlobContentType::default(),
status_code: 0,
status_text: BunString::EMPTY,
method: Method::GET,
Expand All @@ -1221,12 +1209,39 @@ impl Init {
};
Ok(Init {
headers,
pending_content_type: self.pending_content_type.clone(),
status_code: self.status_code,
status_text: self.status_text.clone(),
method: self.method,
})
}

/// Park a `Blob` body's `Content-Type` while there is no header list; an already pending value wins.
pub(crate) fn capture_content_type(&mut self, body: &BodyValue) {
if self.headers.is_some() || !self.pending_content_type.is_empty() {
return;
}
if let BodyValue::Blob(blob) = body {
self.pending_content_type = blob.content_type.get().clone();
}
}

/// Allocate `headers` now if a `Content-Type` is pending.
pub(crate) fn materialize_headers(&mut self, global: &JSGlobalObject) -> JsResult<()> {
if self.headers.is_some() || self.pending_content_type.is_empty() {
return Ok(());
}
let mut headers = HeadersRef::create_empty();
headers.put(
HTTPHeaderName::ContentType,
&BunString::ascii(self.pending_content_type.as_slice()),
global,
)?;
self.pending_content_type = BlobContentType::default();
self.headers = Some(headers);
Ok(())
}

pub(crate) fn init(
global_this: &JSGlobalObject,
response_init: JSValue,
Expand Down
Loading
Loading