diff --git a/src/runtime/webcore/fetch.rs b/src/runtime/webcore/fetch.rs index 2a41b66e2cc4..03553ca68b02 100644 --- a/src/runtime/webcore/fetch.rs +++ b/src/runtime/webcore/fetch.rs @@ -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 @@ -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 // ────────────────────────────────────────────────────────────────────────── @@ -1709,7 +1714,7 @@ fn fetch_impl( 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, @@ -1808,7 +1813,7 @@ fn fetch_impl( 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); diff --git a/test/js/bun/http/fetch-file-upload.test.ts b/test/js/bun/http/fetch-file-upload.test.ts index 38ea1ae6c323..98331b2f7efb 100644 --- a/test/js/bun/http/fetch-file-upload.test.ts +++ b/test/js/bun/http/fetch-file-upload.test.ts @@ -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): Promise { + 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 }); + }); +});