Skip to content
Closed
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
8 changes: 5 additions & 3 deletions src/runtime/server/FileRoute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,13 @@ impl<'a> Default for InitOptions<'a> {
}
}

use crate::webcore::headers_ref::blob_content_type;

#[inline]
fn headers_from(fetch_headers: Option<&FetchHeaders>, blob: &Blob) -> Headers {
bun_http_jsc::headers_jsc::from_fetch_headers(fetch_headers, blob_content_type(blob))
// Match `RequestContext::get_content_type`: fall back to the File/S3 store's
// extension-sniffed mime so a sliced Bun.file() serves the same Content-Type
// as the unsliced file.
let ct = blob.content_type_or_mime_type().filter(|ct| !ct.is_empty());
bun_http_jsc::headers_jsc::from_fetch_headers(fetch_headers, ct)
}

#[inline]
Expand Down
6 changes: 2 additions & 4 deletions src/runtime/server/RequestContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4501,14 +4501,12 @@ fn get_content_type(headers: Option<&mut FetchHeaders>, blob: &AnyBlob) -> (Mime
}
}

if !blob.content_type().is_empty() {
bun_http_types::MimeType::by_name(blob.content_type())
if let Some(ct) = blob.content_type_or_mime_type().filter(|ct| !ct.is_empty()) {
bun_http_types::MimeType::by_name(ct)
} else if let Some(content) = bun_http_types::MimeType::sniff(blob.slice()) {
content
} else if blob.was_string() {
bun_http_types::MimeType::TEXT
// TODO: should we get the mime type off of the Blob.Store if it exists?
// A little wary of doing this right now due to causing some breaking change
} else {
bun_http_types::MimeType::OTHER
}
Expand Down
76 changes: 24 additions & 52 deletions src/runtime/webcore/Blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1371,12 +1371,8 @@ impl BlobExt for Blob {
let slice = content_type_str.slice();
if is_valid_blob_type(slice) {
self.content_type_was_set.set(true);
self.content_type.set(
match global_this.bun_vm().as_mut().mime_type(slice) {
Some(mime) => BlobContentType::from(mime),
None => BlobContentType::from_lowercased(slice),
},
);
self.content_type
.set(BlobContentType::from_lowercased(slice));
}
}
} else if !options_object.is_empty_or_undefined_or_null() {
Expand Down Expand Up @@ -1791,12 +1787,8 @@ impl BlobExt for Blob {
let slice = content_type_str.slice();
if is_valid_blob_type(slice) {
self.content_type_was_set.set(true);
self.content_type.set(
match global_this.bun_vm().as_mut().mime_type(slice) {
Some(mime) => BlobContentType::from(mime),
None => BlobContentType::from_lowercased(slice),
},
);
self.content_type
.set(BlobContentType::from_lowercased(slice));
}
}

Expand Down Expand Up @@ -2016,17 +2008,10 @@ impl BlobExt for Blob {
blob.offset.set(offset);
blob.size.set(len);

let content_type_was_allocated = content_type.is_owned() && !content_type.is_empty();
// infer the content type if it was not specified
if content_type.is_empty()
&& matches!(self.content_type.get(), BlobContentType::Static(s) if !s.is_empty())
{
blob.content_type.set(self.content_type.get().clone());
} else {
blob.content_type.set(content_type);
}
blob.content_type_was_set
.set(self.content_type_was_set.get() || content_type_was_allocated);
// File API: a slice()'s type is the caller-supplied contentType (or "" if
// omitted/invalid); it never inherits the parent blob's type.
blob.content_type_was_set.set(!content_type.is_empty());
Comment thread
robobun marked this conversation as resolved.
Resolved
blob.content_type.set(content_type);
Comment thread
robobun marked this conversation as resolved.
Resolved
Comment thread
robobun marked this conversation as resolved.
Resolved
Comment thread
robobun marked this conversation as resolved.
Resolved

let ptr = Blob::new(blob);
// SAFETY: `ptr` just came from `heap::alloc` in `Blob::new`. Explicit
Expand All @@ -2042,13 +2027,6 @@ impl BlobExt for Blob {
// index the full fixed-3 array (args[2] is written below regardless of len).
let args = &mut arguments_.ptr[..];

if self.size.get() == 0 {
let ptr = Blob::new(Blob::init_empty(global_this));
// SAFETY: `ptr` just came from `heap::alloc` in `Blob::new`; force
// the inherent `Blob::to_js(&mut self)` over `JsClass::to_js`.
return Ok(unsafe { BlobExt::to_js(&*ptr, global_this) });
}

// If the optional start parameter is not used as a parameter, let relativeStart be 0.
let mut relative_start: i64 = 0;
// If the optional end parameter is not used, let relativeEnd be size.
Expand Down Expand Up @@ -2101,10 +2079,7 @@ impl BlobExt for Blob {
if !is_valid_blob_type(slice) {
break 'inner;
}
content_type = match global_this.bun_vm().as_mut().mime_type(slice) {
Some(mime) => BlobContentType::from(mime),
None => BlobContentType::from_lowercased(slice),
};
content_type = BlobContentType::from_lowercased(slice);
Comment thread
robobun marked this conversation as resolved.
Resolved
}
}
}
Expand Down Expand Up @@ -2457,12 +2432,8 @@ impl BlobExt for Blob {
break 'inner;
}
blob.content_type_was_set.set(true);
blob.content_type.set(
match global_this.bun_vm().as_mut().mime_type(slice) {
Some(mime) => BlobContentType::from(mime),
None => BlobContentType::from_lowercased(slice),
},
);
blob.content_type
.set(BlobContentType::from_lowercased(slice));
}
}
}
Expand Down Expand Up @@ -5680,12 +5651,8 @@ pub fn jsdom_file_construct_(
break 'inner;
}
blob.content_type_was_set.set(true);
blob.content_type.set(
match global_this.bun_vm().as_mut().mime_type(slice) {
Some(mime) => BlobContentType::from(mime),
None => BlobContentType::from_lowercased(slice),
},
);
blob.content_type
.set(BlobContentType::from_lowercased(slice));
}
}
}
Expand Down Expand Up @@ -5767,12 +5734,8 @@ pub fn construct_bun_file(
break 'inner;
}
blob.content_type_was_set.set(true);
blob.content_type.set(
match global_object.bun_vm().as_mut().mime_type(slice) {
Some(mime) => BlobContentType::from(mime),
None => BlobContentType::from_lowercased(slice),
},
);
blob.content_type
.set(BlobContentType::from_lowercased(slice));
}
}
}
Expand Down Expand Up @@ -6769,6 +6732,15 @@ impl Any {
}
}

