From f9c923b556f33bbdb3ec71fcfccbb21bb526ccf5 Mon Sep 17 00:00:00 2001 From: Peter Leibiger Date: Fri, 17 Mar 2023 19:49:37 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=91=20Fix=20FormData=20encoding=20(#17?= =?UTF-8?q?41)=20(#1747)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dio/CHANGELOG.md | 1 + dio/lib/src/utils.dart | 7 +- dio/test/formdata_test.dart | 162 ++++++++++++++++++++---------------- 3 files changed, 97 insertions(+), 73 deletions(-) diff --git a/dio/CHANGELOG.md b/dio/CHANGELOG.md index e604d404f..2d0728df0 100644 --- a/dio/CHANGELOG.md +++ b/dio/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - Imply `List` as JSON content in `ImplyContentTypeInterceptor`. +- Fix `FormData` encoding for collections and objects. ## 5.0.2 diff --git a/dio/lib/src/utils.dart b/dio/lib/src/utils.dart index 8420708b5..8bba0b123 100644 --- a/dio/lib/src/utils.dart +++ b/dio/lib/src/utils.dart @@ -38,8 +38,11 @@ String encodeMap( }) { final urlData = StringBuffer(''); bool first = true; - final leftBracket = isQuery ? '[' : '%5B'; - final rightBracket = isQuery ? ']' : '%5D'; + // URL Query parameters are generally encoded but not their + // index or nested names in square brackets. + // When [encode] is false, for example for [FormData], nothing is encoded. + final leftBracket = isQuery || !encode ? '[' : '%5B'; + final rightBracket = isQuery || !encode ? ']' : '%5D'; final encodeComponent = encode ? Uri.encodeQueryComponent : (e) => e; Object? maybeEncode(Object? value) { if (!isQuery || value == null || value is! String) { diff --git a/dio/test/formdata_test.dart b/dio/test/formdata_test.dart index 3d9b056a6..a4b125d88 100644 --- a/dio/test/formdata_test.dart +++ b/dio/test/formdata_test.dart @@ -6,85 +6,105 @@ import 'package:dio/dio.dart'; import 'package:test/test.dart'; void main() async { - test('FormData', () async { - final fm = FormData.fromMap({ - 'name': 'wendux', - 'age': 25, - 'path': '/图片空间/地址', - 'file': MultipartFile.fromString( - 'hello world.', - headers: { - 'test': ['a'] - }, - ), - 'files': [ - await MultipartFile.fromFile( - 'test/mock/_testfile', - filename: '1.txt', - headers: { - 'test': ['b'] - }, - ), - MultipartFile.fromFileSync( - 'test/mock/_testfile', - filename: '2.txt', + group(FormData, () { + test('complex', () async { + final fm = FormData.fromMap({ + 'name': 'wendux', + 'age': 25, + 'path': '/图片空间/地址', + 'file': MultipartFile.fromString( + 'hello world.', headers: { - 'test': ['c'] + 'test': ['a'] }, ), - ] - }); - 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); + 'files': [ + await MultipartFile.fromFile( + 'test/mock/_testfile', + filename: '1.txt', + headers: { + 'test': ['b'] + }, + ), + MultipartFile.fromFileSync( + 'test/mock/_testfile', + filename: '2.txt', + headers: { + 'test': ['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'); + actual = actual.replaceAll('\r\n', '\n'); + content = content.replaceAll('\r\n', '\n'); - expect(actual, content); - expect(fm.readAsBytes(), throwsA(const TypeMatcher())); + expect(actual, content); + expect(fm.readAsBytes(), throwsA(const TypeMatcher())); - 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': ['a'] - }, + 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': ['a'] + }, + ), ), - ), - ); - fm1.files.add( - MapEntry( - 'files', - await MultipartFile.fromFile( - 'test/mock/_testfile', - filename: '1.txt', - headers: { - 'test': ['b'], - }, + ); + fm1.files.add( + MapEntry( + 'files', + await MultipartFile.fromFile( + 'test/mock/_testfile', + filename: '1.txt', + headers: { + 'test': ['b'], + }, + ), ), - ), - ); - fm1.files.add( - MapEntry( - 'files', - await MultipartFile.fromFile( - 'test/mock/_testfile', - filename: '2.txt', - headers: { - 'test': ['c'], - }, + ); + fm1.files.add( + MapEntry( + 'files', + await MultipartFile.fromFile( + 'test/mock/_testfile', + filename: '2.txt', + headers: { + 'test': ['c'], + }, + ), ), - ), - ); - expect(fmStr.length, fm1.length); + ); + expect(fmStr.length, fm1.length); + }); + + test('encodes maps correctly', () async { + final fd = FormData.fromMap({ + 'items': [ + {'name': 'foo', 'value': 1}, + {'name': 'bar', 'value': 2}, + ], + }); + + 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]"')); + }); }); }