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
9 changes: 9 additions & 0 deletions packages/bun-uws/src/Http3Context.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ struct Http3Context {
rd->reset();

Http3Request req(s);
/* RFC 9114 §4.3.1: :path is the path and query of the target URI, or "*"
* for OPTIONS; anything else is malformed (§4.1.2). HttpRouter assumes a
* leading '/', and HttpParser rejects asterisk-form for HTTP/1 as well,
* so only origin-form is routed. */
std::string_view fullUrl = req.getFullUrl();
if (fullUrl.empty() || fullUrl[0] != '/') {
res->writeStatus("400 Bad Request")->end();
return;
}
if (req.getHeader("expect") == "100-continue") res->writeContinue();
cd->router.getUserData() = {res, &req};
if (!cd->router.route(req.getMethod(), req.getUrl())) {
Expand Down
40 changes: 9 additions & 31 deletions src/runtime/server/server_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,22 +280,22 @@ where
// touching `bun_uws_sys`. Only the surface `prepare_js_request_context_for`
// actually needs is exposed.
trait ReqLike {
fn header(&mut self, name: &[u8]) -> Option<&[u8]>;
fn header(&self, name: &[u8]) -> Option<&[u8]>;
fn method(&mut self) -> &[u8];
fn url(&mut self) -> &[u8];
fn url(&self) -> &[u8];
fn set_yield(&mut self, y: bool);
}
impl ReqLike for uws_sys::Request {
#[inline]
fn header(&mut self, name: &[u8]) -> Option<&[u8]> {
fn header(&self, name: &[u8]) -> Option<&[u8]> {
uws_sys::Request::header(self, name)
}
#[inline]
fn method(&mut self) -> &[u8] {
uws_sys::Request::method(self)
}
#[inline]
fn url(&mut self) -> &[u8] {
fn url(&self) -> &[u8] {
uws_sys::Request::url(self)
}
#[inline]
Expand All @@ -305,15 +305,15 @@ impl ReqLike for uws_sys::Request {
}
impl ReqLike for uws_sys::h3::Request {
#[inline]
fn header(&mut self, name: &[u8]) -> Option<&[u8]> {
fn header(&self, name: &[u8]) -> Option<&[u8]> {
uws_sys::h3::Request::header(self, name)
}
#[inline]
fn method(&mut self) -> &[u8] {
uws_sys::h3::Request::method(self)
}
#[inline]
fn url(&mut self) -> &[u8] {
fn url(&self) -> &[u8] {
uws_sys::h3::Request::url(self)
}
#[inline]
Expand Down Expand Up @@ -3270,31 +3270,9 @@ where
std::ptr::from_mut(req).cast::<c_void>(),
))
}));
// NOTE: `ReqLike::{url,header}` both borrow `&mut req`; the
// returned slices alias the same uWS-owned header buffer. Format
// the `https://{host}` prefix while `host` is borrowed so the
// second `&mut req` borrow for `url` is unconflicted.
let prefix: Option<Vec<u8>> = ReqLike::header(req, b"host").map(|host| {
let fmt = bun_fmt::HostFormatter {
is_https: true,
host,
port: None,
};
let mut s = Vec::new();
write!(&mut s, "https://{}", fmt).ok();
s
});
let path = ReqLike::url(req);
if !path.is_empty() && path[0] == b'/' {
if let Some(mut s) = prefix {
s.extend_from_slice(path);
request_object.url.set(BunString::clone_utf8(&s));
} else {
request_object.url.set(BunString::clone_utf8(path));
}
} else {
request_object.url.set(BunString::clone_utf8(path));
}
// uWS surfaces `:authority` as the `host` header.
request_object
.set_url_from_request_target(ReqLike::header(req, b"host"), ReqLike::url(req));
ctx.clear_req();
}

Expand Down
161 changes: 72 additions & 89 deletions src/runtime/webcore/Request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -817,19 +817,10 @@ impl Request {
// S008: `uws::Request` is an `opaque_ffi!` ZST handle — safe deref.
let req = bun_opaque::opaque_deref(req);
let req_url = Self::request_target_path(req.url());
if !req_url.is_empty() && req_url[0] == b'/' {
if let Some(host) = req
.header(b"host")
.filter(|host| Self::is_valid_host_header(host))
{
// With `port: None`, HostFormatter always emits exactly `host`, so the
// formatted byte-count is just `host.len()`. Avoid the `core::fmt::write`
// vtable dispatch that `bun_fmt::count(format_args!(...))` incurs — this
// runs once per request via JSC extra-memory accounting.
return self.get_protocol().len() + host.len() + req_url.len();
}
}
return req_url.len();
return match Self::url_authority(req.header(b"host"), &req_url) {
Some(host) => self.get_protocol().len() + host.len() + req_url.len(),
None => req_url.len(),
};
}

0
Expand Down Expand Up @@ -869,9 +860,7 @@ impl Request {
}
}

/// RFC 3986 3.2.2 `uri-host [ ":" port ]` byte set. A Host value outside it, or an empty
/// one, cannot form a URL authority, so `request.url` synthesis falls back to the
/// configured host instead of pasting the client bytes into the URL.
/// RFC 3986 3.2.2 `uri-host [ ":" port ]` byte set; anything else cannot be a URL authority.
fn is_valid_host_header(host: &[u8]) -> bool {
!host.is_empty()
&& host.iter().all(|&c| {
Expand Down Expand Up @@ -900,6 +889,15 @@ impl Request {
})
}

/// `Some(host)` when `request.url` gets an authority; `None` leaves it as the bare target.
fn url_authority<'a>(host: Option<&'a [u8]>, path: &[u8]) -> Option<&'a [u8]> {
if !path.is_empty() && path[0] == b'/' {
host.filter(|host| Self::is_valid_host_header(host))
} else {
None
}
}

