Skip to content

FormData: match WebKit's multipart boundary exactly#29631

Merged
Jarred-Sumner merged 1 commit into
mainfrom
farm/68b06d9d/formdata-webkit-boundary
Apr 23, 2026
Merged

FormData: match WebKit's multipart boundary exactly#29631
Jarred-Sumner merged 1 commit into
mainfrom
farm/68b06d9d/formdata-webkit-boundary

Conversation

@robobun

@robobun robobun commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

Fixes #29630.

Bug

Bun's FormData generated a multipart boundary that doesn't match any
other mainstream client:

Client Declared boundary Body marker
WebKit browsers ----WebKitFormBoundary{hex} (4 dashes) ------WebKitFormBoundary{hex} (6 dashes)
undici ----formdata-undici-0{N} (4 dashes) ------formdata-undici-0{N} (6 dashes)
Bun (before) -WebkitFormBoundary{hex} (1 dash) ---WebkitFormBoundary{hex} (3 dashes)

Note also the lowercase k — WebKit browsers use capital K.

This unique format triggers bugs in some downstream multipart parsers
(the reporter hit a deterministic 400 from OpenAI's files.create
endpoint on specific binary payloads that uploads fine from Node/undici).

Cause

Single site, src/bun.js/webcore/Blob.zig:588:

break :brk std.fmt.bufPrint(&hex_buf, "-WebkitFormBoundary{x}", .{&random}) catch unreachable;

Fix

Change the format string to match WebKit exactly
(vendor/WebKit/Source/WebCore/platform/network/FormDataBuilder.cpp:142
uses "----WebKitFormBoundary"):

break :brk std.fmt.bufPrint(&hex_buf, "----WebKitFormBoundary{x}", .{&random}) catch unreachable;

The 22-byte prefix + 32 hex chars (54 bytes) still fits in the existing
[70]u8 buffer. The usage sites at lines 603–605 already emit
"--" + boundary + "--\r\n", so they automatically produce the correct
6-dash body markers without changes.

Verification

Before:

content-type: multipart/form-data; boundary=-WebkitFormBoundary22a1c702...
body: ---WebkitFormBoundary22a1c702...\r\n...

After:

content-type: multipart/form-data; boundary=----WebKitFormBoundary5ec838da...
body: ------WebKitFormBoundary5ec838da...\r\n...

Tests

Added two tests to test/js/bun/http/form-data-set-append.test.js
that assert:

  • content-type matches ^multipart/form-data; boundary=----WebKitFormBoundary[0-9a-f]+$
  • body starts with ------WebKitFormBoundary...\r\n
  • body ends with \r\n--<boundary>--\r\n

Gate check:

  • Stashed src changes → both new tests fail (declared boundary has 1 dash, lowercase k).
  • Restored src changes → all 4 tests in the file pass.

@robobun

robobun commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 2:12 AM PT - Apr 23rd, 2026

@robobun, your commit ce16160 has 4 failures in Build #47463 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 29631

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

bun-29631 --bun

@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 — single format-string change to match WebKit's boundary, buffer still fits, well-covered by new tests.

Extended reasoning...

Overview

This PR changes one format string in src/bun.js/webcore/Blob.zig:588 from "-WebkitFormBoundary{x}" to "----WebKitFormBoundary{x}", aligning Bun's generated multipart boundary with WebKit's exact format (4 leading dashes, capital K). Two regression tests are added to test/js/bun/http/form-data-set-append.test.js asserting the declared boundary, body opener (6 dashes), and terminator.

Security risks

None. The boundary is still derived from a random UUID; only the static prefix changes. No parsing, auth, crypto, or permission logic is touched.

Level of scrutiny

Low. This is a one-line literal change in production code. I verified:

  • The new prefix (22 bytes) + 32 hex chars = 54 bytes, which fits the existing [70]u8 buffer and stays within RFC 2046's 70-char boundary limit.
  • The downstream usage at lines 603–605/609 reads boundary as an opaque slice, so the extra dashes flow through correctly to body markers and the Content-Type header without further changes.
  • Other repo references to WebKitFormBoundary (in streams.test.js and bun.d.ts) are independent hardcoded fixtures/docs for parsing and don't depend on Bun's generated boundary format.

Other factors

No bugs found by the bug hunter, no outstanding reviewer comments, and the PR includes a gate check showing the new tests fail on the old code and pass on the new code. The change is mechanical and self-contained.

