Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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`).
Comment thread
robobun marked this conversation as resolved.
Outdated
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