Repository navigation
Conversation
…data-mime-check-consumes-body
Collaborator
Author
|
Updated 5:35 PM PT - Oct 11th, 2026
✅ @robobun, your commit a02fcc19ca885a7c9e3ae701fb9e2b33c1b306b5 passed in 🧪 To try this PR locally: bunx bun-pr 44969That installs a local version of the PR into your bun-44969 --bun |
Collaborator
Author
|
Reproduced on the released 1.4.3 line with two reduced programs, each run on that build and on this branch:
Test: This is a draft. It is a breaking change and needs a maintainer's yes. #44999 fixes the locked stream of 1.4.3 with no behaviour change (a failed |
This was referenced Oct 11, 2026
Collaborator
Author
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
formData()looks at the type, so aformData()that rejects on the type has used the body up. WPT asserts it (fetch/api/response/response-consume.html,checkBodyFormDataError). Bun leavesbodyUsedfalse.get_form_data(src/runtime/webcore/Body.rs) checks the type before it reads anything.Fix
formData()reads the body first, then rejects with the sameERR_FORMDATA_PARSE_ERROR.bodyUsedis then true for every body that is not null.readableStreamConsumeThenRejectdrains a stream body. A body still arriving waits for its end.test/js/web/fetch/body.test.ts(47 new cases, 46 fail on main). Mirrored WPT assertions: 15 of 15 (1.4.3: 6).Background
x.clone().formData(), or test the Content-Type before the call.Downsides
try { await r.formData() } catch { await r.json() }works on every Bun release. Herer.json()rejects withERR_BODY_ALREADY_USED, also in Bun'snode-fetch.Content-Type.Notes
breaking.response-consume.html,response-consume-empty.any.js,request-consume-empty.any.js), 15 cases: node v26.3.0 passes 15, the 1.4.3 line passes 6, this branch passes 15. The 9 that 1.4.3 fails are thecheckBodyFormDataErrorcases. For the stream error shape (response-error-from-stream.any.js):new Response(erroringStream).formData()rejects withERR_FORMDATA_PARSE_ERRORon 1.4.3, and with the stream's own error in node and here..any.jsfiles andresponse-consume.htmlis a page, so it needs a driver of its own. The cases are inbody.test.ts.node-fetch: Bun's built-innode-fetchis the nativeResponse, so a failedformData()uses the body up there too. node-fetch 3.3.2 checks the type before it reads (toFormDatathrows first), so its body stays unused. A maintainer must choose which one Bun'snode-fetchfollows.try { await req.formData() } catch {}followed byfetch(new Request(upstream, req))forwards 0 bytes with status 200 on this branch. 1.4.3 forwards the body. Node throws, becausenew Request()refuses a used body, and that is what those two PRs add.formData()onnew Response(new Blob(["x"], { type: "text/plain" })),headers.get("content-type")isnullhere. 1.4.3 and node keep it. The type lives only on the Blob until.headersis read, and the Blob is gone once the body is used. The other readers (text(),json()) lose it the same way on main.req.formData()with no type check, and a 64 MBapplication/octet-streamupload. 1.4.3 answers 415 after 2 MB were sent, in 0 ms. This branch answers after all 64 MB, in 426 ms, with +67 MB RSS. The drain buffers the body as the other readers do.try { data = await r.formData() } catch { data = await r.json() }on a JSON request prints{"a":1}on 1.4.3 andjson() threw TypeErrorhere. A search of public code for that exact fallback found one use. Most code reads the Content-Type first. Node and browsers already fail that fallback.falseas the content type to mean "drain, then reject". That sentinel is reachable from user code throughBun.readableStreamToFormData(stream, false). Here the drain is its own entry point and its own reaction handler (onReadableStreamConsumedThenReject), andAction::GetFormData(None)carries "not form data" on the Rust side.get_form_dataafter the change:Locked):set_promisewithAction::GetFormData(None). A stream is drained byreadableStreamConsumeThenReject. A body that is still arriving rejects inValue::resolvewhen its last chunk is in.Used, and the call rejects.bodyUsedis false for a null body after every reader.get_form_data. Whichever lands second is merged onto the other. Bun.serve: share a route's payload with the Response's unread .body stream #44930 fixes the same lift inBun.serveroutes and does not depend on this PR.Content-Type(stated, with the PRs it needs), the stream error case was not claimed (added). Open:node-fetch, and the WPT files.body(823 pass on the release build. On the debug build the four "string body consumption does not leak" cases time out at 5 s, as they do without this change),body-clone,body-mixin-errors,FormData,streams.isDebugbuilds now. Release and ASAN builds run them as before.