Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Make MultipartFile recoverable to enable retrying FormData requests #1889

Merged
merged 9 commits into from
Jul 14, 2023
3 changes: 2 additions & 1 deletion dio/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ See the [Migration Guide][] for the complete breaking changes list.**
## Unreleased

- Remove `http` from `dev_dependencies`.
- Add support for cloning `MultipartFile` from `FormData`.
- Only produce null response body when `ResponseType.json`.

## 5.2.1+1
Expand All @@ -29,7 +30,7 @@ See the [Migration Guide][] for the complete breaking changes list.**
Dio 6.0.0 - Please use the replacement `IOHttpClientAdapter.createHttpClient` instead.
- Using `CancelToken` no longer closes and re-creates `HttpClient` for each request when `IOHttpClientAdapter` is used.
- Fix timeout handling for browser `receiveTimeout`.
- Improve performance when sending binary data (`List<int>`/`Uint8List`).
- Improve performance when sending binary data (`List<int>`/`Uint8List`).

## 5.1.2

Expand Down
11 changes: 11 additions & 0 deletions dio/lib/src/multipart_file.dart
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,15 @@ class MultipartFile {
_isFinalized = true;
return _stream;
}

/// Clone MultipartFile, returning a new instance of the same object.
/// This is useful if your request failed and you wish to retry it,
/// such as an unauthorized exception can be solved by refreshing the token.
MultipartFile clone() => MultipartFile(
_stream,
length,
filename: filename,
contentType: contentType,
headers: headers,
);
}
93 changes: 93 additions & 0 deletions dio/test/formdata_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,99 @@ void main() async {
testOn: 'vm',
);

// Cloned multipart files should be able to be read again and be the same
// as the original ones.
test(
'complex with cloning',
() async {
final multipartFile1 = MultipartFile.fromString(
'hello world.',
headers: {
'test': <String>['a']
},
);
final multipartFile2 = await MultipartFile.fromFile(
'test/mock/_testfile',
filename: '1.txt',
headers: {
'test': <String>['b']
},
);
final multipartFile3 = MultipartFile.fromFileSync(
'test/mock/_testfile',
filename: '2.txt',
headers: {
'test': <String>['c']
},
);

final fm = FormData.fromMap({
'name': 'wendux',
'age': 25,
'path': '/图片空间/地址',
'file': multipartFile1,
'files': [
multipartFile2,
multipartFile3,
]
});
final fmStr = await fm.readAsBytes();

// Files are finalized after being read.
try {
multipartFile1.finalize();
fail('Should not be able to finalize a file twice.');
} catch (e) {
expect(e, isA<StateError>());
expect(
(e as StateError).message,
'The MultipartFile has already been finalized. This typically '
'means you are using the same MultipartFile in repeated requests.',
);
}

final fm1 = FormData();
fm1.fields.add(MapEntry('name', 'wendux'));
fm1.fields.add(MapEntry('age', '25'));
fm1.fields.add(MapEntry('path', '/图片空间/地址'));
fm1.files.add(
MapEntry(
'file',
multipartFile1.clone(),
),
);
fm1.files.add(
MapEntry(
'files',
multipartFile2.clone(),
),
);
fm1.files.add(
MapEntry(
'files',
multipartFile3.clone(),
),
);
expect(fmStr.length, fm1.length);

// The cloned multipart files should be able to be read again.
expect(fm.files[0].value.isFinalized, true);
expect(fm.files[1].value.isFinalized, true);
expect(fm.files[2].value.isFinalized, true);
expect(fm1.files[0].value.isFinalized, false);
expect(fm1.files[1].value.isFinalized, false);
expect(fm1.files[2].value.isFinalized, false);

// The cloned multipart files' properties should be the same as the
// original ones.
expect(fm1.files[0].value.filename, multipartFile1.filename);
expect(fm1.files[0].value.contentType, multipartFile1.contentType);
expect(fm1.files[0].value.length, multipartFile1.length);
expect(fm1.files[0].value.headers, multipartFile1.headers);
},
testOn: 'vm',
);

test('encodes maps correctly', () async {
final fd = FormData.fromMap(
{
Expand Down