Skip to content

🐛 Fix FormData.clone() dropping boundaryName and camelCaseContentDisposition#2531

Open
ultramcu wants to merge 1 commit into
cfug:mainfrom
ultramcu:fix/formdata-clone-preserves-options
Open

🐛 Fix FormData.clone() dropping boundaryName and camelCaseContentDisposition#2531
ultramcu wants to merge 1 commit into
cfug:mainfrom
ultramcu:fix/formdata-clone-preserves-options

Conversation

@ultramcu
Copy link
Copy Markdown

Follow-on to merged #2305 and merged #2008. FormData.clone() still drops two of the original constructor options, so a retried multipart request silently re-serialises with the defaults.

1. Bug

clone() builds the new instance via the no-arg FormData() constructor:

FormData clone() {
  final clone = FormData();           // ← resets options to defaults
  clone._boundary = _boundary;        // (added by #2305)
  ...
}

That resets two options that the original may have set:

Option Added in Symptom on a cloned (retried) request
boundaryName original API clone.boundaryName returns the dio default, not the user's label
camelCaseContentDisposition #2008 wire bytes contain content-disposition: form-data; … instead of Content-Disposition: …

The header-casing regression is the user-visible one: any server that matches Content-Disposition case-sensitively (some signing gateways, some upload validators) accepts the first attempt and rejects the retry.

2. Fix

Forward both options through clone():

FormData clone() {
  final clone = FormData(
    boundaryName: boundaryName,
    camelCaseContentDisposition: camelCaseContentDisposition,
  );
  clone._boundary = _boundary;
  clone.fields.addAll(fields);
  for (final file in files) {
    clone.files.add(MapEntry(file.key, file.value.clone()));
  }
  return clone;
}

One-line behavioural change, no new public API.

3. Test

New regression test in dio/test/formdata_test.dart (clone() preserves boundaryName and camelCaseContentDisposition):

  • builds a FormData with boundaryName: '--custom-boundary' and camelCaseContentDisposition: true,
  • clones it,
  • asserts both getters round-trip,
  • decodes readAsBytes() on the clone and asserts the body contains Content-Disposition: form-data; name="name" and does not contain the lower-case content-disposition: form.

Fail-before / pass-after verified locally:

  • before the fix: the new test fails on the wire-body assertions (clone emits content-disposition: lower-case).
  • after the fix: full dio/test/formdata_test.dart suite green (8/8).

4. CI

  • dart format --output=none --set-exit-if-changed . on dio/ — clean (0 of 63 files changed).
  • dart analyze on dio/lib/src/form_data.dart and dio/test/formdata_test.dart — no issues.
  • Full dart test on the dio/ package — 220 passed / 6 network-gated skipped / 0 failed.

New Pull Request Checklist

  • I have read the Documentation
  • I have searched for a similar pull request in the project and found none
  • I have updated this branch with the latest main branch to avoid conflicts (via merge from master or rebase)
  • I have added the required tests to prove the fix/feature I'm adding
  • I have updated the documentation (if necessary)
  • I have run the tests without failures
  • I have updated the CHANGELOG.md in the corresponding package

…osition

cfug#2305 made clone() preserve the boundary value but still constructed the
clone via the no-arg `FormData()` ctor, which silently reset two
constructor options:

  - `boundaryName` — exposed via the `boundaryName` getter; useful to
    callers that key behaviour off a custom boundary label.
  - `camelCaseContentDisposition` (added in cfug#2008) — opts the multipart
    body into the `Content-Disposition` header casing required by some
    servers.

The second is the user-visible regression: a multipart request retried
via a cloned FormData emits `content-disposition` (lower-case) even
when the original was configured with the camel-case opt-in, breaking
servers that match the header case-sensitively.

Forward both options when constructing the clone, and add a regression
test covering the boundary name, the getter values, and the on-wire
header casing.
@ultramcu ultramcu requested a review from a team as a code owner May 29, 2026 01:13
@github-actions
Copy link
Copy Markdown
Contributor

Code Coverage Report: Only Changed Files listed

Package Base Coverage New Coverage Difference
Overall Coverage 🟢 88.88% 🟢 88.89% 🟢 0.01%

Minimum allowed coverage is 0%, this run produced 88.89%

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant