Skip to content

Commit a750dea

Browse files
committed
Make MultipartRequest more closely adhere to browsers' behavior.
[email protected] BUG=17899 Review URL: https://codereview.chromium.org//218993016 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@34631 260f80e4-7a28-3924-810f-c04153c831b5
1 parent 0d59c3f commit a750dea

File tree

3 files changed

+78
-12
lines changed

3 files changed

+78
-12
lines changed

pkg/http/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@
77
positional.
88

99
* Make request headers case-insensitive.
10+
11+
* Make `MultipartRequest` more closely adhere to browsers' encoding conventions.

pkg/http/lib/src/multipart_request.dart

+18-10
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import 'byte_stream.dart';
1313
import 'multipart_file.dart';
1414
import 'utils.dart';
1515

16+
final _newlineRegExp = new RegExp(r"\r\n|\r|\n");
17+
1618
/// A `multipart/form-data` request. Such a request has both string [fields],
1719
/// which function as normal form fields, and (potentially streamed) binary
1820
/// [files].
@@ -61,13 +63,13 @@ class MultipartRequest extends BaseRequest {
6163

6264
fields.forEach((name, value) {
6365
length += "--".length + _BOUNDARY_LENGTH + "\r\n".length +
64-
_headerForField(name, value).length +
66+
UTF8.encode(_headerForField(name, value)).length +
6567
UTF8.encode(value).length + "\r\n".length;
6668
});
6769

6870
for (var file in _files) {
6971
length += "--".length + _BOUNDARY_LENGTH + "\r\n".length +
70-
_headerForFile(file).length +
72+
UTF8.encode(_headerForFile(file)).length +
7173
file.length + "\r\n".length;
7274
}
7375

@@ -91,8 +93,7 @@ class MultipartRequest extends BaseRequest {
9193
var controller = new StreamController<List<int>>(sync: true);
9294

9395
void writeAscii(String string) {
94-
assert(isPlainAscii(string));
95-
controller.add(string.codeUnits);
96+
controller.add(UTF8.encode(string));
9697
}
9798

9899
writeUtf8(String string) => controller.add(UTF8.encode(string));
@@ -133,11 +134,8 @@ class MultipartRequest extends BaseRequest {
133134
/// Returns the header string for a field. The return value is guaranteed to
134135
/// contain only ASCII characters.
135136
String _headerForField(String name, String value) {
136-
// http://tools.ietf.org/html/rfc2388 mandates some complex encodings for
137-
// field names and file names, but in practice user agents seem to just
138-
// URL-encode them so we do the same.
139137
var header =
140-
'content-disposition: form-data; name="${Uri.encodeFull(name)}"';
138+
'content-disposition: form-data; name="${_browserEncode(name)}"';
141139
if (!isPlainAscii(value)) {
142140
header = '$header\r\ncontent-type: text/plain; charset=utf-8';
143141
}
@@ -148,14 +146,24 @@ class MultipartRequest extends BaseRequest {
148146
/// contain only ASCII characters.
149147
String _headerForFile(MultipartFile file) {
150148
var header = 'content-type: ${file.contentType}\r\n'
151-
'content-disposition: form-data; name="${Uri.encodeFull(file.field)}"';
149+
'content-disposition: form-data; name="${_browserEncode(file.field)}"';
152150

153151
if (file.filename != null) {
154-
header = '$header; filename="${Uri.encodeFull(file.filename)}"';
152+
header = '$header; filename="${_browserEncode(file.filename)}"';
155153
}
156154
return '$header\r\n\r\n';
157155
}
158156

157+
/// Encode [value] in the same way browsers do.
158+
String _browserEncode(String value) {
159+
// http://tools.ietf.org/html/rfc2388 mandates some complex encodings for
160+
// field names and file names, but in practice user agents seem not to
161+
// follow this at all. Instead, they URL-encode `\r`, `\n`, and `\r\n` as
162+
// `\r\n`; URL-encode `"`; and do nothing else (even for `%` or non-ASCII
163+
// characters). We follow their behavior.
164+
return value.replaceAll(_newlineRegExp, "%0D%0A").replaceAll('"', "%22");
165+
}
166+
159167
/// Returns a randomly-generated multipart boundary string
160168
String _boundaryString() {
161169
var prefix = "dart-http-boundary-";

pkg/http/test/multipart_test.dart

+58-2
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,33 @@ void main() {
9393

9494
expect(request, bodyMatches('''
9595
--{{boundary}}
96-
content-disposition: form-data; name="f%C3%AF%C4%93ld"
96+
content-disposition: form-data; name="fïēld"
97+
98+
value
99+
--{{boundary}}--
100+
'''));
101+
});
102+
103+
test('with a field name with newlines', () {
104+
var request = new http.MultipartRequest('POST', dummyUrl);
105+
request.fields['foo\nbar\rbaz\r\nbang'] = 'value';
106+
107+
expect(request, bodyMatches('''
108+
--{{boundary}}
109+
content-disposition: form-data; name="foo%0D%0Abar%0D%0Abaz%0D%0Abang"
110+
111+
value
112+
--{{boundary}}--
113+
'''));
114+
});
115+
116+
test('with a field name with a quote', () {
117+
var request = new http.MultipartRequest('POST', dummyUrl);
118+
request.fields['foo"bar'] = 'value';
119+
120+
expect(request, bodyMatches('''
121+
--{{boundary}}
122+
content-disposition: form-data; name="foo%22bar"
97123
98124
value
99125
--{{boundary}}--
@@ -122,7 +148,37 @@ void main() {
122148
expect(request, bodyMatches('''
123149
--{{boundary}}
124150
content-type: text/plain; charset=utf-8
125-
content-disposition: form-data; name="file"; filename="f%C3%AFl%C4%93name.txt"
151+
content-disposition: form-data; name="file"; filename="fïlēname.txt"
152+
153+
contents
154+
--{{boundary}}--
155+
'''));
156+
});
157+
158+
test('with a filename with newlines', () {
159+
var request = new http.MultipartRequest('POST', dummyUrl);
160+
request.files.add(new http.MultipartFile.fromString('file', 'contents',
161+
filename: 'foo\nbar\rbaz\r\nbang'));
162+
163+
expect(request, bodyMatches('''
164+
--{{boundary}}
165+
content-type: text/plain; charset=utf-8
166+
content-disposition: form-data; name="file"; filename="foo%0D%0Abar%0D%0Abaz%0D%0Abang"
167+
168+
contents
169+
--{{boundary}}--
170+
'''));
171+
});
172+
173+
test('with a filename with a quote', () {
174+
var request = new http.MultipartRequest('POST', dummyUrl);
175+
request.files.add(new http.MultipartFile.fromString('file', 'contents',
176+
filename: 'foo"bar'));
177+
178+
expect(request, bodyMatches('''
179+
--{{boundary}}
180+
content-type: text/plain; charset=utf-8
181+
content-disposition: form-data; name="file"; filename="foo%22bar"
126182
127183
contents
128184
--{{boundary}}--

0 commit comments

Comments
 (0)