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
20 changes: 16 additions & 4 deletions src/runtime/server/FileRoute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub struct FileRoute {
blob: Blob,
headers: Headers,
status_code: u16,
status_text: Box<[u8]>,
// Mutated on every request (`on()` runs `hash()`); FileRoute is reached via
// a shared `*const Self` from the route table, so wrap for interior
// mutability. `StatHash` is small POD with `Default`, so `Cell` +
Expand Down Expand Up @@ -85,6 +86,7 @@ impl FileRoute {
size_of::<FileRoute>()
+ self.headers.memory_cost()
+ self.blob.reported_estimated_size.get()
+ self.status_text.len()
}

pub fn last_modified_date(&self) -> JsResult<Option<u64>> {
Expand Down Expand Up @@ -127,6 +129,7 @@ impl FileRoute {
blob,
headers,
status_code: opts.status_code,
status_text: Box::default(),
stat_hash: Cell::new(StatHash::default()),
}))
}
Expand Down Expand Up @@ -176,6 +179,8 @@ impl FileRoute {
*body_value = BodyValue::Blob(blob.dupe());
let headers = headers_from(response.get_init_headers(), &blob);
let status_code = response.status_code();
let status_text_str = response.get_init_status_text();
let status_text_slice = status_text_str.to_utf8_without_ref();

return Ok(Some(bun_core::heap::into_raw(Box::new(FileRoute {
ref_count: Cell::new(1),
Expand All @@ -187,6 +192,7 @@ impl FileRoute {
blob,
headers,
status_code,
status_text: Box::from(status_text_slice.slice()),
stat_hash: Cell::new(StatHash::default()),
}))));
}
Expand All @@ -210,6 +216,7 @@ impl FileRoute {
has_content_range_header: false,
has_date_header: false,
status_code: 200,
status_text: Box::default(),
stat_hash: Cell::new(StatHash::default()),
}))));
}
Expand Down Expand Up @@ -273,10 +280,10 @@ impl FileRoute {
}
}

