Skip to content

Commit d8e6929

Browse files
authored
Only set the charset when the body is set for certain mime types (#1798)
1 parent ef05b37 commit d8e6929

File tree

11 files changed

+126
-51
lines changed

11 files changed

+126
-51
lines changed

pkgs/cronet_http/example/pubspec.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,9 @@ dev_dependencies:
2929

3030
flutter:
3131
uses-material-design: true
32+
33+
# Use the path version of `package:http` so that the behavior matches the
34+
# expectations in http_client_conformance_tests.
35+
dependency_overrides:
36+
http:
37+
path: ../../http/

pkgs/cupertino_http/example/pubspec.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,9 @@ dev_dependencies:
3838

3939
flutter:
4040
uses-material-design: true
41+
42+
# Use the path version of `package:http` so that the behavior matches the
43+
# expectations in http_client_conformance_tests.
44+
dependency_overrides:
45+
http:
46+
path: ../../http/

pkgs/http/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 1.6.0-wip
2+
3+
* **Breaking** Change the behavior of `Request.body` so that a charset
4+
parameter is only added for text and XML media types. This brings the
5+
behavior of `package:http` in line with RFC-8259.
6+
17
## 1.5.0
28

39
* Fixed a bug in `IOClient` where the `HttpClient`'s response stream was

pkgs/http/lib/src/request.dart

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,22 @@ import 'utils.dart';
1414

1515
/// An HTTP request where the entire request body is known in advance.
1616
class Request extends BaseRequest {
17+
/// Whether the given MIME type should have a 'charset' parameter.
18+
static bool _shouldHaveCharset(MediaType? contentType) =>
19+
contentType != null &&
20+
// RFC 8259, section 9 says that "charset" is not defined for JSON.
21+
// Some uncommon non-text, non-xml types do specify charset
22+
// (e.g. application/news-checkgroups) but the user will have to set the
23+
// charset themselves for those types.
24+
(contentType.type == 'text' ||
25+
// XML media types defined by RFC 7303.
26+
// Note that some media types (e.g. cda+xml) specify that the
27+
// charset, when present, must be utf-8. We do not enforce this.
28+
contentType.mimeType == 'application/xml' ||
29+
contentType.mimeType == 'application/xml-external-parsed-entity' ||
30+
contentType.mimeType == 'application/xml-dtd' ||
31+
contentType.mimeType.endsWith('+xml'));
32+
1733
/// The size of the request body, in bytes. This is calculated from
1834
/// [bodyBytes].
1935
///
@@ -61,7 +77,9 @@ class Request extends BaseRequest {
6177
_checkFinalized();
6278
_defaultEncoding = value;
6379
var contentType = _contentType;
64-
if (contentType == null) return;
80+
if (contentType == null || !contentType.parameters.containsKey('charset')) {
81+
return;
82+
}
6583
_contentType = contentType.change(parameters: {'charset': value.name});
6684
}
6785

@@ -93,20 +111,27 @@ class Request extends BaseRequest {
93111
/// This is converted to and from [bodyBytes] using [encoding].
94112
///
95113
/// When this is set, if the request does not yet have a `Content-Type`
96-
/// header, one will be added with the type `text/plain`. Then the `charset`
97-
/// parameter of the `Content-Type` header (whether new or pre-existing) will
98-
/// be set to [encoding] if it wasn't already set.
114+
/// header, one will be added with the type `text/plain` and appropriate
115+
/// `charset` parameter.
116+
///
117+
/// If request has `Content-Type` header with MIME media type name `text` or
118+
/// is an XML MIME type (e.g. `application/xml` or `image/svg+xml`) without
119+
/// `charset` parameter, then the `charset` parameter will be set to
120+
/// [encoding].
99121
///
100-
/// To set the body of the request, without setting the `Content-Type` header,
101-
/// use [bodyBytes].
122+
/// To set the body of the request, without changing the `Content-Type`
123+
/// header, use [bodyBytes].
102124
String get body => encoding.decode(bodyBytes);
103125

104126
set body(String value) {
127+
// IANA defines known media types here:
128+
// https://www.iana.org/assignments/media-types/media-types.xhtml
105129
bodyBytes = encoding.encode(value);
106130
var contentType = _contentType;
107131
if (contentType == null) {
108132
_contentType = MediaType('text', 'plain', {'charset': encoding.name});
109-
} else if (!contentType.parameters.containsKey('charset')) {
133+
} else if (_shouldHaveCharset(_contentType) &&
134+
!contentType.parameters.containsKey('charset')) {
110135
_contentType = contentType.change(parameters: {'charset': encoding.name});
111136
}
112137
}

pkgs/http/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: http
2-
version: 1.5.0
2+
version: 1.6.0-wip
33
description: A composable, multi-platform, Future-based API for HTTP requests.
44
repository: https://github.com/dart-lang/http/tree/master/pkgs/http
55

pkgs/http/test/io/http_test.dart

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,7 @@ void main() {
158158
'method': 'POST',
159159
'path': '/',
160160
'headers': {
161-
'content-type': [
162-
'application/x-www-form-urlencoded; charset=utf-8'
163-
],
161+
'content-type': ['application/x-www-form-urlencoded'],
164162
'content-length': ['40'],
165163
'accept-encoding': ['gzip'],
166164
'user-agent': ['Dart'],
@@ -273,9 +271,7 @@ void main() {
273271
'method': 'PUT',
274272
'path': '/',
275273
'headers': {
276-
'content-type': [
277-
'application/x-www-form-urlencoded; charset=utf-8'
278-
],
274+
'content-type': ['application/x-www-form-urlencoded'],
279275
'content-length': ['40'],
280276
'accept-encoding': ['gzip'],
281277
'user-agent': ['Dart'],
@@ -388,9 +384,7 @@ void main() {
388384
'method': 'PATCH',
389385
'path': '/',
390386
'headers': {
391-
'content-type': [
392-
'application/x-www-form-urlencoded; charset=utf-8'
393-
],
387+
'content-type': ['application/x-www-form-urlencoded'],
394388
'content-length': ['40'],
395389
'accept-encoding': ['gzip'],
396390
'user-agent': ['Dart'],

pkgs/http/test/request_test.dart

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -189,23 +189,12 @@ void main() {
189189
expect(request.headers, containsPair('content-type', 'application/json'));
190190
});
191191

192-
test(
193-
'is set to application/x-www-form-urlencoded with charset utf-8 if '
194-
'bodyFields is set', () {
195-
var request = http.Request('POST', dummyUrl)
196-
..bodyFields = {'hello': 'world'};
197-
expect(request.headers['Content-Type'],
198-
equals('application/x-www-form-urlencoded; charset=utf-8'));
199-
});
200-
201-
test(
202-
'is set to application/x-www-form-urlencoded with the given charset '
203-
'if bodyFields and encoding are set', () {
192+
test('is set to application/x-www-form-urlencoded if bodyFields is set',
193+
() {
204194
var request = http.Request('POST', dummyUrl)
205-
..encoding = latin1
206195
..bodyFields = {'hello': 'world'};
207196
expect(request.headers['Content-Type'],
208-
equals('application/x-www-form-urlencoded; charset=iso-8859-1'));
197+
equals('application/x-www-form-urlencoded'));
209198
});
210199

211200
test(
@@ -218,20 +207,66 @@ void main() {
218207
equals('text/plain; charset=iso-8859-1'));
219208
});
220209

221-
test('is modified to include utf-8 if body is set', () {
210+
test('is modified to include utf-8 if body is set and mime type is text',
211+
() {
212+
var request = http.Request('POST', dummyUrl);
213+
request.headers['Content-Type'] = 'text/plain';
214+
request.body = 'Hello World!';
215+
expect(
216+
request.headers['Content-Type'], equals('text/plain; charset=utf-8'));
217+
});
218+
219+
test('is modified to include utf-8 if body is set and mime type is xml',
220+
() {
221+
var request = http.Request('POST', dummyUrl);
222+
request.headers['Content-Type'] = 'application/xml';
223+
request.body = '<?xml...';
224+
expect(request.headers['Content-Type'],
225+
equals('application/xml; charset=utf-8'));
226+
});
227+
228+
test(
229+
'is not modified to include utf-8 if body is set and mime type is json',
230+
() {
222231
var request = http.Request('POST', dummyUrl);
223232
request.headers['Content-Type'] = 'application/json';
224233
request.body = '{"hello": "world"}';
234+
expect(request.headers['Content-Type'], equals('application/json'));
235+
});
236+
237+
test(
238+
'is modified to include the given encoding if encoding is set and '
239+
'mime type is text', () {
240+
var request = http.Request('POST', dummyUrl);
241+
request.headers['Content-Type'] = 'text/plain';
242+
request
243+
..body = 'Hello World!'
244+
..encoding = latin1;
245+
expect(request.headers['Content-Type'],
246+
equals('text/plain; charset=iso-8859-1'));
247+
});
248+
249+
test(
250+
'is modified to include the given encoding if encoding is set and '
251+
'mime type is xml', () {
252+
var request = http.Request('POST', dummyUrl);
253+
request.headers['Content-Type'] = 'application/xml';
254+
request
255+
..body = '<?xml...'
256+
..encoding = latin1;
225257
expect(request.headers['Content-Type'],
226-
equals('application/json; charset=utf-8'));
258+
equals('application/xml; charset=iso-8859-1'));
227259
});
228260

229-
test('is modified to include the given encoding if encoding is set', () {
261+
test(
262+
'is not modified to include the given encoding if encoding is set mime '
263+
'type is json', () {
230264
var request = http.Request('POST', dummyUrl);
231265
request.headers['Content-Type'] = 'application/json';
232-
request.encoding = latin1;
233-
expect(request.headers['Content-Type'],
234-
equals('application/json; charset=iso-8859-1'));
266+
request
267+
..body = '{"hello": "world"}'
268+
..encoding = latin1;
269+
expect(request.headers['Content-Type'], equals('application/json'));
235270
});
236271

237272
test('has its charset overridden by an explicit encoding', () {

pkgs/http/test/stub_server.dart

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,20 @@ void hybridMain(StreamChannel<dynamic> channel) async {
6565
dynamic requestBody;
6666
if (requestBodyBytes.isEmpty) {
6767
requestBody = null;
68-
} else if (request.headers.contentType?.charset != null) {
69-
var encoding =
70-
requiredEncodingForCharset(request.headers.contentType!.charset!);
71-
requestBody = encoding.decode(requestBodyBytes);
7268
} else {
73-
requestBody = requestBodyBytes;
69+
requestBody = switch ((
70+
request.headers.contentType?.mimeType,
71+
request.headers.contentType?.charset
72+
)) {
73+
(_, var charset?) =>
74+
requiredEncodingForCharset(charset).decode(requestBodyBytes),
75+
// This is not a complete set of mime types that default to utf-8,
76+
// just the ones found in the tests.
77+
('application/json' || 'application/x-www-form-urlencoded', null) =>
78+
utf8.decode(requestBodyBytes),
79+
_ => requestBodyBytes,
80+
};
7481
}
75-
7682
final headers = <String, List<String>>{};
7783

7884
request.headers.forEach((name, values) {

pkgs/http_client_conformance_tests/lib/src/request_body_tests.dart

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,7 @@ void testRequestBody(Client client) {
7878
final serverReceivedContentType = await httpServerQueue.next;
7979
final serverReceivedBody = await httpServerQueue.next;
8080

81-
expect(serverReceivedContentType,
82-
['application/x-www-form-urlencoded; charset=utf-8']);
81+
expect(serverReceivedContentType, ['application/x-www-form-urlencoded']);
8382
expect(serverReceivedBody, 'key=value');
8483
});
8584

@@ -90,8 +89,7 @@ void testRequestBody(Client client) {
9089
final serverReceivedContentType = await httpServerQueue.next;
9190
final serverReceivedBody = await httpServerQueue.next;
9291

93-
expect(serverReceivedContentType,
94-
['application/x-www-form-urlencoded; charset=plus2']);
92+
expect(serverReceivedContentType, ['application/x-www-form-urlencoded']);
9593
expect(serverReceivedBody, 'gau;r]hqa'); // key=value
9694
});
9795

@@ -149,7 +147,7 @@ void testRequestBody(Client client) {
149147
final serverReceivedContentType = await httpServerQueue.next;
150148
final serverReceivedBody = await httpServerQueue.next as String;
151149

152-
expect(serverReceivedContentType, ['image/png; charset=plus2']);
150+
expect(serverReceivedContentType, ['image/png']);
153151
expect(serverReceivedBody.codeUnits, [1, 2, 3, 4, 5]);
154152
});
155153

pkgs/http_client_conformance_tests/pubspec.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ dependencies:
1515
stream_channel: ^2.1.1
1616
test: ^1.21.2
1717

18-
# TODO(brianquinlan): Remove dependency_overrides when package:http 1.5.0 is released.
1918
dependency_overrides:
2019
http:
2120
path: ../http

0 commit comments

Comments
 (0)