pub(crate) fn ensure_url(&self) -> Result<(), AllocError> {
if !self.url.get().is_empty() {
return Ok(());
Expand All @@ -908,86 +906,71 @@ impl Request {
if let Some(req) = self.request_context.get_request() {
// S008: `uws::Request` is an `opaque_ffi!` ZST handle — safe deref.
let req = bun_opaque::opaque_deref(req);
let req_url = Self::request_target_path(req.url());
if !req_url.is_empty() && req_url[0] == b'/' {
if let Some(host) = req
.header(b"host")
.filter(|host| Self::is_valid_host_header(host))
{
// With `port: None`, HostFormatter always emits exactly `host`. Compute the
// length and assemble the URL with straight slice copies instead of going
// through `core::fmt::write` (which is not monomorphized and shows up in
// per-request profiles).
let protocol = self.get_protocol();
let url_bytelength = protocol.len() + host.len() + req_url.len();

debug_assert!(self.size_of_url() == url_bytelength);

if url_bytelength < 128 {
let mut buffer = [0u8; 128];
let url = {
let mut at = 0;
buffer[at..at + protocol.len()].copy_from_slice(protocol);
at += protocol.len();
buffer[at..at + host.len()].copy_from_slice(host);
at += host.len();
buffer[at..at + req_url.len()].copy_from_slice(&req_url);
at += req_url.len();
&buffer[..at]
};

debug_assert!(self.size_of_url() == url.len());

let href = bun_url::href_from_string(&BunString::from_bytes(url));
if !href.is_empty() {
if core::ptr::eq(href.byte_slice().as_ptr(), url.as_ptr()) {
self.url.set(BunString::clone_latin1(&url[..href.length()]));
href.deref();
} else {
self.url.set(href);
}
} else {
// TODO: what is the right thing to do for invalid URLS?
self.url.set(BunString::clone_utf8(url));
}

return Ok(());
}
self.set_url_from_request_target(req.header(b"host"), req.url());
}
Ok(())
}

if strings::is_all_ascii(host) && strings::is_all_ascii(&req_url) {
let (new_url, bytes) =
BunString::create_uninitialized_latin1(url_bytelength);
self.url.set(new_url);
// exact space was counted above
let (a, rest) = bytes.split_at_mut(protocol.len());
let (b, c) = rest.split_at_mut(host.len());
a.copy_from_slice(protocol);
b.copy_from_slice(host);
c.copy_from_slice(&req_url);
} else {
// slow path
let mut temp_url: Vec<u8> = Vec::with_capacity(url_bytelength);
temp_url.extend_from_slice(protocol);
temp_url.extend_from_slice(host);
temp_url.extend_from_slice(&req_url);
// `defer bun.default_allocator.free(temp_url)` → Vec drops at scope end
self.url.set(BunString::clone_utf8(&temp_url));
}
/// Used by both HTTP/1 (lazily, via `ensure_url`) and HTTP/3 (eagerly, by the server).
pub(crate) fn set_url_from_request_target(&self, host: Option<&[u8]>, target: &[u8]) {
let req_url = Self::request_target_path(target);
let Some(host) = Self::url_authority(host, &req_url) else {
self.url.set(BunString::clone_utf8(&req_url));
return;
};

let href = bun_url::href_from_string(&self.url.get());
// TODO: what is the right thing to do for invalid URLS?
if !href.is_empty() {
self.url.set(href);
}
// Hand-assembled: `core::fmt::write` is not monomorphized and shows up in profiles.
let protocol = self.get_protocol();
let url_bytelength = protocol.len() + host.len() + req_url.len();

if url_bytelength < 128 {
let mut buffer = [0u8; 128];
let url = {
let mut at = 0;
buffer[at..at + protocol.len()].copy_from_slice(protocol);
at += protocol.len();
buffer[at..at + host.len()].copy_from_slice(host);
at += host.len();
buffer[at..at + req_url.len()].copy_from_slice(&req_url);
at += req_url.len();
&buffer[..at]
};

return Ok(());
let href = bun_url::href_from_string(&BunString::from_bytes(url));
if !href.is_empty() {
if core::ptr::eq(href.byte_slice().as_ptr(), url.as_ptr()) {
self.url.set(BunString::clone_latin1(&url[..href.length()]));
href.deref();
} else {
self.url.set(href);
}
} else {
self.url.set(BunString::clone_utf8(url));
}
return;
}

debug_assert!(self.size_of_url() == req_url.len());
self.url.set(BunString::clone_utf8(&req_url));
if strings::is_all_ascii(host) && strings::is_all_ascii(&req_url) {
let (new_url, bytes) = BunString::create_uninitialized_latin1(url_bytelength);
self.url.set(new_url);
// exact space was counted above
let (a, rest) = bytes.split_at_mut(protocol.len());
let (b, c) = rest.split_at_mut(host.len());
a.copy_from_slice(protocol);
b.copy_from_slice(host);
c.copy_from_slice(&req_url);
} else {
let mut temp_url: Vec<u8> = Vec::with_capacity(url_bytelength);
temp_url.extend_from_slice(protocol);
temp_url.extend_from_slice(host);
temp_url.extend_from_slice(&req_url);
self.url.set(BunString::clone_utf8(&temp_url));
}

let href = bun_url::href_from_string(&self.url.get());
if !href.is_empty() {
self.url.set(href);
}
Ok(())
}
}

Expand Down
8 changes: 4 additions & 4 deletions src/uws_sys/h3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ impl Request {
pub fn set_yield(&mut self, y: bool) {
c::uws_h3_req_set_yield(self, y)
}
pub fn url(&mut self) -> &[u8] {
pub fn url(&self) -> &[u8] {
let mut p: *const u8 = ptr::null();
let n = c::uws_h3_req_get_url(self, &mut p);
// SAFETY: uws returns a pointer+len pair valid for the lifetime of the request
Expand All @@ -61,7 +61,7 @@ impl Request {
// SAFETY: uws returns a pointer+len pair valid for the lifetime of the request
unsafe { bun_core::ffi::slice(p, n) }
}
pub fn header(&mut self, name: &[u8]) -> Option<&[u8]> {
pub fn header(&self, name: &[u8]) -> Option<&[u8]> {
let mut p: *const u8 = ptr::null();
// SAFETY: self is a live FFI handle; name ptr/len valid for read; out-ptr is a valid local
let n = unsafe { c::uws_h3_req_get_header(self, name.as_ptr(), name.len(), &raw mut p) };
Expand Down Expand Up @@ -733,10 +733,10 @@ mod c {
// Out-param `out` is `&mut *const u8` (non-null, valid for write); the C
// shim only stores a pointer into request-owned storage and returns its
// length — no read-through precondition, so `safe fn`.
pub(super) safe fn uws_h3_req_get_url(req: &mut Request, out: &mut *const u8) -> usize;
pub(super) safe fn uws_h3_req_get_url(req: &Request, out: &mut *const u8) -> usize;
pub(super) safe fn uws_h3_req_get_method(req: &mut Request, out: &mut *const u8) -> usize;
pub(super) fn uws_h3_req_get_header(
req: *mut Request,
req: *const Request,
name: *const u8,
len: usize,
out: *mut *const u8,
Expand Down
Loading