/// `content_type()`, or for a File-/S3-backed Blob with no explicit type,
/// the extension-sniffed `mime_type` held on the store.
pub fn content_type_or_mime_type(&self) -> Option<&[u8]> {
match self {
Any::Blob(b) => b.content_type_or_mime_type(),
_ => Some(self.content_type()),
}
}

pub fn was_string(&self) -> bool {
match self {
Any::Blob(b) => b.charset.get() == strings::AsciiStatus::AllAscii,
Expand Down
14 changes: 3 additions & 11 deletions src/runtime/webcore/S3File.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,10 @@ where
)?;

formatter.print_comma::<W, ENABLE_ANSI_COLORS>(writer)?;
if offset > 0 {
writer.write_str("\n")?;
}
}

if offset > 0 {
writer.write_str("\n")?;
let mut formatter = formatter.indented();
formatter.write_indent(writer)?;

Expand Down Expand Up @@ -399,10 +397,7 @@ pub(crate) fn construct_s3_file_with_s3_credentials_and_options(
}
blob.content_type_was_set.set(true);
blob.content_type
.set(match global.bun_vm().as_mut().mime_type(slice) {
Some(mime) => blob::BlobContentType::from(mime),
None => blob::BlobContentType::from_lowercased(slice),
});
.set(blob::BlobContentType::from_lowercased(slice));
}
}
}
Expand Down Expand Up @@ -446,10 +441,7 @@ pub(crate) fn construct_s3_file_with_s3_credentials(
}
blob.content_type_was_set.set(true);
blob.content_type
.set(match global.bun_vm().as_mut().mime_type(slice) {
Some(mime) => blob::BlobContentType::from(mime),
None => blob::BlobContentType::from_lowercased(slice),
});
.set(blob::BlobContentType::from_lowercased(slice));
Comment thread
robobun marked this conversation as resolved.
Resolved
}
}
}
Expand Down
21 changes: 5 additions & 16 deletions src/runtime/webcore/headers_ref.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,21 @@
//! Helpers for extracting the body content-type byte slice that
//! `bun_http_jsc::headers_jsc::from_fetch_headers` consumes. Shared by
//! `webcore::fetch`, `server::StaticRoute`, `server::FileRoute`, and
//! `bake::DevServer`.
//! `webcore::fetch`, `server::StaticRoute`, and `bake::DevServer`.

use crate::webcore::Blob;
use crate::webcore::blob::Any as AnyBlob;

/// `Some(ct)` only when the body has a *user-set* content-type.
/// `Some(ct)` only when the body has a non-empty user-set content-type.
#[inline]
pub(crate) fn any_blob_content_type(b: &AnyBlob) -> Option<&[u8]> {
if b.has_content_type_from_user() {
Some(b.content_type())
let ct = b.content_type();
(!ct.is_empty()).then_some(ct)
} else {
None
}
}

/// `Some(ct)` only when the body has a *user-set* content-type.
#[inline]
pub(crate) fn blob_content_type(b: &Blob) -> Option<&[u8]> {
if b.has_content_type_from_user() {
Some(b.content_type_slice())
} else {
None
}
}

