Skip to content

Commit

Permalink
[dio] Fix FormData encoding regression for maps with dynamic keys (#1757
Browse files Browse the repository at this point in the history
)

In relation to #1741 

### New Pull Request Checklist

- [x] I have read the
[Documentation](https://pub.dev/documentation/dio/latest/)
- [x] I have searched for a similar pull request in the
[project](https://github.com/cfug/dio/pulls) and found none
- [x] I have updated this branch with the latest `main` branch to avoid
conflicts (via merge from master or rebase)
- [x] I have added the required tests to prove the fix/feature I'm
adding
- [x] I have updated the documentation (if necessary)
- [x] I have run the tests without failures
- [x] I have updated the `CHANGELOG.md` in the corresponding package

### Additional context and info (if any)

Fixes #1741
Fixes #1758

---------

Signed-off-by: Alex Li <github@alexv525.com>
Co-authored-by: Alex Li <github@alexv525.com>
  • Loading branch information
kuhnroyal and AlexV525 committed Mar 27, 2023
1 parent b26e7f8 commit d4442a2
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 93 deletions.
1 change: 1 addition & 0 deletions dio/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- Fix double-completion when using `connectionTimeout` on web platform.
- Allow defining adapter methods through their constructors.
- Fix `FormData` encoding regression for maps with dynamic keys, introduced in 5.0.3.

## 5.0.3

Expand Down
2 changes: 1 addition & 1 deletion dio/lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ String encodeMap(
} else {
urlEncode(sub.map(maybeEncode).join(separatorChar), path);
}
} else if (sub is Map<String, dynamic>) {
} else if (sub is Map) {
sub.forEach((k, v) {
if (path == '') {
urlEncode(maybeEncode(v), '${encodeComponent(k)}');
Expand Down
37 changes: 26 additions & 11 deletions dio/test/basic_test.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
@TestOn('vm')
import 'dart:async';
import 'dart:io';

import 'package:dio/dio.dart';
import 'package:test/test.dart';

import 'mock/adapters.dart';
import 'utils.dart';

void main() {
test('test headers', () {
Expand Down Expand Up @@ -38,16 +38,18 @@ void main() {
expect(headers1.map.isEmpty, isTrue);
});

test(
'send with an invalid URL',
() async {
await expectLater(
Dio().get('http://http.invalid'),
throwsA((e) => e is DioError && e.error is SocketException),
);
},
testOn: 'vm',
);
test('send with an invalid URL', () async {
await expectLater(
Dio().get('http://http.invalid'),
throwsA(allOf([
isA<DioError>(),
(DioError e) =>
e.type ==
(isWeb ? DioErrorType.connectionError : DioErrorType.unknown),
if (!isWeb) (DioError e) => e.error is SocketException,
])),
);
});

test('cancellation', () async {
final dio = Dio()
Expand Down Expand Up @@ -84,4 +86,17 @@ void main() {
);
expect(r.statusCode, 401);
});

test('post map', () async {
final dio = Dio()
..options.baseUrl = EchoAdapter.mockBase
..httpClientAdapter = EchoAdapter();

final response = await dio.post(
'/post',
data: {'a': 1, 'b': 2, 'c': 3},
);
expect(response.data, '{"a":1,"b":2,"c":3}');
expect(response.statusCode, 200);
});
}
2 changes: 1 addition & 1 deletion dio/test/browser_test.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@TestOn('chrome')
@TestOn('browser')
import 'dart:typed_data';

import 'package:dio/browser.dart';
Expand Down
247 changes: 167 additions & 80 deletions dio/test/formdata_test.dart
Original file line number Diff line number Diff line change
@@ -1,110 +1,197 @@
@TestOn('vm')
import 'dart:convert';
import 'dart:io';

import 'package:dio/dio.dart';
import 'package:test/test.dart';

import 'mock/adapters.dart';

void main() async {
group(FormData, () {
test('complex', () async {
final fm = FormData.fromMap({
'name': 'wendux',
'age': 25,
'path': '/图片空间/地址',
'file': MultipartFile.fromString(
'hello world.',
headers: {
'test': <String>['a']
},
),
'files': [
await MultipartFile.fromFile(
'test/mock/_testfile',
filename: '1.txt',
headers: {
'test': <String>['b']
},
),
MultipartFile.fromFileSync(
'test/mock/_testfile',
filename: '2.txt',
headers: {
'test': <String>['c']
},
),
]
});
final fmStr = await fm.readAsBytes();
final f = File('test/mock/_formdata');
String content = f.readAsStringSync();
content = content.replaceAll('--dio-boundary-3788753558', fm.boundary);
String actual = utf8.decode(fmStr, allowMalformed: true);

actual = actual.replaceAll('\r\n', '\n');
content = content.replaceAll('\r\n', '\n');

expect(actual, content);
expect(fm.readAsBytes(), throwsA(const TypeMatcher<StateError>()));

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',
MultipartFile.fromString(
test(
'complex',
() async {
final fm = FormData.fromMap({
'name': 'wendux',
'age': 25,
'path': '/图片空间/地址',
'file': MultipartFile.fromString(
'hello world.',
headers: {
'test': <String>['a']
},
),
),
);
fm1.files.add(
MapEntry(
'files',
await MultipartFile.fromFile(
'test/mock/_testfile',
filename: '1.txt',
headers: {
'test': <String>['b'],
},
'files': [
await MultipartFile.fromFile(
'test/mock/_testfile',
filename: '1.txt',
headers: {
'test': <String>['b']
},
),
MultipartFile.fromFileSync(
'test/mock/_testfile',
filename: '2.txt',
headers: {
'test': <String>['c']
},
),
]
});
final fmStr = await fm.readAsBytes();
final f = File('test/mock/_formdata');
String content = f.readAsStringSync();
content = content.replaceAll('--dio-boundary-3788753558', fm.boundary);
String actual = utf8.decode(fmStr, allowMalformed: true);

actual = actual.replaceAll('\r\n', '\n');
content = content.replaceAll('\r\n', '\n');

expect(actual, content);
expect(fm.readAsBytes(), throwsA(const TypeMatcher<StateError>()));

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',
MultipartFile.fromString(
'hello world.',
headers: {
'test': <String>['a']
},
),
),
),
);
fm1.files.add(
MapEntry(
'files',
await MultipartFile.fromFile(
'test/mock/_testfile',
filename: '2.txt',
headers: {
'test': <String>['c'],
},
);
fm1.files.add(
MapEntry(
'files',
await MultipartFile.fromFile(
'test/mock/_testfile',
filename: '1.txt',
headers: {
'test': <String>['b'],
},
),
),
);
fm1.files.add(
MapEntry(
'files',
await MultipartFile.fromFile(
'test/mock/_testfile',
filename: '2.txt',
headers: {
'test': <String>['c'],
},
),
),
),
);
expect(fmStr.length, fm1.length);
},
testOn: 'vm',
);

test('encodes maps correctly', () async {
final fd = FormData.fromMap(
{
'items': [
{'name': 'foo', 'value': 1},
{'name': 'bar', 'value': 2},
],
'api': {
'dest': '/',
'data': {
'a': 1,
'b': 2,
'c': 3,
},
},
},
ListFormat.multiCompatible,
);
expect(fmStr.length, fm1.length);

final data = await fd.readAsBytes();
final result = utf8.decode(data, allowMalformed: true);

expect(result, contains('name="items[0][name]"'));
expect(result, contains('name="items[0][value]"'));

expect(result, contains('name="items[1][name]"'));
expect(result, contains('name="items[1][value]"'));
expect(result, contains('name="items[1][value]"'));

expect(result, contains('name="api[dest]"'));
expect(result, contains('name="api[data][a]"'));
expect(result, contains('name="api[data][b]"'));
expect(result, contains('name="api[data][c]"'));
});

test('encodes maps correctly', () async {
final fd = FormData.fromMap({
'items': [
{'name': 'foo', 'value': 1},
{'name': 'bar', 'value': 2},
],
});
test('encodes dynamic Map correctly', () async {
final dynamicData = <dynamic, dynamic>{
'a': 1,
'b': 2,
'c': 3,
};

final request = {
'api': {
'dest': '/',
'data': dynamicData,
}
};

final fd = FormData.fromMap(request);
final data = await fd.readAsBytes();
final result = utf8.decode(data, allowMalformed: true);
expect(result, contains('name="api[dest]"'));
expect(result, contains('name="api[data][a]"'));
expect(result, contains('name="api[data][b]"'));
expect(result, contains('name="api[data][c]"'));
});

test('posts maps correctly', () async {
final fd = FormData.fromMap(
{
'items': [
{'name': 'foo', 'value': 1},
{'name': 'bar', 'value': 2},
],
'api': {
'dest': '/',
'data': {
'a': 1,
'b': 2,
'c': 3,
},
},
},
ListFormat.multiCompatible,
);

final dio = Dio()
..options.baseUrl = EchoAdapter.mockBase
..httpClientAdapter = EchoAdapter();

final response = await dio.post(
'/post',
data: fd,
);

final result = response.data;
expect(result, contains('name="items[0][name]"'));
expect(result, contains('name="items[0][value]"'));

expect(result, contains('name="items[1][name]"'));
expect(result, contains('name="items[1][value]"'));
expect(result, contains('name="items[1][value]"'));

expect(result, contains('name="api[dest]"'));
expect(result, contains('name="api[data][a]"'));
expect(result, contains('name="api[data][b]"'));
expect(result, contains('name="api[data][c]"'));
});
});
}
4 changes: 4 additions & 0 deletions dio/test/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Encoding requiredEncodingForCharset(String charset) =>
/// The URL for the current server instance.
Uri get serverUrl => Uri.parse('http://localhost:${_server?.port}');

const isWeb = bool.hasEnvironment('dart.library.js_util')
? bool.fromEnvironment('dart.library.js_util')
: identical(0, 0.0);

/// Starts a new HTTP server.
Future<void> startServer() async {
_server = (await HttpServer.bind('localhost', 0))
Expand Down

0 comments on commit d4442a2

Please sign in to comment.