@coderabbitai

coderabbitai Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

FormData multipart boundary generation changed from -WebkitFormBoundary{uuid} to ----WebKitFormBoundary{uuid} (four leading dashes and capital K). Tests were added to assert the new boundary format and that emitted multipart body markers and terminator match the declared boundary.

Changes

Cohort / File(s) Summary
FormData Boundary Format
src/bun.js/webcore/Blob.zig
Changed the hard-coded multipart boundary prefix from -WebkitFormBoundary to ----WebKitFormBoundary (adds three leading dashes and capitalizes K).
Boundary Format Tests
test/js/bun/http/form-data-set-append.test.js
Added two tests asserting Content-Type: multipart/form-data; boundary=----WebKitFormBoundary{32 hex} and that multipart body markers and final terminator use the matching WebKit-style boundary.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change—updating FormData's multipart boundary format to match WebKit's standard.
Description check ✅ Passed The description comprehensively covers the bug, cause, fix, verification, and tests, closely matching the template structure with detailed technical context.
Linked Issues check ✅ Passed The code changes fully address all requirements from issue #29630: boundary format corrected from '-WebkitFormBoundary' to '----WebKitFormBoundary', capitalization fixed, and comprehensive tests added [#29630].
Out of Scope Changes check ✅ Passed All changes are directly scoped to the FormData multipart boundary fix; no unrelated modifications to other features or systems detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@robobun

robobun commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator Author

CI red — investigated, all failures are unrelated pre-existing Windows/memory flakes, nothing touches FormData/Blob:

  • test/js/bun/net/socket.test.ts should not leak memory — heapStats objectTypeCount off-by-one (4 vs ≤3). Same failure on Bun.spawn: don't close user-provided extra stdio fds #29621 (unrelated change).
  • test/cli/run/require-cache.test.ts files transpiled and loaded don't leak file paths — RSS leak detection (107 MB) on Windows 2019 x64.
  • test/bake/dev-and-prod.test.ts hmr handles rapid consecutive edits — HMR 15 s timeout.
  • test/bake/dev/incremental-graph-edge-deletion.test.ts — HMR cycle test.
  • Build-zig flakes on aarch64-musl / darwin-x64 / linux-x64-baseline / linux-x64 (same source passed on 6 other build-zig configs, including aarch64 and musl).

None of the failing tests reference FormData, Blob, or multipart code. Can a maintainer please retry the failed jobs?

@robobun robobun force-pushed the farm/68b06d9d/formdata-webkit-boundary branch from 11231fe to b889951 Compare April 23, 2026 07:11

@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 — one-line format string fix with regression tests; the inline comment-ordering nit is non-blocking.

Extended reasoning...

Overview

This PR changes a single format string in src/bun.js/webcore/Blob.zig:588 from "-WebkitFormBoundary{x}" to "----WebKitFormBoundary{x}" so Bun's FormData multipart boundary matches WebKit (and undici's 4-dash convention) exactly. It also adds two regression tests to test/js/bun/http/form-data-set-append.test.js asserting the declared boundary, body opener, and terminator formats.

I verified the surrounding code: the [70]u8 buffer comfortably holds the new 54-byte string (22-byte prefix + 32 hex chars), and the body-marker emission at lines 603–605 ("--" + boundary + "--\r\n") automatically yields 6-dash markers without further changes. A grep for the old WebkitFormBoundary (lowercase k) shows no other code or tests depend on it; the WebKitFormBoundary references in streams.test.js are hardcoded parser fixtures, unaffected by the generator change.

Security risks

None. The boundary still includes 32 hex chars of UUID-derived randomness, so uniqueness/unpredictability is unchanged. This is purely a cosmetic prefix adjustment for compatibility with picky downstream parsers.

Level of scrutiny

Low. This is a mechanical, well-understood string-literal fix with a clear root cause, a vendored WebKit reference confirming the target format, and gate-checked tests. No CODEOWNERS apply to either touched file.

Other factors

  • The only bug-hunter finding is a cosmetic nit: the new tests were inserted between an existing #12325 issue-link comment and its two tests, leaving the comment slightly orphaned. Zero runtime impact; not worth blocking merge.
  • CI failures were investigated by robobun and are all pre-existing Windows/memory/HMR flakes unrelated to FormData/Blob.
  • No prior claude[bot] reviews on this PR.

