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 src/runtime/webcore/ReadableStream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,15 @@ impl ReadableStream {
let blobby = self.ptr.file().expect("matched File");
if let webcore::file_reader::Lazy::Blob(store) = blobby.lazy.get() {
let blob = Blob::init_with_store(store.clone(), global_this);
// `init_with_store` spans the whole store; the window of the
// Blob this stream was made from lives on the reader
// (see `from_blob_copy_ref`).
if let Some(offset) = blobby.start_offset {
blob.offset.set(offset as webcore::blob::SizeType);
}
if let Some(size) = blobby.max_size {
blob.size.set(size as webcore::blob::SizeType);
}
// it should be lazy, file shouldn't have opened yet.
debug_assert!(!blobby.started.get());
self.done(global_this);
Expand Down
39 changes: 39 additions & 0 deletions test/js/bun/http/bun-serve-file.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ describe("Bun.file in serve routes", () => {
}),
"/partial.txt": new Response(Bun.file(join(tempDir, "partial.txt"))),
"/partial-slice.txt": new Response(Bun.file(join(tempDir, "partial.txt")).slice(5, 10)),
// An unread Bun.file() stream is turned back into the file Blob when a
// route or handler response is built from it; the slice must survive.
"/partial-slice-stream.txt": new Response(Bun.file(join(tempDir, "partial.txt")).slice(5, 10).stream()),
"/partial-slice-stream-handler": () => new Response(Bun.file(join(tempDir, "partial.txt")).slice(5, 10).stream()),
"/partial-open-slice-stream-handler": () =>
new Response(Bun.file(join(tempDir, "partial.txt")).slice(10).stream()),
"/partial-empty-slice-stream-handler": () =>
new Response(Bun.file(join(tempDir, "partial.txt")).slice(5, 5).stream()),
"/partial-stream-handler": () => new Response(Bun.file(join(tempDir, "partial.txt")).stream()),
"/fd-not-supported.txt": (() => {
// This would test file descriptors, but they're not supported yet
return new Response(Bun.file(join(tempDir, "hello.txt")));
Expand Down Expand Up @@ -713,6 +722,36 @@ describe("Bun.file in serve routes", () => {
expect(res.headers.get("Content-Length")).toBe("5");
});

it("serves the slice behind a sliced file's stream as a route", async () => {
const res = await fetch(new URL(`/partial-slice-stream.txt`, server.url));
expect(res.status).toBe(200);
expect(await res.text()).toBe("56789");
expect(res.headers.get("Content-Length")).toBe("5");
});

it("serves the slice behind a sliced file's stream from a handler", async () => {
const serve = async (pathname: string) => {
const get = await fetch(new URL(pathname, server.url));
const head = await fetch(new URL(pathname, server.url), { method: "HEAD" });
return {
body: await get.text(),
contentLength: get.headers.get("Content-Length"),
headContentLength: head.headers.get("Content-Length"),
};
};
expect({
"slice(5, 10)": await serve("/partial-slice-stream-handler"),
"slice(10)": await serve("/partial-open-slice-stream-handler"),
"slice(5, 5)": await serve("/partial-empty-slice-stream-handler"),
"whole file": await serve("/partial-stream-handler"),
}).toEqual({
"slice(5, 10)": { body: "56789", contentLength: "5", headContentLength: "5" },
"slice(10)": { body: "ABCDEF", contentLength: "6", headContentLength: "6" },
"slice(5, 5)": { body: "", contentLength: "0", headContentLength: "0" },
"whole file": { body: "0123456789ABCDEF", contentLength: "16", headContentLength: "16" },
});
});

// The slice is shorter than the file, so the byte budget runs out before
// the reader reports EOF: the response completes inline while a deferred
// completion still hops through the event loop. Repeated requests must
Expand Down
55 changes: 54 additions & 1 deletion test/js/web/fetch/body.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { file, spawn, version, type Socket } from "bun";
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, exampleSite } from "harness";
import { bunEnv, bunExe, exampleSite, tempDir } from "harness";
import net from "net";

const exampleServer = exampleSite("http");
Expand Down Expand Up @@ -287,6 +287,59 @@ for (const { body, fn } of bodyTypes) {
expect(await subject.text()).toBe("bye");
expect(subject.bodyUsed).toBe(true);
});

// The readers move an unread Bun.file() stream back into a Blob the same
// way. That Blob has to cover the slice the stream was made from, not
// the whole file. (text() is not in here: it pumps the stream instead.)
describe("made from a sliced Bun.file()", () => {
const alphabet = "abcdefghijklmnopqrstuvwxyz";

test("bytes() returns the window of the slice the stream was made from", async () => {
using dir = tempDir("body-file-slice-stream", { "data.txt": alphabet });
const file = () => Bun.file(`${dir}/data.txt`);
const bytesOf = async (blob: Blob) => Buffer.from(await fn(blob.stream()).bytes()).toString();
expect({
"slice(3, 8)": await bytesOf(file().slice(3, 8)),
"slice(21)": await bytesOf(file().slice(21)),
"slice(3, 1000)": await bytesOf(file().slice(3, 1000)),
"slice(4, 4)": await bytesOf(file().slice(4, 4)),
"slice(3, 20).slice(2, 6)": await bytesOf(file().slice(3, 20).slice(2, 6)),
"whole file": await bytesOf(file()),
}).toEqual({
"slice(3, 8)": "defgh",
"slice(21)": "vwxyz",
"slice(3, 1000)": "defghijklmnopqrstuvwxyz",
"slice(4, 4)": "",
"slice(3, 20).slice(2, 6)": "fghi",
"whole file": alphabet,
});
});

test("arrayBuffer() and blob() return the slice too", async () => {
using dir = tempDir("body-file-slice-stream-readers", { "data.txt": alphabet });
const slice = () => Bun.file(`${dir}/data.txt`).slice(3, 8);
const blob = await fn(slice().stream()).blob();
expect({
arrayBuffer: Buffer.from(await fn(slice().stream()).arrayBuffer()).toString(),
blob: [blob.size, await blob.text()],
}).toEqual({
arrayBuffer: "defgh",
blob: [5, "defgh"],
});
});

test("json() parses only the slice", async () => {
using dir = tempDir("body-file-slice-stream-json", { "data.json": `--{"ok":true}--` });
expect(await fn(Bun.file(`${dir}/data.json`).slice(2, 13).stream()).json()).toEqual({ ok: true });
});

test("bytes() after the body getter turned a sliced Bun.file() body into a stream", async () => {
using dir = tempDir("body-file-slice-body-getter", { "data.txt": alphabet });
const subject = fn(Bun.file(`${dir}/data.txt`).slice(3, 8));
expect(subject.body).toBeInstanceOf(ReadableStream);
expect([Buffer.from(await subject.bytes()).toString(), subject.bodyUsed]).toEqual(["defgh", true]);
});
});
});
for (const { string, buffer } of utf8) {
describe("arrayBuffer()", () => {
Expand Down
Loading