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
10 changes: 4 additions & 6 deletions src/runtime/webcore/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1746,11 +1746,6 @@ fn fetch_impl<const ALLOW_GET_BODY: bool>(

let original_size = body.any_blob().blob().size.get();
let stat_size = blob::SizeType::try_from(stat.st_size).expect("int cast");
let blob_size = if bun_sys::S::ISREG(stat.st_mode as u32) {
stat_size
} else {
original_size.min(stat_size)
};
let blob_offset = body.any_blob().blob().offset.get();

// `http::SendFile` fields are `usize`; blob sizes/offsets
Expand All @@ -1759,7 +1754,7 @@ fn fetch_impl<const ALLOW_GET_BODY: bool>(
fd: opened_fd,
remain: (blob_offset + original_size) as usize,
offset: blob_offset as usize,
content_size: blob_size as usize,
content_size: original_size.min(stat_size) as usize,
};

if bun_sys::S::ISREG(stat.st_mode as u32) {
Expand All @@ -1770,6 +1765,9 @@ fn fetch_impl<const ALLOW_GET_BODY: bool>(
.max(sf.offset)
.min(stat_size_usize)
.saturating_sub(sf.offset);
// `remain` is now the exact byte count we will send (the slice
// window clamped to the file); that is the Content-Length.
sf.content_size = sf.remain;
}
body.detach();
body = HTTPRequestBody::Sendfile(sf);
Expand Down
67 changes: 65 additions & 2 deletions test/js/bun/http/fetch-file-upload.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from "bun:test";
import { isBroken, isWindows, withoutAggressiveGC } from "harness";
import { describe, expect, test } from "bun:test";
import { isBroken, isWindows, tempDir, withoutAggressiveGC } from "harness";
import { tmpdir } from "os";
import { join } from "path";

Expand Down Expand Up @@ -159,6 +159,69 @@ test.todoIf(isBroken && isWindows)(
10_000,
);

describe("Bun.file().slice() upload sends the slice's Content-Length", () => {
// The sendfile fast path is entered when the backing file is >= 32 KiB.
// It previously advertised the whole file's stat size as Content-Length,
// while sending only the slice bytes, so the origin waited forever.
for (const fileSize of [32 * 1024 - 1, 32 * 1024, 64 * 1024, 1024 * 1024]) {
test.concurrent(`file size ${fileSize}`, async () => {
const bytes = Buffer.alloc(fileSize);
for (let i = 0; i < fileSize; i++) bytes[i] = i & 0xff;
using dir = tempDir("fetch-file-slice-upload", { "f.bin": bytes });
const p = join(String(dir), "f.bin");

let contentLength: string | null = "?";
let received = Buffer.alloc(0);
await using server = Bun.serve({
port: 0,
development: false,
async fetch(req) {
contentLength = req.headers.get("content-length");
received = Buffer.from(await req.arrayBuffer());
return new Response("ok");
},
});

const body = Bun.file(p).slice(10, 110);
expect(body.size).toBe(100);

const res = await fetch(server.url, { method: "POST", body });
expect(await res.text()).toBe("ok");
expect(res.status).toBe(200);
expect({ contentLength, received: received.length, firstByte: received[0], lastByte: received[99] }).toEqual({
contentLength: "100",
received: 100,
firstByte: 10,
lastByte: 109,
});
});
}

test.concurrent("open-ended slice(10)", async () => {
const fileSize = 64 * 1024;
using dir = tempDir("fetch-file-slice-upload-open", { "f.bin": Buffer.alloc(fileSize, 7) });
const p = join(String(dir), "f.bin");

let contentLength: string | null = "?";
let received = 0;
await using server = Bun.serve({
port: 0,
development: false,
maxRequestBodySize: fileSize * 2,
async fetch(req) {
contentLength = req.headers.get("content-length");
for await (const c of req.body!) received += c.length;
return new Response("ok");
},
});

const res = await fetch(server.url, { method: "POST", body: Bun.file(p).slice(10) });
expect(await res.text()).toBe("ok");
expect(res.status).toBe(200);
expect({ contentLength, received }).toEqual({ contentLength: String(fileSize - 10), received: fileSize - 10 });
});
});

test("missing file throws the expected error", async () => {
Bun.gc(true);
// Run this 1000 times to check for GC bugs
Expand Down
Loading