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
11 changes: 8 additions & 3 deletions src/runtime/webcore/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ use bun_core::{String as BunString, Tag as BunStringTag, ZigStringSlice};
use bun_http::{self as http, FetchRedirect, Headers, HeadersExt as _, MimeType};
use bun_http_jsc::method_jsc;
use bun_http_types::Method::Method;
use bun_jsc::{HTTPHeaderName, StringJsc as _, SysErrorJsc as _};
use bun_jsc::{HTTPHeaderName, StringJsc as _};
use bun_paths::{self, PathBuffer};
use bun_sys::FdExt as _;
// `FromJsEnum for FetchRedirect` lives in bun_http_jsc; importing the impl crate
Expand Down Expand Up @@ -368,6 +368,11 @@ fn reject_on_exception(
Ok(JSPromise::dangerously_create_rejected_promise_value_without_notifying_vm(global_this, err))
}

/// Same `TypeError` shape as the network errors from `FetchTasklet::on_reject`.
fn request_body_file_error(err: &bun_sys::Error, global_this: &JSGlobalObject) -> JSValue {
jsc::SystemError::from(err.to_system_error()).to_type_error_instance(global_this)
}

// ──────────────────────────────────────────────────────────────────────────
// URLType
// ──────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -1709,7 +1714,7 @@ fn fetch_impl<const ALLOW_GET_BODY: bool>(

let opened_fd = match opened_fd_res {
Err(err) => {
let err_js = err.to_js(global_this);
let err_js = request_body_file_error(&err, global_this);
let rejected_value =
JSPromise::dangerously_create_rejected_promise_value_without_notifying_vm(
global_this,
Expand Down Expand Up @@ -1808,7 +1813,7 @@ fn fetch_impl<const ALLOW_GET_BODY: bool>(
let rejected_value =
JSPromise::dangerously_create_rejected_promise_value_without_notifying_vm(
global_this,
err.to_js(global_this),
request_body_file_error(&err, global_this),
);
body.detach();
return Ok(rejected_value);
Expand Down
65 changes: 65 additions & 0 deletions test/js/bun/http/fetch-file-upload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,68 @@ test("missing file throws the expected error", async () => {
});
Bun.gc(true);
});

// Like the network errors fetch() rejects with, a Bun.file() body that cannot
// be read rejects with a TypeError that still carries the system error fields.
describe.concurrent("Bun.file() body that cannot be read rejects with a TypeError", () => {
// The body is read before anything is connected, so the promise comes back
// already rejected. Checking that (and the system error fields) pins the
// rejection to the body, not to the connection the URL would refuse.
function earlyRejection(promise: Promise<Response>): Promise<unknown> {
const error = promise.then(
() => {
throw new Error("fetch() resolved");
},
err => err,
);
expect(Bun.peek.status(promise)).toBe("rejected");
return error;
}

test("file that does not exist (open fails)", async () => {
using dir = tempDir("fetch-unreadable-body", {});
const path = join(String(dir), "missing.txt");

const err = await earlyRejection(fetch("http://127.0.0.1:1/", { method: "POST", body: Bun.file(path) }));
expect(err).toBeInstanceOf(TypeError);
expect(err).toMatchObject({
code: "ENOENT",
errno: expect.any(Number),
syscall: "open",
path: expect.stringContaining("missing.txt"),
});
});

test("file that does not exist, as the body of a Request", async () => {
using dir = tempDir("fetch-unreadable-body", {});
const path = join(String(dir), "missing.txt");

const request = new Request("http://127.0.0.1:1/", { method: "POST", body: Bun.file(path) });
const err = await earlyRejection(fetch(request));
expect(err).toBeInstanceOf(TypeError);
expect(err).toMatchObject({
code: "ENOENT",
errno: expect.any(Number),
syscall: "open",
path: expect.stringContaining("missing.txt"),
});
});

test("directory (open succeeds, read fails)", async () => {
using dir = tempDir("fetch-unreadable-body", {});

const err = await earlyRejection(fetch("http://127.0.0.1:1/", { method: "POST", body: Bun.file(String(dir)) }));
expect(err).toBeInstanceOf(TypeError);
expect(err).toMatchObject({ code: "EISDIR", errno: expect.any(Number), syscall: "read" });
});

test("file descriptor that is not open (dup fails)", async () => {
const fd = 1 << 30;

const err = await earlyRejection(fetch("http://127.0.0.1:1/", { method: "POST", body: Bun.file(fd) }));
expect(err).toBeInstanceOf(TypeError);
expect(err).toMatchObject({ code: expect.any(String), errno: expect.any(Number) });
// Windows does not report EBADF for a descriptor number that was never opened.
if (!isWindows) expect(err).toMatchObject({ code: "EBADF", fd });
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
Loading