Comment thread test/js/bun/http/form-data-set-append.test.js Outdated

@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 the current code and only fix it if needed.

Inline comments:
In `@test/js/bun/http/form-data-set-append.test.js`:
- Around line 33-43: The test "FormData via Response uses WebKit-compatible
boundary (`#29630`)" should parse the boundary from contentType (using
res.headers.get("content-type")) and bind the body assertions to that parsed
boundary instead of hardcoding the "------WebKitFormBoundary" prefix; update the
test to extract the boundary token from contentType (the part after
"boundary="), assert the body starts with the exact boundary prefixed by "--"
and also assert the body contains the correct closing delimiter ("--" + boundary
+ "--") so the Response-path framing matches the header-parsed boundary.
🪄 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: 5c1d64b5-c14e-4960-b506-53ef015f372c

📥 Commits

Reviewing files that changed from the base of the PR and between 11231fe3b0a03d6e7cb67b4755c058d0e46b1c22 and b8899515cf0ed3331de08e638e1366585471c7e6.

📒 Files selected for processing (2)
  • src/bun.js/webcore/Blob.zig
  • test/js/bun/http/form-data-set-append.test.js

Comment thread test/js/bun/http/form-data-set-append.test.js
@robobun robobun force-pushed the farm/68b06d9d/formdata-webkit-boundary branch from b889951 to 2a1c71b Compare April 23, 2026 07:31

@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 — one-line format-string fix, buffer still fits (22+32=54 ≤ 70); the earlier test-ordering nit is non-blocking.

Extended reasoning...

Overview

Single production change: src/bun.js/webcore/Blob.zig:588 swaps the multipart boundary format string from "-WebkitFormBoundary{x}" to "----WebKitFormBoundary{x}" (3 extra leading dashes + capital K) so Bun's FormData serialization matches WebKit's exactly. Two regression tests added to test/js/bun/http/form-data-set-append.test.js. Verified the only generation site is this line; other WebKitFormBoundary references in the repo are parser-side test fixtures (test/js/web/streams/streams.test.js) and a doc comment (bun.d.ts), both independent of what Bun emits.

Security risks

None. The boundary is a static prefix + random UUID hex; no user input flows into the format string. The new 54-byte output (22-byte prefix + 32 hex) fits the existing [70]u8 stack buffer with room to spare, so no overflow risk. The downstream framing at lines 603–605/609 already concatenates "--" + boundary and boundary={s} generically, so no length assumptions are violated.

Level of scrutiny

Low. This is a cosmetic/compat tweak to a literal string with no control-flow, allocation, or API-surface changes. The previous value was already RFC-2046-valid (any boundary ≤70 chars works); the fix only aligns dash count and casing with mainstream clients to dodge buggy third-party parsers. CODEOWNERS does not cover these paths.

Other factors

I previously left a 🟡 nit about the new tests being inserted between the #12325 comment header and the tests it labels — that's purely a readability concern in the test file and doesn't affect correctness, so it shouldn't block. CodeRabbit's resolved nit about strengthening the Response-path test is likewise optional (the Request-path test already covers the header↔body binding). CI failures were triaged by robobun as pre-existing Windows/HMR flakes unrelated to FormData/Blob.

@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.

♻️ Duplicate comments (1)
test/js/bun/http/form-data-set-append.test.js (1)

33-43: ⚠️ Potential issue | 🟡 Minor

Strengthen Response-path framing checks against parsed boundary.

Line 42 only checks a hardcoded prefix. This can miss header/body boundary mismatches; parse boundary from content-type and assert both opening and closing delimiters like the Request test.

