Skip to content

fetch: reject an unreadable Bun.file() request body with a TypeError - #37501

Open
robobun wants to merge 4 commits into
mainfrom
farm/33fd93ab/fetch-file-body-type-error
Open

fetch: reject an unreadable Bun.file() request body with a TypeError#37501
robobun wants to merge 4 commits into
mainfrom
farm/33fd93ab/fetch-file-body-type-error

Conversation

@robobun

@robobun robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

fetch() with a Bun.file() request body that cannot be read rejects with a plain Error. Since #35855 the network errors fetch() produces are TypeErrors that carry the system error fields, and this rejection is the one remaining system error on the request side that was left out:

try {
  await fetch("http://127.0.0.1:1/", { method: "POST", body: Bun.file("/definitely/missing/upload") });
} catch (e) {
  console.log(e.constructor.name, e instanceof TypeError, e.code);
}
// before: Error false ENOENT
// after:  TypeError true ENOENT
// for comparison, fetch("http://127.0.0.1:1/") rejects with TypeError { code: "ConnectionRefused" }

The same applies to a directory as the body (EISDIR from the read), a descriptor that is not open (EBADF from the dup), and a Request whose body is the Bun.file().

Cause: in the needs_to_read_file() block of src/runtime/webcore/fetch.rs, both the open/dup failure and the read_file failure converted the bun_sys::Error with to_js(), which is SystemError::to_error_instance, i.e. a plain Error.

