Skip to content

Commit f9c923b

Browse files
authored
🚑 Fix FormData encoding (#1741) (#1747)
1 parent 9c30e9a commit f9c923b

File tree

3 files changed

+97
-73
lines changed

3 files changed

+97
-73
lines changed

dio/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased
44

55
- Imply `List<Map>` as JSON content in `ImplyContentTypeInterceptor`.
6+
- Fix `FormData` encoding for collections and objects.
67

78
## 5.0.2
89

dio/lib/src/utils.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@ String encodeMap(
3838
}) {
3939
final urlData = StringBuffer('');
4040
bool first = true;
41-
final leftBracket = isQuery ? '[' : '%5B';
42-
final rightBracket = isQuery ? ']' : '%5D';
41+
// URL Query parameters are generally encoded but not their
42+
// index or nested names in square brackets.
43+
// When [encode] is false, for example for [FormData], nothing is encoded.
44+
final leftBracket = isQuery || !encode ? '[' : '%5B';
45+
final rightBracket = isQuery || !encode ? ']' : '%5D';
4346
final encodeComponent = encode ? Uri.encodeQueryComponent : (e) => e;
4447
Object? maybeEncode(Object? value) {
4548
if (!isQuery || value == null || value is! String) {

dio/test/formdata_test.dart

Lines changed: 91 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -6,85 +6,105 @@ import 'package:dio/dio.dart';
66
import 'package:test/test.dart';
77

88
void main() async {
9-
test('FormData', () async {
10-
final fm = FormData.fromMap({
11-
'name': 'wendux',
12-
'age': 25,
13-
'path': '/图片空间/地址',
14-
'file': MultipartFile.fromString(
15-
'hello world.',
16-
headers: {
17-
'test': <String>['a']
18-
},
19-
),
20-
'files': [
21-
await MultipartFile.fromFile(
22-
'test/mock/_testfile',
23-
filename: '1.txt',
24-
headers: {
25-
'test': <String>['b']
26-
},
27-
),
28-
MultipartFile.fromFileSync(
29-
'test/mock/_testfile',
30-
filename: '2.txt',
9+
group(FormData, () {
10+
test('complex', () async {
11+
final fm = FormData.fromMap({
12+
'name': 'wendux',
13+
'age': 25,
14+
'path': '/图片空间/地址',
15+
'file': MultipartFile.fromString(
16+
'hello world.',
3117
headers: {
32-
'test': <String>['c']
18+
'test': <String>['a']
3319
},
3420
),
35-
]
36-
});
37-
final fmStr = await fm.readAsBytes();
38-
final f = File('test/mock/_formdata');
39-
String content = f.readAsStringSync();
40-
content = content.replaceAll('--dio-boundary-3788753558', fm.boundary);
41-
String actual = utf8.decode(fmStr, allowMalformed: true);
21+
'files': [
22+
await MultipartFile.fromFile(
23+
'test/mock/_testfile',
24+
filename: '1.txt',
25+
headers: {
26+
'test': <String>['b']
27+
},
28+
),
29+
MultipartFile.fromFileSync(
30+
'test/mock/_testfile',
31+
filename: '2.txt',
32+
headers: {
33+
'test': <String>['c']
34+
},
35+
),
36+
]
37+
});
38+
final fmStr = await fm.readAsBytes();
39+
final f = File('test/mock/_formdata');
40+
String content = f.readAsStringSync();
41+
content = content.replaceAll('--dio-boundary-3788753558', fm.boundary);
42+
String actual = utf8.decode(fmStr, allowMalformed: true);
4243

43-
actual = actual.replaceAll('\r\n', '\n');
44-
content = content.replaceAll('\r\n', '\n');
44+
actual = actual.replaceAll('\r\n', '\n');
45+
content = content.replaceAll('\r\n', '\n');
4546

46-
expect(actual, content);
47-
expect(fm.readAsBytes(), throwsA(const TypeMatcher<StateError>()));
47+
expect(actual, content);
48+
expect(fm.readAsBytes(), throwsA(const TypeMatcher<StateError>()));
4849

49-
final fm1 = FormData();
50-
fm1.fields.add(MapEntry('name', 'wendux'));
51-
fm1.fields.add(MapEntry('age', '25'));
52-
fm1.fields.add(MapEntry('path', '/图片空间/地址'));
53-
fm1.files.add(
54-
MapEntry(
55-
'file',
56-
MultipartFile.fromString(
57-
'hello world.',
58-
headers: {
59-
'test': <String>['a']
60-
},
50+
final fm1 = FormData();
51+
fm1.fields.add(MapEntry('name', 'wendux'));
52+
fm1.fields.add(MapEntry('age', '25'));
53+
fm1.fields.add(MapEntry('path', '/图片空间/地址'));
54+
fm1.files.add(
55+
MapEntry(
56+
'file',
57+
MultipartFile.fromString(
58+
'hello world.',
59+
headers: {
60+
'test': <String>['a']
61+
},
62+
),
6163
),
62-
),
63-
);
64-
fm1.files.add(
65-
MapEntry(
66-
'files',
67-
await MultipartFile.fromFile(
68-
'test/mock/_testfile',
69-
filename: '1.txt',
70-
headers: {
71-
'test': <String>['b'],
72-
},
64+
);
65+
fm1.files.add(
66+
MapEntry(
67+
'files',
68+
await MultipartFile.fromFile(
69+
'test/mock/_testfile',
70+
filename: '1.txt',
71+
headers: {
72+
'test': <String>['b'],
73+
},
74+
),
7375
),
74-
),
75-
);
76-
fm1.files.add(
77-
MapEntry(
78-
'files',
79-
await MultipartFile.fromFile(
80-
'test/mock/_testfile',
81-
filename: '2.txt',
82-
headers: {
83-
'test': <String>['c'],
84-
},
76+
);
77+
fm1.files.add(
78+
MapEntry(
79+
'files',
80+
await MultipartFile.fromFile(
81+
'test/mock/_testfile',
82+
filename: '2.txt',
83+
headers: {
84+
'test': <String>['c'],
85+
},
86+
),
8587
),
86-
),
87-
);
88-
expect(fmStr.length, fm1.length);
88+
);
89+
expect(fmStr.length, fm1.length);
90+
});
91+
92+
test('encodes maps correctly', () async {
93+
final fd = FormData.fromMap({
94+
'items': [
95+
{'name': 'foo', 'value': 1},
96+
{'name': 'bar', 'value': 2},
97+
],
98+
});
99+
100+
final data = await fd.readAsBytes();
101+
final result = utf8.decode(data, allowMalformed: true);
102+
103+
expect(result, contains('name="items[0][name]"'));
104+
expect(result, contains('name="items[0][value]"'));
105+
106+
expect(result, contains('name="items[1][name]"'));
107+
expect(result, contains('name="items[1][value]"'));
108+
});
89109
});
90110
}

0 commit comments

Comments
 (0)