Skip to content
Merged
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
60 changes: 38 additions & 22 deletions src/http/HeaderValueIterator.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use bun_core::strings;

// Stores the remaining input slice and inlines the tokenize-by-',' logic in
// `next()` — the std iterator equivalent (`slice::Split` + `.filter(..)`) has
// an unnameable closure type that can't be stored as a field.
/// Iterates comma-separated HTTP header list tokens, trimming OWS and skipping empties.
pub struct HeaderValueIterator<'a> {
remaining: &'a [u8],
}
Expand All @@ -13,28 +11,46 @@ impl<'a> HeaderValueIterator<'a> {
remaining: strings::trim(input, b" \t"),
}
}
}

pub fn next(&mut self) -> Option<&'a [u8]> {
// tokenizeScalar semantics: skip leading delimiters, take until next delimiter.
while let Some((&b',', rest)) = self.remaining.split_first() {
self.remaining = rest;
}
if self.remaining.is_empty() {
return None;
}
let end = self
.remaining
.iter()
.position(|&b| b == b',')
.unwrap_or(self.remaining.len());
let token = &self.remaining[..end];
self.remaining = &self.remaining[end..];
impl<'a> Iterator for HeaderValueIterator<'a> {
type Item = &'a [u8];

let slice = strings::trim(token, b" \t");
if slice.is_empty() {
return self.next();
fn next(&mut self) -> Option<&'a [u8]> {
loop {
while let Some((&b',', rest)) = self.remaining.split_first() {
self.remaining = rest;
}
if self.remaining.is_empty() {
return None;
}
let end = strings::index_of_char(self.remaining, b',')
.map_or(self.remaining.len(), |i| i as usize);
let token = strings::trim(&self.remaining[..end], b" \t");
self.remaining = &self.remaining[end..];
if !token.is_empty() {
return Some(token);
}
}
}
}

Some(slice)
/// True if the request `Upgrade` header names any protocol other than `h2`/`h2c` (which we ignore).
pub fn upgrade_header_is_not_h2(value: &[u8]) -> bool {
HeaderValueIterator::init(value)
.any(|token| !strings::eql_any_case_insensitive_ascii(token, &[b"h2", b"h2c"]))
}

/// `Some(false)` if any token is `close` (wins), `Some(true)` if `keep-alive`, else `None`.
pub fn connection_header_keep_alive(value: &[u8]) -> Option<bool> {
let mut keep_alive = None;
for token in HeaderValueIterator::init(value) {
if strings::eql_case_insensitive_ascii_check_length(token, b"close") {
return Some(false);
}
if strings::eql_case_insensitive_ascii_check_length(token, b"keep-alive") {
keep_alive = Some(true);
}
}
keep_alive
}
115 changes: 42 additions & 73 deletions src/http/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ pub enum Protocol {
}

pub use bun_http_types::Encoding::Encoding;
pub use header_value_iterator::HeaderValueIterator;
pub use header_value_iterator::{
HeaderValueIterator, connection_header_keep_alive, upgrade_header_is_not_h2,
};
pub use init_error::InitError;