Suggested patch
 test("FormData via Response uses WebKit-compatible boundary (`#29630`)", async () => {
   const fd = new FormData();
   fd.append("field", "value");

   const res = new Response(fd);
   const contentType = res.headers.get("content-type");
   expect(contentType).toMatch(/^multipart\/form-data; boundary=----WebKitFormBoundary[0-9a-f]{32}$/);
+  const boundary = contentType.slice("multipart/form-data; boundary=".length);

   const body = await res.text();
-  expect(body.startsWith("------WebKitFormBoundary")).toBe(true);
+  expect(body.startsWith(`--${boundary}\r\n`)).toBe(true);
+  expect(body.endsWith(`\r\n--${boundary}--\r\n`)).toBe(true);
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/js/bun/http/form-data-set-append.test.js` around lines 33 - 43, The test
"FormData via Response uses WebKit-compatible boundary (`#29630`)" currently only
checks that body starts with a hardcoded prefix; instead parse the boundary from
the contentType (value in contentType / variable contentType) and use that
parsed boundary to assert both the opening delimiter (e.g., `--${boundary}`)
appears at the start of body (variable body) and the closing delimiter
(`--${boundary}--`) appears at the end, mirroring the Request test behavior;
update the assertions to extract boundary from res.headers.get("content-type")
and validate opening and closing delimiters consistently rather than using a
hardcoded prefix.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@test/js/bun/http/form-data-set-append.test.js`:
- Around line 33-43: The test "FormData via Response uses WebKit-compatible
boundary (`#29630`)" currently only checks that body starts with a hardcoded
prefix; instead parse the boundary from the contentType (value in contentType /
variable contentType) and use that parsed boundary to assert both the opening
delimiter (e.g., `--${boundary}`) appears at the start of body (variable body)
and the closing delimiter (`--${boundary}--`) appears at the end, mirroring the
Request test behavior; update the assertions to extract boundary from
res.headers.get("content-type") and validate opening and closing delimiters
consistently rather than using a hardcoded prefix.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 53e50538-bc4a-4cea-9c95-9673921d5faa

📥 Commits

Reviewing files that changed from the base of the PR and between b8899515cf0ed3331de08e638e1366585471c7e6 and 2a1c71b641093f099cb7976a73bf7565a48f87c6.

📒 Files selected for processing (2)
  • src/bun.js/webcore/Blob.zig
  • test/js/bun/http/form-data-set-append.test.js

FormData was generating "-WebkitFormBoundary{hex}" (1 leading dash,
lowercase k), producing 3-dash body markers. WebKit browsers emit
"----WebKitFormBoundary{hex}" (4 dashes, capital K → 6-dash markers)
and undici emits a 4-dash form; Bun's unique shape trips some buggy
downstream multipart parsers (e.g. some OpenAI endpoints).

Change the format string to match WebKit exactly.

Fixes #29630
@robobun robobun force-pushed the farm/68b06d9d/formdata-webkit-boundary branch from 2a1c71b to ce16160 Compare April 23, 2026 07:35

@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 — one-line boundary literal fix to match WebKit, with tests; thanks for fixing the comment placement.

Extended reasoning...

Overview

Single-character-class change to a format string literal in src/bun.js/webcore/Blob.zig:588: "-WebkitFormBoundary{x}""----WebKitFormBoundary{x}" (4 leading dashes, capital K), aligning Bun's multipart boundary with WebKit's FormDataBuilder.cpp. Two regression tests added to test/js/bun/http/form-data-set-append.test.js. Grep confirms this is the only boundary-generation site in src/.

Security risks

None. The boundary remains derived from a random UUID (16 bytes → 32 hex chars), so entropy is unchanged. The change only adds three dashes and fixes capitalization in a delimiter string — no parsing, auth, or memory-safety implications. Buffer math verified: 22-byte prefix + 32 hex = 54 bytes, well within the existing [70]u8 stack buffer, so the catch unreachable on bufPrint remains sound.

Level of scrutiny

Low. This is a cosmetic interop fix to a string literal with no control-flow changes. The header (content_type at line 609) and body markers (lines 603–605) both reference the same boundary slice, so they stay consistent automatically. The added tests assert the exact format (4-dash declared boundary, 6-dash body markers, correct terminator) for both Request and Response paths.

Other factors

My prior nit (orphaned #12325 issue-link comment) was addressed in ce16160 — the link now sits directly above its two tests. CodeRabbit's trivial nit about tightening the Response-path test is marked resolved; the Request-path test already provides full header/body binding coverage, so the lighter Response test is fine as a supplementary check. CI failures were investigated by the author and are unrelated pre-existing flakes (Windows memory/HMR), none touching FormData/Blob.

@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.

♻️ Duplicate comments (1)
test/js/bun/http/form-data-set-append.test.js (1)

31-41: ⚠️ Potential issue | 🟡 Minor

Bind Response body assertions to the parsed boundary and assert the closing delimiter.

Line 40 currently checks only a hardcoded prefix. This can miss header/body boundary mismatches on the Response path.

🔧 Suggested test tightening
 test("FormData via Response uses WebKit-compatible boundary (`#29630`)", async () => {
   const fd = new FormData();
   fd.append("field", "value");

   const res = new Response(fd);
   const contentType = res.headers.get("content-type");
   expect(contentType).toMatch(/^multipart\/form-data; boundary=----WebKitFormBoundary[0-9a-f]{32}$/);
+  const boundary = contentType.slice("multipart/form-data; boundary=".length);

   const body = await res.text();
-  expect(body.startsWith("------WebKitFormBoundary")).toBe(true);
+  expect(body.startsWith(`--${boundary}\r\n`)).toBe(true);
+  expect(body.endsWith(`\r\n--${boundary}--\r\n`)).toBe(true);
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/js/bun/http/form-data-set-append.test.js` around lines 31 - 41, The test
"FormData via Response uses WebKit-compatible boundary (`#29630`)" currently only
asserts a hardcoded prefix; instead parse the actual boundary from contentType
(using the same regex that matches /^multipart\/form-data; boundary=(.+)$/ or
the existing WebKit regex) and then assert the Response body begins with "--" +
boundary and also contains the proper closing delimiter "--" + boundary + "--"
(or ends with that closing delimiter possibly preceded by CRLF); update the
assertions around contentType and body in this test (variables: fd, res,
contentType, body) to extract boundary and assert both opening and closing
delimiters match it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@test/js/bun/http/form-data-set-append.test.js`:
- Around line 31-41: The test "FormData via Response uses WebKit-compatible
boundary (`#29630`)" currently only asserts a hardcoded prefix; instead parse the
actual boundary from contentType (using the same regex that matches
/^multipart\/form-data; boundary=(.+)$/ or the existing WebKit regex) and then
assert the Response body begins with "--" + boundary and also contains the
proper closing delimiter "--" + boundary + "--" (or ends with that closing
delimiter possibly preceded by CRLF); update the assertions around contentType
and body in this test (variables: fd, res, contentType, body) to extract
boundary and assert both opening and closing delimiters match it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1df60629-2e9d-43e0-aa2b-84ac94eb39e5

📥 Commits

Reviewing files that changed from the base of the PR and between 2a1c71b641093f099cb7976a73bf7565a48f87c6 and ce16160.

📒 Files selected for processing (2)
  • src/bun.js/webcore/Blob.zig
  • test/js/bun/http/form-data-set-append.test.js

@Jarred-Sumner Jarred-Sumner merged commit f675c9f into main Apr 23, 2026
58 of 62 checks passed
@Jarred-Sumner Jarred-Sumner deleted the farm/68b06d9d/formdata-webkit-boundary branch April 23, 2026 19:29
Jarred-Sumner pushed a commit that referenced this pull request Apr 24, 2026
…mory' (#29663)

`test/js/bun/net/socket.test.ts` "should not leak memory" has been
hitting `TCPSocket`/`TLSSocket` count `4` on the Windows 2019 agents,
one past the previous `isWindows ? 3` bound. Same flake shipped with
merged #29631 today and has been blocking #29593 through ~15 retriggers.

Non-Windows bound stays at `2` — that's where real retention regressions
are caught. The Windows bound is already loosened for its shared
prototype/structure retention; this just accommodates the one additional
retained instance the Win2019 lane is observing.

## Observed

- Build [#47600 Win2019
x64](https://buildkite.com/bun/bun/builds/47600#019dbc49-ff0c-49c2-afaa-134711815456)
— `Received: 4, Expected: <= 3`
- Build [#47616 Win2019
x64](https://buildkite.com/bun/bun/builds/47616#019dbc99-fc61-4111-8170-080d9729866a)
— same
- Multiple prior PRs:
[#29631](#29631),
[#29651](#29651),
[#29649](#29649),
[#29650](#29650),
[#29645](#29645),
[#29639](#29639) all hit the same
pattern

## Scope

3-line diff + comment. If the Win2019 retention count IS a regression
worth investigating, that's a separate issue; this change is strictly
about not holding other PRs hostage to the shared flake.

Co-authored-by: robobun <robobun@bun.sh>
structwafel pushed a commit to structwafel/bun that referenced this pull request Apr 25, 2026
Fixes oven-sh#29630.

## Bug

Bun's `FormData` generated a multipart boundary that doesn't match any
other mainstream client:

| Client | Declared boundary | Body marker |
| ----------------- | ------------------------------------------ |
------------------------------------------ |
| WebKit browsers | `----WebKitFormBoundary{hex}` (4 dashes) |
`------WebKitFormBoundary{hex}` (6 dashes) |
| undici | `----formdata-undici-0{N}` (4 dashes) |
`------formdata-undici-0{N}` (6 dashes) |
| **Bun (before)** | `-WebkitFormBoundary{hex}` (**1 dash**) |
`---WebkitFormBoundary{hex}` (**3 dashes**) |

Note also the lowercase `k` — WebKit browsers use capital `K`.

This unique format triggers bugs in some downstream multipart parsers
(the reporter hit a deterministic 400 from OpenAI's `files.create`
endpoint on specific binary payloads that uploads fine from
Node/undici).

## Cause

Single site, `src/bun.js/webcore/Blob.zig:588`:

```zig
break :brk std.fmt.bufPrint(&hex_buf, "-WebkitFormBoundary{x}", .{&random}) catch unreachable;
```

## Fix

Change the format string to match WebKit exactly
(`vendor/WebKit/Source/WebCore/platform/network/FormDataBuilder.cpp:142`
uses `"----WebKitFormBoundary"`):

```zig
break :brk std.fmt.bufPrint(&hex_buf, "----WebKitFormBoundary{x}", .{&random}) catch unreachable;
```

The 22-byte prefix + 32 hex chars (54 bytes) still fits in the existing
`[70]u8` buffer. The usage sites at lines 603–605 already emit
`"--" + boundary + "--\r\n"`, so they automatically produce the correct
6-dash body markers without changes.

## Verification

Before:
```
content-type: multipart/form-data; boundary=-WebkitFormBoundary22a1c702...
body: ---WebkitFormBoundary22a1c702...\r\n...
```

After:
```
content-type: multipart/form-data; boundary=----WebKitFormBoundary5ec838da...
body: ------WebKitFormBoundary5ec838da...\r\n...
```

## Tests

Added two tests to `test/js/bun/http/form-data-set-append.test.js`
that assert:
- `content-type` matches `^multipart/form-data;
boundary=----WebKitFormBoundary[0-9a-f]+$`
- body starts with `------WebKitFormBoundary...\r\n`
- body ends with `\r\n--<boundary>--\r\n`

Gate check:
- Stashed src changes → both new tests **fail** (declared boundary has 1
dash, lowercase k).
- Restored src changes → all 4 tests in the file **pass**.

Co-authored-by: robobun <robobun@users.noreply.github.com>
structwafel pushed a commit to structwafel/bun that referenced this pull request Apr 25, 2026
…mory' (oven-sh#29663)

`test/js/bun/net/socket.test.ts` "should not leak memory" has been
hitting `TCPSocket`/`TLSSocket` count `4` on the Windows 2019 agents,
one past the previous `isWindows ? 3` bound. Same flake shipped with
merged oven-sh#29631 today and has been blocking oven-sh#29593 through ~15 retriggers.

Non-Windows bound stays at `2` — that's where real retention regressions
are caught. The Windows bound is already loosened for its shared
prototype/structure retention; this just accommodates the one additional
retained instance the Win2019 lane is observing.

## Observed

- Build [#47600 Win2019
x64](https://buildkite.com/bun/bun/builds/47600#019dbc49-ff0c-49c2-afaa-134711815456)
— `Received: 4, Expected: <= 3`
- Build [#47616 Win2019
x64](https://buildkite.com/bun/bun/builds/47616#019dbc99-fc61-4111-8170-080d9729866a)
— same
- Multiple prior PRs:
[oven-sh#29631](oven-sh#29631),
[oven-sh#29651](oven-sh#29651),
[oven-sh#29649](oven-sh#29649),
[oven-sh#29650](oven-sh#29650),
[oven-sh#29645](oven-sh#29645),
[oven-sh#29639](oven-sh#29639) all hit the same
pattern

## Scope

3-line diff + comment. If the Win2019 retention count IS a regression
worth investigating, that's a separate issue; this change is strictly
about not holding other PRs hostage to the shared flake.

Co-authored-by: robobun <robobun@bun.sh>
xhjkl pushed a commit to xhjkl/bun that referenced this pull request May 14, 2026
Fixes oven-sh#29630.

## Bug

Bun's `FormData` generated a multipart boundary that doesn't match any
other mainstream client:

| Client | Declared boundary | Body marker |
| ----------------- | ------------------------------------------ |
------------------------------------------ |
| WebKit browsers | `----WebKitFormBoundary{hex}` (4 dashes) |
`------WebKitFormBoundary{hex}` (6 dashes) |
| undici | `----formdata-undici-0{N}` (4 dashes) |
`------formdata-undici-0{N}` (6 dashes) |
| **Bun (before)** | `-WebkitFormBoundary{hex}` (**1 dash**) |
`---WebkitFormBoundary{hex}` (**3 dashes**) |

Note also the lowercase `k` — WebKit browsers use capital `K`.

This unique format triggers bugs in some downstream multipart parsers
(the reporter hit a deterministic 400 from OpenAI's `files.create`
endpoint on specific binary payloads that uploads fine from
Node/undici).

## Cause

Single site, `src/bun.js/webcore/Blob.zig:588`:

```zig
break :brk std.fmt.bufPrint(&hex_buf, "-WebkitFormBoundary{x}", .{&random}) catch unreachable;
```

## Fix

Change the format string to match WebKit exactly
(`vendor/WebKit/Source/WebCore/platform/network/FormDataBuilder.cpp:142`
uses `"----WebKitFormBoundary"`):

```zig
break :brk std.fmt.bufPrint(&hex_buf, "----WebKitFormBoundary{x}", .{&random}) catch unreachable;
```

The 22-byte prefix + 32 hex chars (54 bytes) still fits in the existing
`[70]u8` buffer. The usage sites at lines 603–605 already emit
`"--" + boundary + "--\r\n"`, so they automatically produce the correct
6-dash body markers without changes.

## Verification

Before:
```
content-type: multipart/form-data; boundary=-WebkitFormBoundary22a1c702...
body: ---WebkitFormBoundary22a1c702...\r\n...
```

After:
```
content-type: multipart/form-data; boundary=----WebKitFormBoundary5ec838da...
body: ------WebKitFormBoundary5ec838da...\r\n...
```

## Tests

Added two tests to `test/js/bun/http/form-data-set-append.test.js`
that assert:
- `content-type` matches `^multipart/form-data;
boundary=----WebKitFormBoundary[0-9a-f]+$`
- body starts with `------WebKitFormBoundary...\r\n`
- body ends with `\r\n--<boundary>--\r\n`

Gate check:
- Stashed src changes → both new tests **fail** (declared boundary has 1
dash, lowercase k).
- Restored src changes → all 4 tests in the file **pass**.

Co-authored-by: robobun <robobun@users.noreply.github.com>
xhjkl pushed a commit to xhjkl/bun that referenced this pull request May 14, 2026
…mory' (oven-sh#29663)

`test/js/bun/net/socket.test.ts` "should not leak memory" has been
hitting `TCPSocket`/`TLSSocket` count `4` on the Windows 2019 agents,
one past the previous `isWindows ? 3` bound. Same flake shipped with
merged oven-sh#29631 today and has been blocking oven-sh#29593 through ~15 retriggers.

Non-Windows bound stays at `2` — that's where real retention regressions
are caught. The Windows bound is already loosened for its shared
prototype/structure retention; this just accommodates the one additional
retained instance the Win2019 lane is observing.

## Observed

- Build [#47600 Win2019
x64](https://buildkite.com/bun/bun/builds/47600#019dbc49-ff0c-49c2-afaa-134711815456)
— `Received: 4, Expected: <= 3`
- Build [#47616 Win2019
x64](https://buildkite.com/bun/bun/builds/47616#019dbc99-fc61-4111-8170-080d9729866a)
— same
- Multiple prior PRs:
[oven-sh#29631](oven-sh#29631),
[oven-sh#29651](oven-sh#29651),
[oven-sh#29649](oven-sh#29649),
[oven-sh#29650](oven-sh#29650),
[oven-sh#29645](oven-sh#29645),
[oven-sh#29639](oven-sh#29639) all hit the same
pattern

## Scope

3-line diff + comment. If the Win2019 retention count IS a regression
worth investigating, that's a separate issue; this change is strictly
about not holding other PRs hostage to the shared flake.

Co-authored-by: robobun <robobun@bun.sh>
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.

FormData generates non-standard multipart boundary format (-WebkitFormBoundary{hex}, 1 leading dash) differing from undici and WebKit

2 participants