Skip to content

Commit

Permalink
Fix encoding of query parameters (#1704)
Browse files Browse the repository at this point in the history
Changes and test for correct encoding of collections in query
parameters.

I am totally not sure if the encoding for `x-www-form-urlencoded` is
supposed to be different from query parameters.


### 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
- [ ] 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)

<!-- Provide more context and info about the PR. -->

---------

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

## Unreleased

- Fix wrong encoding of collection query parameters.
- Fix "unsupported operation" error on web platform.

## 5.0.1
Expand Down
10 changes: 5 additions & 5 deletions dio/lib/src/options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ mixin OptionsMixin {
///
/// List values use the default [ListFormat.multiCompatible].
///
/// The value can be overridden per parameter by adding a [MultiParam]
/// The value can be overridden per parameter by adding a [ListParam]
/// object wrapping the actual List value and the desired format.
late Map<String, dynamic> queryParameters;

Expand Down Expand Up @@ -572,7 +572,7 @@ class RequestOptions extends _RequestConfig with OptionsMixin {
url = '${s[0]}:/${s[1].replaceAll('//', '/')}';
}
}
final query = Transformer.urlEncodeMap(queryParameters, listFormat);
final query = Transformer.urlEncodeQueryMap(queryParameters, listFormat);
if (query.isNotEmpty) {
url += (url.contains('?') ? '&' : '?') + query;
}
Expand All @@ -585,7 +585,7 @@ class RequestOptions extends _RequestConfig with OptionsMixin {
/// When using `x-www-url-encoded` body data,
/// List values use the default [ListFormat.multi].
///
/// The value can be overridden per value by adding a [MultiParam]
/// The value can be overridden per value by adding a [ListParam]
/// object wrapping the actual List value and the desired format.
dynamic data;

Expand Down Expand Up @@ -695,7 +695,7 @@ class _RequestConfig {

set receiveTimeout(Duration? value) {
if (value != null && value.isNegative) {
throw StateError("reveiveTimeout should be positive");
throw StateError("receiveTimeout should be positive");
}
_receiveTimeout = value;
}
Expand Down Expand Up @@ -776,7 +776,7 @@ class _RequestConfig {
/// Possible values defined in [ListFormat] are `csv`, `ssv`, `tsv`, `pipes`, `multi`, `multiCompatible`.
/// The default value is `multi`.
///
/// The value can be overridden per parameter by adding a [MultiParam]
/// The value can be overridden per parameter by adding a [ListParam]
/// object to the query or body data map.
late ListFormat listFormat;
}
18 changes: 17 additions & 1 deletion dio/lib/src/transformer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ abstract class Transformer {
/// Deep encode the [Map<String, dynamic>] to percent-encoding.
/// It is mostly used with the "application/x-www-form-urlencoded" content-type.
static String urlEncodeMap(
Map map, [
Map<String, dynamic> map, [
ListFormat listFormat = ListFormat.multi,
]) {
return encodeMap(
Expand All @@ -42,6 +42,22 @@ abstract class Transformer {
);
}

/// Deep encode the [Map<String, dynamic>] to a query parameter string.
static String urlEncodeQueryMap(
Map<String, dynamic> map, [
ListFormat listFormat = ListFormat.multi,
]) {
return encodeMap(
map,
(key, value) {
if (value == null) return key;
return '$key=$value';
},
listFormat: listFormat,
isQuery: true,
);
}

/// Following: https://mimesniff.spec.whatwg.org/#json-mime-type
static bool isJsonMimeType(String? contentType) {
if (contentType == null) return false;
Expand Down
2 changes: 1 addition & 1 deletion dio/lib/src/transformers/sync_transformer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class SyncTransformer extends Transformer {
final dynamic data = options.data ?? '';
if (data is! String && Transformer.isJsonMimeType(options.contentType)) {
return jsonEncodeCallback(data);
} else if (data is Map) {
} else if (data is Map<String, dynamic>) {
return Transformer.urlEncodeMap(data);
} else {
return data.toString();
Expand Down
34 changes: 21 additions & 13 deletions dio/lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,25 @@ String encodeMap(
data,
DioEncodeHandler handler, {
bool encode = true,
bool isQuery = false,
ListFormat listFormat = ListFormat.multi,
}) {
final urlData = StringBuffer('');
bool first = true;
final leftBracket = encode ? '%5B' : '[';
final rightBracket = encode ? '%5D' : ']';
final leftBracket = isQuery ? '[' : '%5B';
final rightBracket = isQuery ? ']' : '%5D';
final encodeComponent = encode ? Uri.encodeQueryComponent : (e) => e;
void urlEncode(dynamic sub, String path) {
Object? maybeEncode(Object? value) {
if (!isQuery || value == null || value is! String) {
return value;
}
return encodeComponent(value);
}

void urlEncode(Object? sub, String path) {
// Detect if the list format for this parameter derivatives from default.
final format = sub is ListParam ? sub.format : listFormat;
final separatorChar = _getSeparatorChar(format);
final separatorChar = _getSeparatorChar(format, isQuery);

if (sub is ListParam) {
// Need to unwrap all param objects here
Expand All @@ -54,28 +62,28 @@ String encodeMap(
sub[i] is Map || sub[i] is List || sub[i] is ListParam;
if (listFormat == ListFormat.multi) {
urlEncode(
sub[i],
maybeEncode(sub[i]),
'$path${isCollection ? '$leftBracket$i$rightBracket' : ''}',
);
} else {
// Forward compatibility
urlEncode(
sub[i],
maybeEncode(sub[i]),
'$path$leftBracket${isCollection ? i : ''}$rightBracket',
);
}
}
} else {
urlEncode(sub.join(separatorChar), path);
urlEncode(sub.map(maybeEncode).join(separatorChar), path);
}
} else if (sub is Map) {
} else if (sub is Map<String, dynamic>) {
sub.forEach((k, v) {
if (path == '') {
urlEncode(v, '${encodeComponent(k as String)}');
urlEncode(maybeEncode(v), '${encodeComponent(k)}');
} else {
urlEncode(
v,
'$path$leftBracket${encodeComponent(k as String)}$rightBracket',
maybeEncode(v),
'$path$leftBracket${encodeComponent(k)}$rightBracket',
);
}
});
Expand All @@ -96,12 +104,12 @@ String encodeMap(
return urlData.toString();
}

String _getSeparatorChar(ListFormat collectionFormat) {
String _getSeparatorChar(ListFormat collectionFormat, bool isQuery) {
switch (collectionFormat) {
case ListFormat.csv:
return ',';
case ListFormat.ssv:
return ' ';
return isQuery ? '%20' : ' ';
case ListFormat.tsv:
return r'\t';
case ListFormat.pipes:
Expand Down
193 changes: 116 additions & 77 deletions dio/test/encoding_test.dart
Original file line number Diff line number Diff line change
@@ -1,90 +1,129 @@
import 'package:dio/dio.dart';
import 'package:dio/src/utils.dart';
import 'package:test/test.dart';

void main() {
final data = {
'a': '你好',
'b': [5, '6'],
'c': {
'd': 8,
'e': {
'a': 5,
'b': [66, 8]
group(Transformer.urlEncodeMap, () {
final data = {
'a': '你好',
'b': [5, '6'],
'c': {
'd': 8,
'e': {
'a': 5,
'b': [66, 8]
}
}
}
};
test('default ', () {
// a=你好&b=5&b=6&c[d]=8&c[e][a]=5&c[e][b]=66&c[e][b]=8
final result =
'a=%E4%BD%A0%E5%A5%BD&b=5&b=6&c%5Bd%5D=8&c%5Be%5D%5Ba%5D=5&c%5Be%5D%5Bb%5D=66&c%5Be%5D%5Bb%5D=8';
expect(Transformer.urlEncodeMap(data), result);
});
test('csv', () {
// a=你好&b=5,6&c[d]=8&c[e][a]=5&c[e][b]=66,8
final result =
'a=%E4%BD%A0%E5%A5%BD&b=5%2C6&c%5Bd%5D=8&c%5Be%5D%5Ba%5D=5&c%5Be%5D%5Bb%5D=66%2C8';
expect(Transformer.urlEncodeMap(data, ListFormat.csv), result);
});
test('ssv', () {
// a=你好&b=5+6&c[d]=8&c[e][a]=5&c[e][b]=66+8
final result =
'a=%E4%BD%A0%E5%A5%BD&b=5+6&c%5Bd%5D=8&c%5Be%5D%5Ba%5D=5&c%5Be%5D%5Bb%5D=66+8';
expect(Transformer.urlEncodeMap(data, ListFormat.ssv), result);
});
test('tsv', () {
// a=你好&b=5\t6&c[d]=8&c[e][a]=5&c[e][b]=66\t8
final result =
'a=%E4%BD%A0%E5%A5%BD&b=5%5Ct6&c%5Bd%5D=8&c%5Be%5D%5Ba%5D=5&c%5Be%5D%5Bb%5D=66%5Ct8';
expect(Transformer.urlEncodeMap(data, ListFormat.tsv), result);
});
test('pipe', () {
//a=你好&b=5|6&c[d]=8&c[e][a]=5&c[e][b]=66|8
final result =
'a=%E4%BD%A0%E5%A5%BD&b=5%7C6&c%5Bd%5D=8&c%5Be%5D%5Ba%5D=5&c%5Be%5D%5Bb%5D=66%7C8';
expect(Transformer.urlEncodeMap(data, ListFormat.pipes), result);
});
};
test('default ', () {
// a=你好&b=5&b=6&c[d]=8&c[e][a]=5&c[e][b]=66&c[e][b]=8
final result =
'a=%E4%BD%A0%E5%A5%BD&b=5&b=6&c%5Bd%5D=8&c%5Be%5D%5Ba%5D=5&c%5Be%5D%5Bb%5D=66&c%5Be%5D%5Bb%5D=8';
expect(Transformer.urlEncodeMap(data), result);
});
test('csv', () {
// a=你好&b=5,6&c[d]=8&c[e][a]=5&c[e][b]=66,8
final result =
'a=%E4%BD%A0%E5%A5%BD&b=5%2C6&c%5Bd%5D=8&c%5Be%5D%5Ba%5D=5&c%5Be%5D%5Bb%5D=66%2C8';
expect(Transformer.urlEncodeMap(data, ListFormat.csv), result);
});
test('ssv', () {
// a=你好&b=5+6&c[d]=8&c[e][a]=5&c[e][b]=66+8
final result =
'a=%E4%BD%A0%E5%A5%BD&b=5+6&c%5Bd%5D=8&c%5Be%5D%5Ba%5D=5&c%5Be%5D%5Bb%5D=66+8';
expect(Transformer.urlEncodeMap(data, ListFormat.ssv), result);
});
test('tsv', () {
// a=你好&b=5\t6&c[d]=8&c[e][a]=5&c[e][b]=66\t8
final result =
'a=%E4%BD%A0%E5%A5%BD&b=5%5Ct6&c%5Bd%5D=8&c%5Be%5D%5Ba%5D=5&c%5Be%5D%5Bb%5D=66%5Ct8';
expect(Transformer.urlEncodeMap(data, ListFormat.tsv), result);
});
test('pipe', () {
//a=你好&b=5|6&c[d]=8&c[e][a]=5&c[e][b]=66|8
final result =
'a=%E4%BD%A0%E5%A5%BD&b=5%7C6&c%5Bd%5D=8&c%5Be%5D%5Ba%5D=5&c%5Be%5D%5Bb%5D=66%7C8';
expect(Transformer.urlEncodeMap(data, ListFormat.pipes), result);
});

test('multi', () {
//a=你好&b[]=5&b[]=6&c[d]=8&c[e][a]=5&c[e][b][]=66&c[e][b][]=8
final result =
'a=%E4%BD%A0%E5%A5%BD&b%5B%5D=5&b%5B%5D=6&c%5Bd%5D=8&c%5Be%5D%5Ba%5D=5&c%5Be%5D%5Bb%5D%5B%5D=66&c%5Be%5D%5Bb%5D%5B%5D=8';
expect(Transformer.urlEncodeMap(data, ListFormat.multiCompatible), result);
});
test('multi', () {
//a=你好&b[]=5&b[]=6&c[d]=8&c[e][a]=5&c[e][b][]=66&c[e][b][]=8
final result =
'a=%E4%BD%A0%E5%A5%BD&b%5B%5D=5&b%5B%5D=6&c%5Bd%5D=8&c%5Be%5D%5Ba%5D=5&c%5Be%5D%5Bb%5D%5B%5D=66&c%5Be%5D%5Bb%5D%5B%5D=8';
expect(
Transformer.urlEncodeMap(data, ListFormat.multiCompatible), result);
});

test('multi2', () {
final data = {
'a': 'string',
'b': 'another_string',
'z': ['string'],
};
// a=string&b=another_string&z[]=string
final result = 'a=string&b=another_string&z%5B%5D=string';
expect(Transformer.urlEncodeMap(data, ListFormat.multiCompatible), result);
test('multi2', () {
final data = {
'a': 'string',
'b': 'another_string',
'z': ['string'],
};
// a=string&b=another_string&z[]=string
final result = 'a=string&b=another_string&z%5B%5D=string';
expect(
Transformer.urlEncodeMap(data, ListFormat.multiCompatible), result);
});

test('custom', () {
final result =
'a=%E4%BD%A0%E5%A5%BD&b=5%7C6&c%5Bd%5D=8&c%5Be%5D%5Ba%5D=5&c%5Be%5D%5Bb%5D=foo%2Cbar&c%5Be%5D%5Bc%5D=foo+bar&c%5Be%5D%5Bd%5D%5B%5D=foo&c%5Be%5D%5Bd%5D%5B%5D=bar&c%5Be%5D%5Be%5D=foo%5Ctbar';
expect(
Transformer.urlEncodeMap(
{
'a': '你好',
'b': ListParam<int>([5, 6], ListFormat.pipes),
'c': {
'd': 8,
'e': {
'a': 5,
'b': ListParam<String>(['foo', 'bar'], ListFormat.csv),
'c': ListParam<String>(['foo', 'bar'], ListFormat.ssv),
'd': ListParam<String>(['foo', 'bar'], ListFormat.multi),
'e': ListParam<String>(['foo', 'bar'], ListFormat.tsv),
},
},
},
ListFormat.multiCompatible,
),
result,
);
});
});

test('custom', () {
//a=你好&b=5|6&c[d]=8&c[e][a]=5&c[e][b]=foo,bar&c[e][c]=foo+bar&c[e][d][]=foo&c[e][d][]=bar&c[e][e]=foo\tbar
final result =
'a=%E4%BD%A0%E5%A5%BD&b=5%7C6&c%5Bd%5D=8&c%5Be%5D%5Ba%5D=5&c%5Be%5D%5Bb%5D=foo%2Cbar&c%5Be%5D%5Bc%5D=foo+bar&c%5Be%5D%5Bd%5D%5B%5D=foo&c%5Be%5D%5Bd%5D%5B%5D=bar&c%5Be%5D%5Be%5D=foo%5Ctbar';
expect(
Transformer.urlEncodeMap(
{
'a': '你好',
'b': ListParam<int>([5, 6], ListFormat.pipes),
'c': {
'd': 8,
'e': {
'a': 5,
'b': ListParam<String>(['foo', 'bar'], ListFormat.csv),
'c': ListParam<String>(['foo', 'bar'], ListFormat.ssv),
'd': ListParam<String>(['foo', 'bar'], ListFormat.multi),
'e': ListParam<String>(['foo', 'bar'], ListFormat.tsv),
group(Transformer.urlEncodeQueryMap, () {
test(ListFormat.csv, () {
expect(
Transformer.urlEncodeQueryMap({
'foo': ListParam(['1', '%', '\$'], ListFormat.csv)
}),
'foo=1,%25,%24',
);
});

test('custom', () {
expect(
Transformer.urlEncodeQueryMap(
{
'a': '你好',
'b': ListParam<int>([5, 6], ListFormat.pipes),
'c': {
'd': 8,
'e': {
'a': 5,
'b': ListParam<Object>(['foo', 'bar', 1, 2.2], ListFormat.csv),
'c': ListParam<Object>(['foo', 'bar', 1, 2.2], ListFormat.ssv),
'd':
ListParam<Object>(['foo', 'bar', 1, 2.2], ListFormat.multi),
'e': ListParam<Object>(['foo', 'bar', 1, 2.2], ListFormat.tsv),
},
},
},
},
ListFormat.multiCompatible,
),
result,
);
ListFormat.multiCompatible,
),
'a=%E4%BD%A0%E5%A5%BD&b=5|6&c[d]=8&c[e][a]=5&c[e][b]=foo,bar,1,2.2&c[e][c]=foo%20bar%201%202.2&c[e][d][]=foo&c[e][d][]=bar&c[e][d][]=1&c[e][d][]=2.2&c[e][e]=foo\\tbar\\t1\\t2.2',
);
});
});
}

0 comments on commit 017588b

Please sign in to comment.