Skip to content

server: preserve request Content-Type in request.blob() - #32806

Closed
robobun wants to merge 5 commits into
mainfrom
farm/9059bd42/fix-request-blob-content-type
Closed

robobun wants to merge 5 commits into
mainfrom
farm/9059bd42/fix-request-blob-content-type

Conversation

@robobun

@robobun robobun commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

Fixes #32801

Repro

await using server = Bun.serve({
  port: 0,
  async fetch(request) {
    return new Response((await request.blob()).type);
  },
});

const res = await fetch(server.url, {
  method: "POST",
  body: new File(['"name"'], "file.json", { type: "application/json;charset=utf-8" }),
});
console.log(await res.text());
// expected: application/json;charset=utf-8
// actual:   text/plain;charset=utf-8

Every content type collapsed to the text/plain;charset=utf-8 fallback, regardless of what the client sent.

Cause

When a Bun.serve handler reads the request body with request.blob(), the body is in the Locked state and is resolved asynchronously once the bytes arrive, in on_buffered_body_chunk (src/runtime/server/RequestContext.rs). That call passed None for the headers argument of Body::Value::resolve, so the Content-Type header was never consulted and the Blob fell back to text/plain;charset=utf-8.

The fetch client path already does this correctly, passing the response's fetch headers into resolve.

Fix

Pass the request object's fetch headers into resolve (reached via request_weakref), mirroring the fetch client. resolve reads the Content-Type from them and sets it on the resolved Blob.

Note: the sibling resolve call in on_start_buffering keeps None. That branch is only reached when the body is not Locked (its guard is the exact negation of the condition that makes the body Locked), so it never produces a Blob and passing headers there would be dead code.

Verification

test/js/bun/http/serve.test.ts POSTs a File with four distinct types and asserts (await request.blob()).type round-trips each one.

  • Fails before the fix: all four types come back as text/plain;charset=utf-8.
  • Passes after.

@coderabbitai

coderabbitai Bot commented Jun 27, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The request-body resolution path now passes fetch headers into Body::Value::resolve when converting a locked body. A new server test posts File bodies with explicit MIME types and checks that request.blob().type preserves them.

Changes

Request body Blob type preservation

Layer / File(s) Summary
Locked body resolves with fetch headers
src/runtime/server/RequestContext.rs
RequestContext::on_buffered_body_chunk reads fetch headers from request_weakref and passes them into Body::Value::resolve for the locked-body branch.
Blob type regression test
test/js/bun/http/serve.test.ts
A serve test posts File bodies with explicit MIME types, returns blob.type, and asserts the response matches each input type.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: preserving request Content-Type in request.blob().
Description check ✅ Passed The description explains the bug, cause, fix, and verification, covering the template's intent despite different headings.
Linked Issues check ✅ Passed The code and regression test address #32801 by preserving the uploaded File MIME type through request.blob().
Out of Scope Changes check ✅ Passed The changes stay focused on the reported MIME-type bug and its regression test, with no unrelated scope.

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

@robobun

robobun commented Jun 27, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 4:45 PM PT - Jul 8th, 2026

❌ @robobun, your commit 9301f5f has 1 failures in Build #70657 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 32806

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

bun-32806 --bun

@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 `@src/runtime/server/RequestContext.rs`:
- Around line 3853-3859: The zero-body resolve path in on_start_buffering()
still drops request headers, so forward the same fetch headers there that are
already passed in the last-chunk branch. Update the Body::Value::resolve call in
RequestContext’s on_start_buffering logic to use headers from
request_weakref/get_fetch_headers instead of None, matching the existing resolve
flow used for the non-empty body path. Keep the change scoped to the request
blob resolution path so Content-Type survives Content-Length: 0 and header-only
requests.
🪄 Autofix (Beta)

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: b79e66ff-26b1-4023-bdd7-11d56a788cdd

📥 Commits

Reviewing files that changed from the base of the PR and between 96a7627 and 1d4da0c.

📒 Files selected for processing (2)
  • src/runtime/server/RequestContext.rs
  • test/js/bun/http/serve.test.ts

Comment thread src/runtime/server/RequestContext.rs
@robobun

robobun commented Jun 27, 2026 •

Copy link
Copy Markdown
Collaborator Author

This change is green on every lane it affects, including the serve.test.ts regression test, which passes on Linux, Windows, and macOS.

CI has been red across several runs, but on a different unrelated test each time, and never on this change's code or test:

  • build 65242: Windows internal-sourcemap-roundtrip.test.ts (backslash path separator) + Ubuntu v8-heap-snapshot.test.ts (OOM/SIGKILL)
  • build 70652: Windows napi.test.ts (napi_wrap > has the right lifetime, a GC-timing test)
  • build 70657 (re-roll): macOS bun run --no-orphans (perl) and memory probe ... x1200 (both timed out at 30-40s) + a job-level SIGKILL

Each run's failures clear on the next run and are replaced by a different set, which is the signature of cross-platform CI flakiness (timeouts, OOM, GC timing), not a defect in this diff. This PR only threads the request Content-Type header into the server's locked-body resolve path; it does not touch source maps, heap snapshots, NAPI, process reaping, or the proxy memory probe.

I've used my one CI re-roll. The diff is ready for review; the remaining red is pre-existing/flaky infrastructure on lanes unrelated to this change.