Fix: both sites now go through a small helper that builds the same SystemError with to_type_error_instance (the binding #35855 added, and what ValueError::SystemTypeError uses for the network errors from FetchTasklet::on_reject). code, errno, syscall, path/fd and the message are unchanged; only the prototype differs. The SysErrorJsc import goes away because these were its only uses in the file.

Why a TypeError

The fetch spec turns a request body that fails to be read into a network error (HTTP-network fetch, processBodyError), and fetch() rejects every network error with a TypeError. Node does the same for the equivalent input: a body stream over a missing file rejects with TypeError: fetch failed (cause ENOENT). Bun's convention since #35855 is to put the system error fields directly on the TypeError rather than under cause, and that is the shape this change produces, so err instanceof TypeError now identifies all of fetch()'s own failures regardless of whether the body or the connection failed.

Out of scope and unchanged here: a FormData body holding an unreadable Bun.file() (a body extraction error shared with the Request/Response constructors) and data: URL failures still reject with a plain Error; those are separate code paths. The rejected promise is still created the same way as before, so this composes with #37434, which changes how these early rejections are reported but not their class.

How did you verify your code works?

New describe block in test/js/bun/http/fetch-file-upload.test.ts covering the open failure (missing file, also via a Request), the read failure (directory) and the dup failure (descriptor that is not open). Each test checks that the promise comes back already rejected (Bun.peek.status), that the error is instanceof TypeError, and that the system error fields are still there. All four fail on the current release (Expected constructor: TypeError, received the plain Error with the same fields) and pass with this change; the rest of the file passes as well. cargo clippy -p bun_runtime is clean.

The open and read failures for a Bun.file() request body were built with
SystemError::to_error_instance, so they rejected as a plain Error while
every network error fetch() produces is a TypeError carrying the same
system error fields. Build them with to_type_error_instance instead;
code, errno, syscall and path are unchanged.
@robobun

robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 6:11 AM PT - Aug 11th, 2026

@robobun, your commit 20abf1187942bfa3b5491844484841de96b26f27 passed in Build #92260! 🎉


🧪   To try this PR locally:

bunx bun-pr 37501

That installs a local version of the PR into your bun-37501 executable, so you can run:

bun-37501 --bun

@robobun

robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

Status

Reproduced on the 1.4.0 release with fetch(url, { method: "POST", body: Bun.file("/missing") }): the rejection is a plain Error (code: "ENOENT"), while the network errors from the same call are TypeErrors. Same for a directory body (EISDIR) and a descriptor that is not open (EBADF).

Fix is in this PR (#37501): the two rejection sites in the needs_to_read_file() block of fetch.rs now build the error with to_type_error_instance; fields are unchanged. The new tests in test/js/bun/http/fetch-file-upload.test.ts fail on the release build and pass with the debug build.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 21 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 152d801a-5280-4610-81ea-f3c63fba4f84

📥 Commits

Reviewing files that changed from the base of the PR and between 698bc9f and 20abf11.

📒 Files selected for processing (2)
  • src/runtime/webcore/fetch.rs
  • test/js/bun/http/fetch-file-upload.test.ts

Walkthrough

The fetch implementation now converts file-body open and read failures into TypeError instances while preserving system error fields. Tests cover direct and Request bodies, missing files, directories, and invalid file descriptors.

Changes

Fetch file-body error handling

Layer / File(s) Summary
Convert file-body failures
src/runtime/webcore/fetch.rs
File-body open and read failures now use request_body_file_error, which creates TypeError instances with system error details.
Validate rejection behavior
test/js/bun/http/fetch-file-upload.test.ts
Tests verify early rejection and preserved error metadata for missing files, directories, and invalid file descriptors.

Possibly related PRs

  • oven-sh/bun#35855: Extends fetch error handling for file-body system errors.
  • oven-sh/bun#35988: Uses TypeError conversion with preserved system-error fields for file-body failures.
  • oven-sh/bun#37425: Covers related unreadable file-backed fetch errors and rejection behavior.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: unreadable Bun.file() request bodies now reject with a TypeError.
Description check ✅ Passed The description explains the change, cause, fix, scope, verification steps, test coverage, and clippy result using the required headings.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/bun/http/fetch-file-upload.test.ts`:
- Around line 260-297: Strengthen the assertions in the four fetch error tests
so each verifies all retained system error fields: include errno alongside code,
syscall, and path where applicable, and assert syscall plus fd for the invalid
descriptor case. Preserve platform-specific errno differences by checking the
expected Windows value separately from POSIX EBADF, while still requiring the
error to be a TypeError on every platform.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 74413441-a90b-4b6d-a9fb-8c966f4c42ed

📥 Commits

Reviewing files that changed from the base of the PR and between da3851e and 698bc9f.

📒 Files selected for processing (2)
  • src/runtime/webcore/fetch.rs
  • test/js/bun/http/fetch-file-upload.test.ts

Comment thread test/js/bun/http/fetch-file-upload.test.ts
Comment thread src/runtime/webcore/fetch.rs Outdated
Comment thread src/runtime/webcore/fetch.rs Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the two file-body rejection sites now build the same TypeError-with-system-fields shape as FetchTasklet::on_reject, and only the error prototype changes.

What was reviewed

  • Confirmed SystemError::to_type_error_instance is the same binding ValueError::SystemTypeError uses for network errors, and that the removed SysErrorJsc import has no remaining uses in fetch.rs.
  • Checked the open-failure early return: the missing body.detach() there is pre-existing and unchanged by this diff.
  • Tests cover open/read/dup failures and the Request-body variant, pin synchronous rejection via Bun.peek.status, and assert the retained code/errno/syscall/path/fd fields (Windows dup errno intentionally left unpinned).
Extended reasoning...

Overview

Two call sites in the needs_to_read_file() block of src/runtime/webcore/fetch.rs — the open/dup failure and the read_file failure — swap err.to_js(global_this) (plain Error) for a small helper request_body_file_error that produces jsc::SystemError::from(err.to_system_error()).to_type_error_instance(global_this). The now-unused SysErrorJsc trait import is dropped. A new describe.concurrent block in test/js/bun/http/fetch-file-upload.test.ts covers ENOENT (direct and via Request), EISDIR, and EBADF, each asserting instanceof TypeError, synchronous rejection (Bun.peek.status), and the retained system-error fields.

Security risks

None. This changes only which JS error prototype the rejection uses; the error data (code, errno, syscall, path/fd, message) is identical to before. No new inputs are parsed and no control flow around fd ownership or cleanup is altered.

Level of scrutiny

Low-to-medium. The Rust diff is a two-line substitution plus a three-line helper and an import cleanup; the surrounding rejection/cleanup logic is untouched. I verified to_type_error_instance exists on jsc::SystemError (src/jsc/SystemError.rs:91) and is exactly what ValueError::SystemTypeError routes through in Body.rs, so the helper matches the sibling network-error shape the doc comment references. A grep confirms no remaining SysErrorJsc / .to_js(global uses in fetch.rs, so the import removal is safe.

Other factors

All prior review threads are resolved: the comment-cop feedback was addressed by trimming the helper's doc comment to one line, and the CodeRabbit request to strengthen assertions was applied in 6bb2428 (errno on all four cases, fd on POSIX for the descriptor case; Windows errno intentionally not pinned per the noted dup() quirk). The bug-hunting system found nothing; the one candidate it examined (missing body.detach() on the open-failure path) is pre-existing and out of scope — this diff only changes the value passed to the rejected promise, not the cleanup around it. Tests use tempDir, 127.0.0.1:1, and describe.concurrent, so they are hermetic and fast.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant