Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
26 changes: 24 additions & 2 deletions src/jsc/bindings/NodeHTTP.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,9 @@ static void writeFetchHeadersToUWSResponse(WebCore::FetchHeaders& headers, uWS::
auto& internalHeaders = headers.internalHeaders();

for (auto& value : internalHeaders.getSetCookieHeaders()) {

if (value.isEmpty()) {
continue;
}
if (value.is8Bit()) {
const auto valueSpan = value.span8();
res->writeHeader(std::string_view("set-cookie", 10), std::string_view(reinterpret_cast<const char*>(valueSpan.data()), valueSpan.size()));
Expand All @@ -795,6 +797,15 @@ static void writeFetchHeadersToUWSResponse(WebCore::FetchHeaders& headers, uWS::
const auto& name = WebCore::httpHeaderNameString(header.key);
const auto& value = header.value;

// FetchHeaders strips leading/trailing HTTP whitespace, so an empty value here
// covers `""` and whitespace-only. Treat it as absent: don't write the line and
// don't set the wrote-this-header state bits, so the matching auto-header (Date,
// Content-Type from render_metadata) is emitted exactly once instead of alongside
// an empty duplicate.
if (value.isEmpty()) {
continue;
}
Comment thread
robobun marked this conversation as resolved.

// We have to tell uWS not to automatically insert a TransferEncoding or Date header.
// Otherwise, you get this when using Fastify;
//
Expand Down Expand Up @@ -843,7 +854,9 @@ static void writeFetchHeadersToUWSResponse(WebCore::FetchHeaders& headers, uWS::
for (auto& header : internalHeaders.uncommonHeaders()) {
const auto& name = header.key;
const auto& value = header.value;

if (value.isEmpty()) {
continue;
}
Comment thread
robobun marked this conversation as resolved.
writeResponseHeader<isSSL>(res, name, value);
}
}
Expand Down Expand Up @@ -1523,6 +1536,9 @@ static void writeFetchHeadersToH3Response(WebCore::FetchHeaders& headers, uWS::H
};

for (auto& value : internalHeaders.getSetCookieHeaders()) {
if (value.isEmpty()) {
continue;
}
if (value.is8Bit()) {
const auto s = value.span8();
res->writeHeader(std::string_view("set-cookie", 10), std::string_view(reinterpret_cast<const char*>(s.data()), s.size()));
Expand All @@ -1533,6 +1549,9 @@ static void writeFetchHeadersToH3Response(WebCore::FetchHeaders& headers, uWS::H
}

for (const auto& header : internalHeaders.commonHeaders()) {
if (header.value.isEmpty()) {
continue;
}
if (header.key == WebCore::HTTPHeaderName::ContentLength) {
if (!(data->state & uWS::Http3ResponseData::HTTP_WROTE_CONTENT_LENGTH_HEADER)) {
data->state |= uWS::Http3ResponseData::HTTP_WROTE_CONTENT_LENGTH_HEADER;
Expand All @@ -1548,6 +1567,9 @@ static void writeFetchHeadersToH3Response(WebCore::FetchHeaders& headers, uWS::H
}

for (auto& header : internalHeaders.uncommonHeaders()) {
if (header.value.isEmpty()) {
continue;
}
writeOne(header.key, header.value);
}
}
Expand Down
33 changes: 25 additions & 8 deletions src/runtime/server/FileRoute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,12 @@ impl FileRoute {
bun_core::heap::into_raw(Box::new(FileRoute {
ref_count: Cell::new(1),
server: Cell::new(opts.server),
has_last_modified_header: headers.get(b"last-modified").is_some(),
has_content_length_header: headers.get(b"content-length").is_some(),
has_content_range_header: headers.get(b"content-range").is_some(),
has_date_header: headers.get(b"date").is_some(),
has_last_modified_header: headers.get(b"last-modified").is_some_and(|v| !v.is_empty()),
has_content_length_header: headers
.get(b"content-length")
.is_some_and(|v| !v.is_empty()),
has_content_range_header: headers.get(b"content-range").is_some_and(|v| !v.is_empty()),
has_date_header: headers.get(b"date").is_some_and(|v| !v.is_empty()),
blob,
headers,
status_code: opts.status_code,
Expand Down Expand Up @@ -180,10 +182,16 @@ impl FileRoute {
return Ok(Some(bun_core::heap::into_raw(Box::new(FileRoute {
ref_count: Cell::new(1),
server: Cell::new(None),
has_last_modified_header: headers.get(b"last-modified").is_some(),
has_content_length_header: headers.get(b"content-length").is_some(),
has_content_range_header: headers.get(b"content-range").is_some(),
has_date_header: headers.get(b"date").is_some(),
has_last_modified_header: headers
.get(b"last-modified")
.is_some_and(|v| !v.is_empty()),
has_content_length_header: headers
.get(b"content-length")
.is_some_and(|v| !v.is_empty()),
has_content_range_header: headers
.get(b"content-range")
.is_some_and(|v| !v.is_empty()),
has_date_header: headers.get(b"date").is_some_and(|v| !v.is_empty()),
blob,
headers,
status_code,
Expand Down Expand Up @@ -230,6 +238,9 @@ impl FileRoute {
AnyResponse::SSL(s) => {
let s = bun_opaque::opaque_deref_mut(s);
for (name, value) in names.iter().zip(values) {
if value.length == 0 {
continue;
}
s.write_header(sp_slice(*name, buf), sp_slice(*value, buf));
}
if let Some(srv) = self.server.get() {
Expand All @@ -241,6 +252,9 @@ impl FileRoute {
AnyResponse::TCP(s) => {
let s = bun_opaque::opaque_deref_mut(s);
for (name, value) in names.iter().zip(values) {
if value.length == 0 {
continue;
}
s.write_header(sp_slice(*name, buf), sp_slice(*value, buf));
}
if let Some(srv) = self.server.get() {
Expand All @@ -252,6 +266,9 @@ impl FileRoute {
AnyResponse::H3(s) => {
let s = bun_opaque::opaque_deref_mut(s);
for (name, value) in names.iter().zip(values) {
if value.length == 0 {
continue;
}
s.write_header(sp_slice(*name, buf), sp_slice(*value, buf));
}
// tag == .H3 → no alt-svc header
Expand Down
10 changes: 7 additions & 3 deletions src/runtime/server/RequestContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1831,7 +1831,7 @@ where
let user_handles_range = if let Some(r) = self.response_weakref.get() {
r.status_code() != 200
|| r.get_init_headers_mut()
.map(|h| h.fast_has(jsc::HTTPHeaderName::ContentRange))
.map(|h| h.fast_get(jsc::HTTPHeaderName::ContentRange).is_some())
.unwrap_or(false)
} else {
false
Expand Down Expand Up @@ -3630,8 +3630,12 @@ where
let mut has_content_disposition = false;
let mut has_content_range = false;
if let Some(mut headers_) = response.swap_init_headers() {
has_content_disposition = headers_.fast_has(jsc::HTTPHeaderName::ContentDisposition);
has_content_range = headers_.fast_has(jsc::HTTPHeaderName::ContentRange);
has_content_disposition = headers_
.fast_get(jsc::HTTPHeaderName::ContentDisposition)
.is_some();
has_content_range = headers_
.fast_get(jsc::HTTPHeaderName::ContentRange)
.is_some();
// For .slice()-driven ranges, only promote to 206 if the user
// also set Content-Range (preserves the old contract). For an
// incoming Range: header (sendfile.total > 0) we always 206.
Expand Down
30 changes: 20 additions & 10 deletions src/runtime/server/StaticRoute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,14 @@
}

// Generate ETag if not already present
if headers.get(b"etag").is_none() {
if headers.get(b"etag").is_none_or(|v| v.is_empty()) {
if !blob.slice().is_empty() {
append_etag(blob.slice(), &mut headers);
}
}

let cached_blob_size = blob.size();
let has_date = headers.get(b"date").is_some();
let has_date = headers.get(b"date").is_some_and(|v| !v.is_empty());
bun_core::heap::into_raw(Box::new(StaticRoute {
ref_count: Cell::new(1),
blob,
Expand Down Expand Up @@ -222,13 +222,20 @@
// Consuming the body left a plain `Blob` behind, which no longer implies
// the `text/plain` a string body carried. Record it on the response's own
// headers so re-registering the same `Response` serves the same type.
// `fast_get` (unlike `put_default`'s `fast_has`) treats an empty value as
// absent, so an explicit `content-type: ""` is overwritten here — a
// Bun-specific choice for the `static:` route API, not at Fetch-spec
// construction time.
if was_string {
let text_mime = bun_http_types::MimeType::TEXT;
response.get_or_create_headers(global_this)?.put_default(
HTTPHeaderName::ContentType,
&bun_core::String::ascii(text_mime.value.as_ref()),
global_this,
)?;
let h = response.get_or_create_headers(global_this)?;
if h.fast_get(HTTPHeaderName::ContentType).is_none() {
let text_mime = bun_http_types::MimeType::TEXT;
h.put(
HTTPHeaderName::ContentType,
&bun_core::String::ascii(text_mime.value.as_ref()),
global_this,
)?;
}
}

let mut headers: Headers = bun_http_jsc::headers_jsc::from_fetch_headers(
Expand All @@ -237,14 +244,14 @@
);

// Generate ETag if not already present
if headers.get(b"etag").is_none() {
if headers.get(b"etag").is_none_or(|v| v.is_empty()) {
if !blob.slice().is_empty() {
append_etag(blob.slice(), &mut headers);
}
}

Check warning on line 251 in src/runtime/server/StaticRoute.rs

View check run for this annotation

Claude / Claude Code Review

Empty-etag static route: auto-ETag is appended (not replaced), so render_precondition still sees the empty entry — If-None-Match never 304s, If-Match 412s on the served tag

The new `.is_none_or(|v| v.is_empty())` etag gate (StaticRoute.rs:98, :247) triggers `append_etag` for `{ etag: "" }`, but `append_etag` calls `headers.append(b"etag", ...)` — adding a second entry after the empty one rather than replacing it. `do_write_headers` skips the empty entry so the wire correctly serves the auto content-hash (the new test passes), but `render_precondition` reads via `headers.get(b"etag").filter(|v| !v.is_empty())`, and `Headers::get` is a first-match linear scan that re
Comment thread
robobun marked this conversation as resolved.
Outdated

let cached_blob_size = blob.size();
let has_date = headers.get(b"date").is_some();
let has_date = headers.get(b"date").is_some_and(|v| !v.is_empty());
Comment thread
robobun marked this conversation as resolved.
return Ok(Some(bun_core::heap::into_raw(Box::new(StaticRoute {
ref_count: Cell::new(1),
blob,
Expand Down Expand Up @@ -494,6 +501,9 @@

debug_assert_eq!(names.len(), values.len());
for (name, value) in names.iter().zip(values) {
if value.length == 0 {
continue;
}
Comment thread
robobun marked this conversation as resolved.
resp.write_header(
&buf[name.offset as usize..][..name.length as usize],
&buf[value.offset as usize..][..value.length as usize],
Expand Down
Loading
Loading