/// `Some(ct)` only when the body has a *user-set* content-type.
/// `Some(ct)` only when the body has a non-empty user-set content-type.
#[inline]
pub(crate) fn any_blob_content_type_opt(b: Option<&AnyBlob>) -> Option<&[u8]> {
b.and_then(any_blob_content_type)
Expand Down
10 changes: 3 additions & 7 deletions test/js/bun/http/bun-serve-static.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,11 +208,7 @@ describe("static route Content-Type", () => {
// change the Content-Type the next registration of the same Response produces.
test.each([
["string body", () => new Response("hello"), "text/plain;charset=utf-8"],
[
"typed Blob body",
() => new Response(new Blob(["<h1>hi</h1>"], { type: "text/html" })),
"text/html;charset=utf-8",
],
["typed Blob body", () => new Response(new Blob(["<h1>hi</h1>"], { type: "text/html" })), "text/html"],
["explicit header", () => new Response("hello", { headers: { "Content-Type": "text/foo" } }), "text/foo"],
["Response.json", () => Response.json({ a: 1 }), "application/json;charset=utf-8"],
["Uint8Array body", () => new Response(new Uint8Array([1, 2, 3])), null],
Expand Down Expand Up @@ -252,8 +248,8 @@ describe("static route Content-Type", () => {
untouched: await contentTypeOf(server, "/untouched"),
touched: await contentTypeOf(server, "/touched"),
}).toEqual({
untouched: "text/html;charset=utf-8",
touched: "text/html;charset=utf-8",
untouched: "text/html",
touched: "text/html",
});
});

Expand Down
8 changes: 2 additions & 6 deletions test/js/bun/s3/s3.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,9 @@ describe.concurrent.skipIf(!r2Credentials.endpoint && !isCI)("Virtual Hosted-Sty
);
}
{
const file = client
.file("filename.txt", {
type: "text/plain",
})
.slice(10);
const file = client.file("filename.txt").slice(10, "text/plain");
expect(Bun.inspect(file)).toBe(
'S3Ref ("bucket/filename.txt") {\n type: "text/plain;charset=utf-8",\n offset: 10,\n endpoint: "bucket.test.r2.cloudflarestorage.com",\n region: "auto",\n accessKeyId: "[REDACTED]",\n secretAccessKey: "[REDACTED]",\n partSize: 5242880,\n queueSize: 5,\n retry: 3\n}',
'S3Ref ("bucket/filename.txt") {\n type: "text/plain",\n offset: 10,\n endpoint: "bucket.test.r2.cloudflarestorage.com",\n region: "auto",\n accessKeyId: "[REDACTED]",\n secretAccessKey: "[REDACTED]",\n partSize: 5242880,\n queueSize: 5,\n retry: 3\n}',
);
}
});
Expand Down
12 changes: 6 additions & 6 deletions test/js/bun/util/inspect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -619,23 +619,23 @@ describe("console.logging class displays names and extends", async () => {

it("console.log on a Blob shows name", () => {
const blob = new Blob(["foo"], { type: "text/plain" });
expect(Bun.inspect(blob)).toBe('Blob (3 bytes) {\n type: "text/plain;charset=utf-8"\n}');
expect(Bun.inspect(blob)).toBe('Blob (3 bytes) {\n type: "text/plain"\n}');
blob.name = "bar";
expect(Bun.inspect(blob)).toBe('Blob (3 bytes) {\n name: "bar",\n type: "text/plain;charset=utf-8"\n}');
expect(Bun.inspect(blob)).toBe('Blob (3 bytes) {\n name: "bar",\n type: "text/plain"\n}');
blob.name = "foobar";
expect(Bun.inspect(blob)).toBe('Blob (3 bytes) {\n name: "foobar",\n type: "text/plain;charset=utf-8"\n}');
expect(Bun.inspect(blob)).toBe('Blob (3 bytes) {\n name: "foobar",\n type: "text/plain"\n}');

const file = new File(["foo"], "bar.txt", { type: "text/plain" });
expect(Bun.inspect(file)).toBe(
`File (3 bytes) {\n name: "bar.txt",\n type: "text/plain;charset=utf-8",\n lastModified: ${file.lastModified}\n}`,
`File (3 bytes) {\n name: "bar.txt",\n type: "text/plain",\n lastModified: ${file.lastModified}\n}`,
);
file.name = "foobar";
expect(Bun.inspect(file)).toBe(
`File (3 bytes) {\n name: "foobar",\n type: "text/plain;charset=utf-8",\n lastModified: ${file.lastModified}\n}`,
`File (3 bytes) {\n name: "foobar",\n type: "text/plain",\n lastModified: ${file.lastModified}\n}`,
);
file.name = "";
expect(Bun.inspect(file)).toBe(
`File (3 bytes) {\n name: "",\n type: "text/plain;charset=utf-8",\n lastModified: ${file.lastModified}\n}`,
`File (3 bytes) {\n name: "",\n type: "text/plain",\n lastModified: ${file.lastModified}\n}`,
);
});

Expand Down
Loading
Loading