/// Cloned response metadata (headers + url + status). Ownership transfers to
Expand Down Expand Up @@ -2328,6 +2330,7 @@ impl<'a> HTTPClient<'a> {
let mut override_accept_header = false;
let mut override_host_header = false;
let mut override_connection_header = false;
let mut connection_close_requested = false;
let mut override_user_agent = false;
let mut add_transfer_encoding = true;
let mut original_content_length: Option<&[u8]> = None;
Expand Down Expand Up @@ -2359,17 +2362,15 @@ impl<'a> HTTPClient<'a> {
h if h == hash_header_const(b"Connection") => {
if will_append {
override_connection_header = true;
let connection_value = self.header_str(header_values[i]);
if bun_core::strings::eql_case_insensitive_ascii_check_length(
connection_value,
b"close",
) {
self.flags.disable_keepalive = true;
} else if bun_core::strings::eql_case_insensitive_ascii_check_length(
connection_value,
b"keep-alive",
) {
self.flags.disable_keepalive = false;
match connection_header_keep_alive(self.header_str(header_values[i])) {
Some(false) => {
connection_close_requested = true;
self.flags.disable_keepalive = true;
}
Some(true) if !connection_close_requested => {
self.flags.disable_keepalive = false;
}
_ => {}
}
}
}
Expand Down Expand Up @@ -2404,11 +2405,7 @@ impl<'a> HTTPClient<'a> {
}
h if h == hash_header_const(b"Upgrade") => {
if will_append {
let value = self.header_str(header_values[i]);
if !bun_core::strings::eql_any_case_insensitive_ascii(
value,
&[b"h2", b"h2c"],
) {
if upgrade_header_is_not_h2(self.header_str(header_values[i])) {
self.flags.upgrade_state = HTTPUpgradeState::Pending;
}
}
Expand Down Expand Up @@ -4752,6 +4749,7 @@ impl<'a> HTTPClient<'a> {
let mut location: &[u8] = b"";
let mut pretend_304 = false;
let mut is_server_sent_events = false;
let mut content_codings: u32 = 0;
for (header_i, header) in response.headers.list.iter().enumerate() {
match hash_header_name(header.name()) {
h if h == hash_header_const(b"Content-Length") => {
Expand Down Expand Up @@ -4800,25 +4798,21 @@ impl<'a> HTTPClient<'a> {
}
h if h == hash_header_const(b"Content-Encoding") => {
if !self.flags.disable_decompression {
// RFC 9110 §8.4.1: content codings are case-insensitive.
// `x-gzip` is a registered deprecated alias of `gzip`.
let value = header.value();
if strings::eql_case_insensitive_ascii_check_length(value, b"gzip")
|| strings::eql_case_insensitive_ascii_check_length(value, b"x-gzip")
{
self.state.encoding = Encoding::Gzip;
self.state.content_encoding_i = header_i as u8;
} else if strings::eql_case_insensitive_ascii_check_length(
value, b"deflate",
) {
self.state.encoding = Encoding::Deflate;
self.state.content_encoding_i = header_i as u8;
} else if strings::eql_case_insensitive_ascii_check_length(value, b"br") {
self.state.encoding = Encoding::Brotli;
self.state.content_encoding_i = header_i as u8;
} else if strings::eql_case_insensitive_ascii_check_length(value, b"zstd") {
self.state.encoding = Encoding::Zstd;
self.state.content_encoding_i = header_i as u8;
for token in HeaderValueIterator::init(header.value()) {
match Encoding::from_token(token) {
Some(Encoding::Identity) => {}
Some(coding) if coding.is_compressed() && content_codings == 0 => {
self.state.encoding = coding;
self.state.content_encoding_i = header_i as u8;
content_codings = 1;
}
// Stacked or unknown codings: we can only strip one layer, so pass through raw.
_ => {
self.state.encoding = Encoding::Identity;
self.state.content_encoding_i = u8::MAX;
content_codings = u32::MAX;
}
}
}
}
}
Expand All @@ -4832,52 +4826,27 @@ impl<'a> HTTPClient<'a> {
{
continue;
}
// RFC 9112 §7: transfer-coding names are case-insensitive.
let value = header.value();
if strings::eql_case_insensitive_ascii_check_length(value, b"gzip")
|| strings::eql_case_insensitive_ascii_check_length(value, b"x-gzip")
{
if !self.flags.disable_decompression {
self.state.transfer_encoding = Encoding::Gzip;
}
} else if strings::eql_case_insensitive_ascii_check_length(value, b"deflate") {
if !self.flags.disable_decompression {
self.state.transfer_encoding = Encoding::Deflate;
}
} else if strings::eql_case_insensitive_ascii_check_length(value, b"br") {
if !self.flags.disable_decompression {
self.state.transfer_encoding = Encoding::Brotli;
// RFC 9112 §6.1: `chunked`, if present, must be the final coding.
for token in HeaderValueIterator::init(header.value()) {
if self.state.transfer_encoding == Encoding::Chunked {
return Err(crate::Error::UnsupportedTransferEncoding);
}
} else if strings::eql_case_insensitive_ascii_check_length(value, b"zstd") {
if !self.flags.disable_decompression {
self.state.transfer_encoding = Encoding::Zstd;
match Encoding::from_token(token) {
Some(Encoding::Chunked) => {
self.state.transfer_encoding = Encoding::Chunked;
}
Some(_) => {}
None => return Err(crate::Error::UnsupportedTransferEncoding),
}
} else if strings::eql_case_insensitive_ascii_check_length(value, b"identity") {
self.state.transfer_encoding = Encoding::Identity;
} else if strings::eql_case_insensitive_ascii_check_length(value, b"chunked") {
self.state.transfer_encoding = Encoding::Chunked;
} else {
return Err(crate::Error::UnsupportedTransferEncoding);
}
}
h if h == hash_header_const(b"Location") => {
location = header.value();
}
h if h == hash_header_const(b"Connection") => {
// `close` applies on any status (RFC 9112 §9.6); only an
// explicit `keep-alive` is gated on a 2xx success.
if bun_core::strings::eql_case_insensitive_ascii_check_length(
header.value(),
b"close",
) {
// `close` on any field line, any status, is sticky (RFC 9110 §5.3, RFC 9112 §9.6).
if connection_header_keep_alive(header.value()) == Some(false) {
self.state.flags.allow_keepalive = false;
} else if (200..=299).contains(&response.status_code)
&& bun_core::strings::eql_case_insensitive_ascii_check_length(
header.value(),
b"keep-alive",
)
{
self.state.flags.allow_keepalive = true;
}
Comment thread
claude[bot] marked this conversation as resolved.
}
h if h == hash_header_const(b"Last-Modified") => {
Expand Down
28 changes: 13 additions & 15 deletions src/http_jsc/websocket_client/WebSocketUpgradeClient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1307,7 +1307,8 @@ impl<const SSL: bool> HTTPClient<SSL> {
remain_buf: &[u8],
) {
let mut upgrade_header = picohttp::Header::ZERO;
let mut connection_header = picohttp::Header::ZERO;
let mut connection_header_seen = false;
let mut connection_has_upgrade = false;
let mut websocket_accept_header = picohttp::Header::ZERO;
let mut protocol_header_seen = false;

Expand All @@ -1323,13 +1324,15 @@ impl<const SSL: bool> HTTPClient<SSL> {
for header in response.headers.list {
match header.name().len() {
len if len == b"Connection".len() => {
if connection_header.name().is_empty()
&& strings::eql_case_insensitive_ascii_ignore_length(
header.name(),
b"Connection",
)
{
connection_header = *header;
if strings::eql_case_insensitive_ascii_ignore_length(
header.name(),
b"Connection",
) {
connection_header_seen = true;
connection_has_upgrade |=
HeaderValueIterator::init(header.value()).any(|t| {
strings::eql_case_insensitive_ascii_check_length(t, b"upgrade")
});
}
}
len if len == b"Upgrade".len() => {
Expand Down Expand Up @@ -1524,12 +1527,7 @@ impl<const SSL: bool> HTTPClient<SSL> {
return;
}

if connection_header
.name()
.len()
.min(connection_header.value().len())
== 0
{
if !connection_header_seen {
// SAFETY: no `&mut Self` is live across this call.
unsafe { Self::terminate(this, ErrorCode::MissingConnectionHeader) };
return;
Expand All @@ -1553,7 +1551,7 @@ impl<const SSL: bool> HTTPClient<SSL> {
return;
}

if !strings::eql_case_insensitive_ascii(connection_header.value(), b"Upgrade", true) {
if !connection_has_upgrade {
// SAFETY: no `&mut Self` is live across this call.
unsafe { Self::terminate(this, ErrorCode::InvalidConnectionHeader) };
return;
Expand Down
20 changes: 20 additions & 0 deletions src/http_types/Encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,26 @@ pub enum Encoding {
}

impl Encoding {
/// Parses one case-insensitive coding token (RFC 9110 §8.4.1 / RFC 9112 §7); `None` if unrecognized.
pub fn from_token(token: &[u8]) -> Option<Encoding> {
use bun_core::strings::eql_case_insensitive_ascii_check_length as eql;
if eql(token, b"gzip") || eql(token, b"x-gzip") {
Some(Encoding::Gzip)
} else if eql(token, b"deflate") {
Some(Encoding::Deflate)
} else if eql(token, b"br") {
Some(Encoding::Brotli)
} else if eql(token, b"zstd") {
Some(Encoding::Zstd)
} else if eql(token, b"identity") {
Some(Encoding::Identity)
} else if eql(token, b"chunked") {
Some(Encoding::Chunked)
} else {
None
}
}

pub fn can_use_lib_deflate(self) -> bool {
matches!(self, Encoding::Gzip | Encoding::Deflate)
}
Expand Down
5 changes: 1 addition & 4 deletions src/runtime/webcore/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1388,10 +1388,7 @@ fn fetch_impl<const ALLOW_GET_BODY: bool>(
}

if let Some(upgrade_) = headers_ref.fast_get(HTTPHeaderName::Upgrade) {
let upgrade = upgrade_.to_slice();
// `defer upgrade.deinit()` → Drop.
let slice = upgrade.slice();
if slice != b"h2" && slice != b"h2c" {
if http::upgrade_header_is_not_h2(upgrade_.to_slice().slice()) {
upgraded_connection = true;
}
}
Expand Down
Loading
Loading