webcore: consume body when formData() rejects on MIME type
What does this PR do?
Body.formData() rejected early on an unrecognized Content-Type without touching the body. After the TypeError, bodyUsed stayed false and a subsequent text()/json()/blob() re-read the full payload. Code like try { await req.formData() } catch { await req.json() } silently worked in Bun and threw everywhere else.
The spec's consume body fully reads the body before packageData runs (which performs the MIME check and may throw), so a failed MIME check must still consume the body. Node/undici match the spec: bodyUsed is true after the rejection and further reads throw TypeError.
// before: {formData:"REJECTED TypeError", bodyUsedAfterReject:false, thenText:'RESOLVED "hello world"'}
// after: {formData:"REJECTED TypeError", bodyUsedAfterReject:true, thenText:'REJECTED TypeError'}
// node: {formData:"REJECTED TypeError", bodyUsedAfterReject:true, thenText:'REJECTED TypeError'}
const r = new Response("hello world");
let fd; try { await r.formData(); fd = "RESOLVED"; } catch (e) { fd = "REJECTED " + e.constructor.name; }
const usedAfterReject = r.bodyUsed;
let t; try { t = "RESOLVED " + JSON.stringify(await r.text()); } catch (e) { t = "REJECTED " + e.constructor.name; }
console.log({ formData: fd, bodyUsedAfterReject: usedAfterReject, thenText: t });
How did you verify your code works?
New tests in test/js/web/fetch/body.test.ts cover string / Uint8Array / Blob / ReadableStream / JSON bodies on both Request and Response, a null body (stays unconsumed, matching Node), and an incoming Bun.serve request body. All fail on the previous build and pass with the fix.
Fix
- Synchronous body: call
use_as_any_blob()before the encoding check so the body is markedUsedfirst. - Locked body: pass the
Option<encoder>throughAction::GetFormData. WithNone, the ReadableStream is still drained (via afalsecontentType sentinel that the fulfillment handler turns into theERR_FORMDATA_PARSE_ERRORrejection), and a pending HTTP body is still buffered beforeresolve()rejects. - Null body: unchanged (
use_as_any_blob()leavesNullasNull), matching Node.
[review] gate passed · iteration 1 · 3 files touched
fails on main (without fix)
ASAN without fix: 11 failed, 4 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/web/fetch/body.test.ts
bun test v1.4.0 (02ea67da4)
test/js/web/fetch/body.test.ts:
(pass) Request > constructor > undefined [5.42ms]
(pass) Request > constructor > null [3.80ms]
(pass) Request > constructor > string > "" [6.92ms]
(pass) Request > constructor > string > "Hello world" [1.70ms]
(pass) Request > constructor > string > "🫠" [1.54ms]
(pass) Request > constructor > string > "⁉️" [1.16ms]
(pass) Request > constructor > ArrayBuffer > empty buffer [10.60ms]
(pass) Request > constructor > ArrayBuffer > small buffer [5.63ms]
(pass) Request > constructor > ArrayBuffer > large buffer [5.09ms]
(pass) Request > constructor > SharedArrayBuffer > empty buffer [2.21ms]
(pass) Request > constructor > SharedArrayBuffer > small buffer [2.09ms]
(pass) Request > constructor > SharedArrayBuffer > large buffer [4.18ms]
(pass) Request > constructor > Buffer > empty buffer [1.89ms]
(pass) Request > constructor > Buffer > small buffer [1.78ms]
(pass) Request > constructor > Buffer > large buffer [4.19ms]
(pass) Request > constructor
... (truncated)
release without fix: 4 skipped
bun test v1.4.0-canary.1 (1f5cfdc95)
test/js/web/fetch/body.test.ts:
(pass) Request > constructor > undefined [5.17ms]
(pass) Request > constructor > null [0.12ms]
(pass) Request > constructor > string > "" [0.13ms]
(pass) Request > constructor > string > "Hello world" [0.02ms]
(pass) Request > constructor > string > "🫠" [0.02ms]
(pass) Request > constructor > string > "⁉️"
(pass) Request > constructor > ArrayBuffer > empty buffer [0.14ms]
(pass) Request > constructor > ArrayBuffer > small buffer [3.39ms]
(pass) Request > constructor > ArrayBuffer > large buffer [1.64ms]
(pass) Request > constructor > SharedArrayBuffer > empty buffer [0.04ms]
(pass) Request > constructor > SharedArrayBuffer > small buffer [0.03ms]
(pass) Request > constructor > SharedArrayBuffer > large buffer [2.11ms]
(pass) Request > constructor > Buffer > empty buffer [0.02ms]
(pass) Request > constructor > Buffer > small buffer [0.02ms]
(pass) Request > constructor > Buffer > large buffer [1.70ms]
(pass) Request > constructor > Uint8Array > empty buffer [0.01ms]
(pass) Request > constructor > Uint8Array > small buffer [0.04ms]
(pass) Request > constructor > Uint8Array > large buffer [1.5
... (truncated)
passes on PR (with fix)
ASAN with fix: 4 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/web/fetch/body.test.ts
bun test v1.4.0 (02ea67da4)
test/js/web/fetch/body.test.ts:
(pass) Request > constructor > undefined [4.98ms]
(pass) Request > constructor > null [3.08ms]
(pass) Request > constructor > string > "" [6.27ms]
(pass) Request > constructor > string > "Hello world" [1.56ms]
(pass) Request > constructor > string > "🫠" [1.44ms]
(pass) Request > constructor > string > "⁉️" [1.10ms]
(pass) Request > constructor > ArrayBuffer > empty buffer [9.92ms]
(pass) Request > constructor > ArrayBuffer > small buffer [5.21ms]
(pass) Request > constructor > ArrayBuffer > large buffer [5.39ms]
(pass) Request > constructor > SharedArrayBuffer > empty buffer [2.36ms]
(pass) Request > constructor > SharedArrayBuffer > small buffer [2.01ms]
(pass) Request > constructor > SharedArrayBuffer > large buffer [4.04ms]
(pass) Request > constructor > Buffer > empty buffer [1.61ms]
(pass) Request > constructor > Buffer > small buffer [1.53ms]
(pass) Request > constructor > Buffer > large buffer [3.60ms]
(pass) Request > constructor
... (truncated)
release with fix: 4 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 909ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/8] gen cpp.rs (cppbind)
[2/8] gen generated_host_exports.rs
generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 240 extern-C blocks audited
[2/8] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)
[1m[92m Compiling[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
[1m[92m Compiling[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
[1m[92m Compiling[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
[1m[92m Compiling[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
[1m[92m Compiling[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
[1m[92m Compiling[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
[1m[92m Compiling[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
[1m[92m Compiling[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
[1m[92m Compiling[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
[1m[92m
... (truncated)
diff hotspot
.../webcore/streams/BunStreamConsumers.cpp | 5 ++
src/runtime/webcore/Body.rs | 68 +++++++++----------
test/js/web/fetch/body.test.ts | 76 ++++++++++++++++++++++
3 files changed, 115 insertions(+), 34 deletions(-)
gate history · 2 passed · 0 rejected · iteration 1
evidence per changed file
file reads edits tests
src/jsc/bindings/webcore/streams/BunStreamConsumers.cpp 4 4 0
src/runtime/webcore/Body.rs 8 10 0
test/js/web/fetch/body.test.ts 2 1 0