(For context, #33128 is a broader, spec-aligned fix for the same issue; this PR is the minimal targeted alternative. Merge order is a maintainer call.)

@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 — minimal, well-tested fix that threads request headers into the locked-body resolve path.

Extended reasoning...

Overview

This PR fixes #32801: request.blob() in a Bun.serve handler was dropping the request's Content-Type header and falling back to text/plain;charset=utf-8. The fix is a 5-line change in src/runtime/server/RequestContext.rs (on_buffered_body_chunk) that passes the request's fetch headers (via request_weakref.get().and_then(get_fetch_headers)) into Body::Value::resolve instead of None. A regression test in test/js/bun/http/serve.test.ts POSTs four File bodies with distinct MIME types and asserts each round-trips through (await request.blob()).type.

Security risks

None. The change only plumbs an already-available request header (Content-Type) through to the resolved Blob's type field. No new parsing, no auth/permissions, no allocation/lifetime changes — request_weakref.get() already returns Option, so a dead weakref degrades to the previous None behavior. The headers are passed as Option<NonNull<FetchHeaders>>, matching the existing resolve signature exactly.

Level of scrutiny

Low. This is a surgical bug fix that mirrors two existing patterns in the codebase: the fetch client path already passes response headers into resolve, and the synchronous get_blob_with_this_value path in Body.rs (line ~2136) already reads Content-Type from BodyMixin::get_fetch_headers for the non-locked case. The change is gated inside if matches!(old, Body::Value::Locked(_)), and resolve's Action::GetBlob arm is the only consumer of the headers argument — it just sets blob.content_type from the header value.

Other factors

The one CodeRabbit suggestion (forward headers in on_start_buffering too) was correctly rebutted (that branch's guard is the exact negation of the condition that produces a Locked body, so headers there would be dead code) and CodeRabbit withdrew it. The PR description explicitly documents this invariant. No CODEOWNERS cover the modified files. The bug-hunting system found no issues. The robobun CI comment notes failures in the build, but the change itself is sound and the test is straightforward; any CI failure is likely unrelated flakiness given the scope of this diff.

@robobun

robobun commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator Author

#33128 also fixes #32801, as a side effect of deriving blob()'s type from the header list per the fetch spec: the normalized type is computed at .blob() time, where the Request and its headers are available, and carried through Action::GetBlob, so Value::resolve no longer takes a headers argument at all. That removes the parameter this PR threads through. #33128 also runs the header through a real MIME parse and serialize, so for example Image/JPEG ; A=B comes back as image/jpeg;a=b rather than the raw header. One of the two should be closed.

robobun and others added 3 commits July 8, 2026 20:37
When a Bun.serve handler reads the request body with request.blob(), the
body is resolved asynchronously after the bytes arrive. That resolve path
passed no headers, so the resulting Blob always fell back to
text/plain;charset=utf-8 instead of the request's Content-Type.

Pass the request's fetch headers into Body::Value::resolve so the Blob
keeps the Content-Type, matching the fetch client path.
@robobun
robobun force-pushed the farm/9059bd42/fix-request-blob-content-type branch from 1d4da0c to df0723c Compare July 8, 2026 20:42
Comment thread test/js/bun/http/serve.test.ts 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 — minimal fix mirroring the fetch-client resolve path, and the nit from my earlier pass was addressed.

Extended reasoning...

Overview

This PR changes one call site in src/runtime/server/RequestContext.rs (on_buffered_body_chunk): instead of passing None for the headers argument to Body::Value::resolve, it now passes the request's fetch headers obtained via request_weakref.get().and_then(BodyMixin::get_fetch_headers). A regression test in test/js/bun/http/serve.test.ts posts File bodies with four distinct MIME types and asserts that (await request.blob()).type round-trips each one.

Security risks

None. The change threads an already-available Option<NonNull<FetchHeaders>> through an existing parameter that resolve already knows how to consume. No new parsing of untrusted input, no allocation, no auth/crypto surface. request_weakref.get() returns None if the request was collected, degrading to the prior behavior.

Level of scrutiny

Low-to-moderate. It touches the HTTP server request-body pipeline, but the change is a 6-line pattern application that exactly mirrors the established fetch-client path in FetchTasklet.rs:793 (response.get_fetch_headers() → resolve(..., headers)). BodyMixin::get_fetch_headers is a simple accessor with no side effects or exception paths. The sibling resolve call in on_start_buffering was deliberately left as None with a well-reasoned justification (that branch cannot run with a Locked body, so headers there would be dead code) — CodeRabbit raised and then withdrew a concern about it after the author explained the invariant.

Other factors

The bug-hunting system found no issues. My earlier nit (bug-history comment in the test) was addressed in 8a33b42. CI on the touched paths is green per the author's build summary. The author flagged that PR #33128 also fixes #32801 via a broader spec-aligned refactor; that's a process/merge-order question for maintainers rather than a defect in this change — this fix is correct on its own and would simply be subsumed if #33128 lands.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favor of #33128, which now carries this fix: it derives request.blob()'s type at .blob() time from the request's header list, so the headers argument to Value::resolve that this PR threads through no longer exists there. The issue repro from this PR (a posted File with four different types round-tripping through request.blob().type) is folded into test/js/web/fetch/body.test.ts on that branch, and #33128 still references #32801 as fixed.

If the larger change turns out not to be wanted, this one can be reopened as the minimal fix for #32801.

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.

Bug: Bun server loses File MIME type when reading request body as Blob

1 participant