fn write_status_code(&self, status: u16, resp: AnyResponse) {
fn write_status_code(&self, status: u16, status_text: &[u8], resp: AnyResponse) {
match resp {
AnyResponse::SSL(r) => write_status::<true>(r, status),
AnyResponse::TCP(r) => write_status::<false>(r, status),
AnyResponse::SSL(r) => write_status::<true>(r, status, status_text),
AnyResponse::TCP(r) => write_status::<false>(r, status, status_text),
AnyResponse::H3(r) => {
let mut b = bun_core::fmt::ItoaBuf::new();
let s = bun_core::fmt::itoa(&mut b, status);
Expand Down Expand Up @@ -520,7 +527,12 @@ impl FileRoute {

req.set_yield(false);

this.write_status_code(status_code, resp);
let status_text: &[u8] = if status_code == this.status_code {
&this.status_text
} else {
&[]
};
this.write_status_code(status_code, status_text, resp);
if this.has_date_header {
resp.mark_wrote_date_header();
}
Expand Down
1 change: 1 addition & 0 deletions src/runtime/server/HTMLBundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,7 @@ impl Route {
blob,
server: Cell::new(Some(server)),
status_code: 200,
status_text: Box::default(),
headers,
cached_blob_size,
has_date: false,
Expand Down
29 changes: 29 additions & 0 deletions src/runtime/server/HTTPStatusText.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,35 @@ pub const fn is_sendable(code: u16) -> bool {
matches!(code, 100..=999)
}

/// RFC 9112 §4 `reason-phrase`: HTAB / SP / VCHAR (0x21..=0x7E) / obs-text (0x80..=0xFF).
pub fn is_valid_reason_phrase(s: &[u8]) -> bool {
s.iter()
.all(|&c| c == b'\t' || (0x20..=0x7E).contains(&c) || c >= 0x80)
}

pub const STATUS_LINE_BUF: usize = 256;

/// `"<code> <reason>"` for uWS `writeStatus`: prefers a valid `status_text`,
/// then [`get`], then an empty reason phrase.
Comment thread
robobun marked this conversation as resolved.
pub fn format<'a>(buf: &'a mut [u8; STATUS_LINE_BUF], code: u16, status_text: &[u8]) -> &'a [u8] {
let mut itoa = bun_core::fmt::ItoaBuf::new();
let c = bun_core::fmt::itoa(&mut itoa, code);
if !status_text.is_empty() && is_valid_reason_phrase(status_text) {
let msg = &status_text[..status_text.len().min(buf.len() - c.len() - 1)];
let n = c.len() + 1 + msg.len();
buf[..c.len()].copy_from_slice(c);
buf[c.len()] = b' ';
buf[c.len() + 1..n].copy_from_slice(msg);
return &buf[..n];
}
if let Some(canned) = get(code) {
return canned;
}
buf[..c.len()].copy_from_slice(c);
buf[c.len()] = b' ';
&buf[..c.len() + 1]
Comment thread
robobun marked this conversation as resolved.
}

pub fn get(code: u16) -> Option<&'static [u8]> {
match code {
100 => Some(b"100 Continue"),
Expand Down
31 changes: 17 additions & 14 deletions src/runtime/server/RequestContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1860,7 +1860,7 @@ where
fd.close();
}
let mut crbuf = [0u8; RangeRequest::CONTENT_RANGE_BUF];
self.do_write_status(416);
self.do_write_status(416, &[]);
if let Some(response) = self.response_weakref.get() {
if let Some(mut headers_) = response.swap_init_headers() {
self.do_write_headers(&mut headers_);
Expand Down Expand Up @@ -3613,6 +3613,9 @@ where
// an async hop keep the Response rooted via response_protected.
let response: &mut Response = self.response_weakref.get().unwrap();
let mut status = response.status_code();
// +0 borrow of `init.status_text`; that field is not mutated below.
let status_text_str = response.get_init_status_text();
let status_text_slice = status_text_str.to_utf8_without_ref();
let mut needs_content_range = self.flags.needs_content_range()
&& (self.sendfile.total > 0 || self.sendfile.remain < self.blob.size());

Expand Down Expand Up @@ -3644,7 +3647,14 @@ where
status = 206;
}

self.do_write_status(status);
self.do_write_status(
status,
if needs_content_range {
&[]
} else {
status_text_slice.slice()
},
);
self.do_write_headers(&mut headers_);
// `HeadersRef` is RAII — its Drop
// already calls `WebCore__FetchHeaders__deref`, so an explicit
Expand All @@ -3653,9 +3663,9 @@ where
drop(headers_);
} else if needs_content_range {
status = 206;
self.do_write_status(status);
self.do_write_status(status, &[]);
} else {
self.do_write_status(status);
self.do_write_status(status, status_text_slice.slice());
}

if let Some(mut cookies) = self.cookies.take() {
Expand Down Expand Up @@ -3754,21 +3764,14 @@ where
}
}

fn do_write_status(&mut self, status: u16) {
fn do_write_status(&mut self, status: u16, status_text: &[u8]) {
debug_assert!(!self.flags.has_written_status());
self.flags.set_has_written_status(true);

// `AnyResponse` is a `Copy` handle; methods take `self` by value.
let Some(resp) = self.resp else { return };
if let Some(text) = HTTPStatusText::get(status) {
resp.write_status(text);
} else {
let mut buf = [0u8; 48];
let mut w = &mut buf[..];
let _ = write!(w, "{} HM", status);
let written = 48 - w.len();
resp.write_status(&buf[..written]);
}
let mut buf = [0u8; HTTPStatusText::STATUS_LINE_BUF];
resp.write_status(HTTPStatusText::format(&mut buf, status, status_text));
}

fn do_write_headers(&mut self, headers: &mut FetchHeaders) {
Expand Down
23 changes: 16 additions & 7 deletions src/runtime/server/StaticRoute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub struct StaticRoute {
pub(super) ref_count: Cell<u32>,
pub server: Cell<Option<AnyServer>>,
pub status_code: u16,
pub status_text: Box<[u8]>,
pub blob: AnyBlob,
pub cached_blob_size: u64,
pub has_date: bool,
Expand Down Expand Up @@ -111,6 +112,7 @@ impl StaticRoute {
headers,
server: Cell::new(options.server),
status_code: options.status_code,
status_text: Box::default(),
}))
}

Expand Down Expand Up @@ -142,11 +144,15 @@ impl StaticRoute {
headers: self.headers.clone(),
server: Cell::new(self.server.get()),
status_code: self.status_code,
status_text: self.status_text.clone(),
})))
}

pub fn memory_cost(&self) -> usize {
size_of::<StaticRoute>() + self.blob.memory_cost() + self.headers.memory_cost()
size_of::<StaticRoute>()
+ self.blob.memory_cost()
+ self.headers.memory_cost()
+ self.status_text.len()
}

pub fn from_js(
Expand Down Expand Up @@ -245,6 +251,8 @@ impl StaticRoute {

let cached_blob_size = blob.size();
let has_date = headers.get(b"date").is_some();
let status_text_str = response.get_init_status_text();
let status_text_slice = status_text_str.to_utf8_without_ref();
return Ok(Some(bun_core::heap::into_raw(Box::new(StaticRoute {
ref_count: Cell::new(1),
blob,
Expand All @@ -253,6 +261,7 @@ impl StaticRoute {
headers,
server: Cell::new(None),
status_code: response.status_code(),
status_text: Box::from(status_text_slice.slice()),
}))));
}

Expand Down Expand Up @@ -467,10 +476,10 @@ impl StaticRoute {
resp.try_end(bytes, all_bytes.len(), resp.should_close_connection())
}

fn do_write_status(&self, status: u16, resp: AnyResponse) {
fn do_write_status(&self, status: u16, status_text: &[u8], resp: AnyResponse) {
match resp {
AnyResponse::SSL(r) => write_status::<true>(r, status),
AnyResponse::TCP(r) => write_status::<false>(r, status),
AnyResponse::SSL(r) => write_status::<true>(r, status, status_text),
AnyResponse::TCP(r) => write_status::<false>(r, status, status_text),
AnyResponse::H3(r) => {
let mut b = bun_core::fmt::ItoaBuf::new();
let s = bun_core::fmt::itoa(&mut b, status);
Expand Down Expand Up @@ -513,7 +522,7 @@ impl StaticRoute {
}

fn render_metadata(&self, resp: AnyResponse) {
self.do_write_status(self.status_code, resp);
self.do_write_status(self.status_code, &self.status_text, resp);
self.do_write_headers(resp);
}

Expand All @@ -526,7 +535,7 @@ impl StaticRoute {
Method::GET => Self::on(this, resp),
Method::HEAD => Self::on_head(this, resp),
_ => {
(*this).do_write_status(405, resp); // Method not allowed
(*this).do_write_status(405, &[], resp); // Method not allowed
resp.write_header(b"Allow", b"GET, HEAD");
resp.write_header_int(b"Content-Length", 0);
resp.end_without_body(resp.should_close_connection());
Expand Down Expand Up @@ -622,7 +631,7 @@ impl StaticRoute {
server.on_pending_request();
resp.timeout(server.config().idle_timeout);
}
(*this).do_write_status(status, resp);
(*this).do_write_status(status, &[], resp);
(*this).do_write_headers(resp);
if !HTTPStatusText::is_null_body(status) {
resp.write_header_int(b"Content-Length", 0);
Expand Down
18 changes: 7 additions & 11 deletions src/runtime/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,11 @@ pub use server_body::{
};

// ─── write_status ────────────────────────────────────────────────────────────
pub fn write_status<const SSL: bool>(resp: *mut uws_sys::NewAppResponse<SSL>, status: u16) {
pub fn write_status<const SSL: bool>(
resp: *mut uws_sys::NewAppResponse<SSL>,
status: u16,
status_text: &[u8],
) {
// The route handlers (`StaticRoute`/`FileRoute`) call here from completion
// paths where the request may already be aborted/detached, so no-op on null.
if resp.is_null() {
Expand All @@ -120,16 +124,8 @@ pub fn write_status<const SSL: bool>(resp: *mut uws_sys::NewAppResponse<SSL>, st
// S008: `Response<SSL>` is a ZST opaque — safe `*mut → &mut` deref
// (non-null checked above).
let resp = bun_opaque::opaque_deref_mut(resp);
if let Some(text) = HTTPStatusText::get(status) {
resp.write_status(text);
} else {
use std::io::Write as _;
let mut buf = [0u8; 48];
let mut cursor = &mut buf[..];
write!(cursor, "{} HM", status).expect("unreachable");
let written = 48 - cursor.len();
resp.write_status(&buf[..written]);
}
let mut buf = [0u8; HTTPStatusText::STATUS_LINE_BUF];
resp.write_status(HTTPStatusText::format(&mut buf, status, status_text));
}

// ─── AnyRoute ────────────────────────────────────────────────────────────────
Expand Down
2 changes: 1 addition & 1 deletion test/js/bun/http/serve-stream-body-error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ test.skipIf(!isASAN)(

// The stream errors after "EB" is on the wire, so the body is force-closed
// without the terminating 0\r\n\r\n chunk (RFC 9112 section 7).
const expected = Array(6).fill({ status: "HTTP/1.1 597 HM", terminated: false });
const expected = Array(6).fill({ status: "HTTP/1.1 597 ", terminated: false });
expect({ stderr, results: stdout.trim() ? JSON.parse(stdout) : stdout, exitCode }).toEqual({
stderr: "",
results: expected,
Expand Down
Loading
Loading