Skip to content

Only set the charset when the body is set for certain mime types #1798

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from
Draft
6 changes: 6 additions & 0 deletions pkgs/cronet_http/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,9 @@ dev_dependencies:

flutter:
uses-material-design: true

# Use the path version of `package:http` so that the behavior matches the
# expectations in http_client_conformance_tests.
dependency_overrides:
http:
path: ../../http/
6 changes: 6 additions & 0 deletions pkgs/cupertino_http/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,9 @@ dev_dependencies:

flutter:
uses-material-design: true

# Use the path version of `package:http` so that the behavior matches the
# expectations in http_client_conformance_tests.
dependency_overrides:
http:
path: ../../http/
5 changes: 5 additions & 0 deletions pkgs/http/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 2.0.0-wip

* **Breaking** Change the behavior of `Request.body` so that a charset
parameter is only added for text and XML media types.

## 1.5.0

* Fixed a bug in `IOClient` where the `HttpClient`'s response stream was
Expand Down
39 changes: 32 additions & 7 deletions pkgs/http/lib/src/request.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ import 'utils.dart';

/// An HTTP request where the entire request body is known in advance.
class Request extends BaseRequest {
/// Whether the given MIME type should have a 'charset' parameter.
bool _shouldHaveCharset(MediaType? contentType) =>
contentType != null &&
// RFC 8259, section 9 says that "charset" is not defined for JSON.
// Some uncommon non-text, non-xml types do specify charset
// (e.g. application/news-checkgroups) but the user will have to set the
// charset themselves for those types.
(contentType.type == 'text' ||
// XML media types defined by RFC 7303.
// Note that some media types (e.g. cda+xml) specify that the
// charset, when present, must be utf-8.
contentType.mimeType == 'application/xml' ||
contentType.mimeType == 'application/xml-external-parsed-entity' ||
contentType.mimeType == 'application/xml-dtd' ||
contentType.mimeType.endsWith('+xml'));

/// The size of the request body, in bytes. This is calculated from
/// [bodyBytes].
///
Expand Down Expand Up @@ -61,7 +77,9 @@ class Request extends BaseRequest {
_checkFinalized();
_defaultEncoding = value;
var contentType = _contentType;
if (contentType == null) return;
if (contentType == null || !contentType.parameters.containsKey('charset')) {
return;
}
_contentType = contentType.change(parameters: {'charset': value.name});
}

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

set body(String value) {
// IANA defines known media types here:
// https://www.iana.org/assignments/media-types/media-types.xhtml
bodyBytes = encoding.encode(value);
var contentType = _contentType;
if (contentType == null) {
_contentType = MediaType('text', 'plain', {'charset': encoding.name});
} else if (!contentType.parameters.containsKey('charset')) {
} else if (_shouldHaveCharset(_contentType) &&
!contentType.parameters.containsKey('charset')) {
_contentType = contentType.change(parameters: {'charset': encoding.name});
}
}
Expand Down
2 changes: 1 addition & 1 deletion pkgs/http/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: http
version: 1.5.0
version: 2.0.0-wip
description: A composable, multi-platform, Future-based API for HTTP requests.
repository: https://github.com/dart-lang/http/tree/master/pkgs/http

Expand Down
12 changes: 3 additions & 9 deletions pkgs/http/test/io/http_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,7 @@ void main() {
'method': 'POST',
'path': '/',
'headers': {
'content-type': [
'application/x-www-form-urlencoded; charset=utf-8'
],
'content-type': ['application/x-www-form-urlencoded'],
'content-length': ['40'],
'accept-encoding': ['gzip'],
'user-agent': ['Dart'],
Expand Down Expand Up @@ -273,9 +271,7 @@ void main() {
'method': 'PUT',
'path': '/',
'headers': {
'content-type': [
'application/x-www-form-urlencoded; charset=utf-8'
],
'content-type': ['application/x-www-form-urlencoded'],
'content-length': ['40'],
'accept-encoding': ['gzip'],
'user-agent': ['Dart'],
Expand Down Expand Up @@ -388,9 +384,7 @@ void main() {
'method': 'PATCH',
'path': '/',
'headers': {
'content-type': [
'application/x-www-form-urlencoded; charset=utf-8'
],
'content-type': ['application/x-www-form-urlencoded'],
'content-length': ['40'],
'accept-encoding': ['gzip'],
'user-agent': ['Dart'],
Expand Down
75 changes: 55 additions & 20 deletions pkgs/http/test/request_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -189,23 +189,12 @@ void main() {
expect(request.headers, containsPair('content-type', 'application/json'));
});

test(
'is set to application/x-www-form-urlencoded with charset utf-8 if '
'bodyFields is set', () {
var request = http.Request('POST', dummyUrl)
..bodyFields = {'hello': 'world'};
expect(request.headers['Content-Type'],
equals('application/x-www-form-urlencoded; charset=utf-8'));
});

test(
'is set to application/x-www-form-urlencoded with the given charset '
'if bodyFields and encoding are set', () {
test('is set to application/x-www-form-urlencoded if bodyFields is set',
() {
var request = http.Request('POST', dummyUrl)
..encoding = latin1
..bodyFields = {'hello': 'world'};
expect(request.headers['Content-Type'],
equals('application/x-www-form-urlencoded; charset=iso-8859-1'));
equals('application/x-www-form-urlencoded'));
});

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

test('is modified to include utf-8 if body is set', () {
test('is modified to include utf-8 if body is set and mime type is text',
() {
var request = http.Request('POST', dummyUrl);
request.headers['Content-Type'] = 'text/plain';
request.body = 'Hello World!';
expect(
request.headers['Content-Type'], equals('text/plain; charset=utf-8'));
});

test('is modified to include utf-8 if body is set and mime type is xml',
() {
var request = http.Request('POST', dummyUrl);
request.headers['Content-Type'] = 'application/xml';
request.body = '<?xml...';
expect(request.headers['Content-Type'],
equals('application/xml; charset=utf-8'));
});

test(
'is not modified to include utf-8 if body is set and mime type is json',
() {
var request = http.Request('POST', dummyUrl);
request.headers['Content-Type'] = 'application/json';
request.body = '{"hello": "world"}';
expect(request.headers['Content-Type'], equals('application/json'));
});

test(
'is modified to include the given encoding if encoding is set and '
'mime type is text', () {
var request = http.Request('POST', dummyUrl);
request.headers['Content-Type'] = 'text/plain';
request
..body = 'Hello World!'
..encoding = latin1;
expect(request.headers['Content-Type'],
equals('text/plain; charset=iso-8859-1'));
});

test(
'is modified to include the given encoding if encoding is set and '
'mime type is xml', () {
var request = http.Request('POST', dummyUrl);
request.headers['Content-Type'] = 'application/xml';
request
..body = '<?xml...'
..encoding = latin1;
expect(request.headers['Content-Type'],
equals('application/json; charset=utf-8'));
equals('application/xml; charset=iso-8859-1'));
});

test('is modified to include the given encoding if encoding is set', () {
test(
'is not modified to include the given encoding if encoding is set mime '
'type is json', () {
var request = http.Request('POST', dummyUrl);
request.headers['Content-Type'] = 'application/json';
request.encoding = latin1;
expect(request.headers['Content-Type'],
equals('application/json; charset=iso-8859-1'));
request
..body = '{"hello": "world"}'
..encoding = latin1;
expect(request.headers['Content-Type'], equals('application/json'));
});

test('has its charset overridden by an explicit encoding', () {
Expand Down
18 changes: 12 additions & 6 deletions pkgs/http/test/stub_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,20 @@ void hybridMain(StreamChannel<dynamic> channel) async {
dynamic requestBody;
if (requestBodyBytes.isEmpty) {
requestBody = null;
} else if (request.headers.contentType?.charset != null) {
var encoding =
requiredEncodingForCharset(request.headers.contentType!.charset!);
requestBody = encoding.decode(requestBodyBytes);
} else {
requestBody = requestBodyBytes;
requestBody = switch ((
request.headers.contentType?.mimeType,
request.headers.contentType?.charset
)) {
(_, var charset?) =>
requiredEncodingForCharset(charset).decode(requestBodyBytes),
// This is not a complete set of mime types that default to utf-8,
// just the ones found in the tests.
('application/json' || 'application/x-www-form-urlencoded', null) =>
utf8.decode(requestBodyBytes),
_ => requestBodyBytes,
};
}

final headers = <String, List<String>>{};

request.headers.forEach((name, values) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,7 @@ void testRequestBody(Client client) {
final serverReceivedContentType = await httpServerQueue.next;
final serverReceivedBody = await httpServerQueue.next;

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

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

expect(serverReceivedContentType,
['application/x-www-form-urlencoded; charset=plus2']);
expect(serverReceivedContentType, ['application/x-www-form-urlencoded']);
expect(serverReceivedBody, 'gau;r]hqa'); // key=value
});

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

expect(serverReceivedContentType, ['image/png; charset=plus2']);
expect(serverReceivedContentType, ['image/png']);
expect(serverReceivedBody.codeUnits, [1, 2, 3, 4, 5]);
});

Expand Down
3 changes: 1 addition & 2 deletions pkgs/http_client_conformance_tests/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@ environment:
dependencies:
async: ^2.8.2
dart_style: ">=2.3.7 <4.0.0"
http: ^1.2.0
http: ">=1.2.0 <3.0.0" # XXX
stream_channel: ^2.1.1
test: ^1.21.2

# TODO(brianquinlan): Remove dependency_overrides when package:http 1.5.0 is released.
dependency_overrides:
http:
path: ../http
Expand Down
4 changes: 2 additions & 2 deletions pkgs/ok_http/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ flutter:
assets:
- test_certs/ # Used in integration tests.

# TODO(brianquinlan): Remove this when a release version of `package:http`
# supports abortable requests.
# Use the path version of `package:http` so that the behavior matches the
# expectations in http_client_conformance_tests.
dependency_overrides:
http:
path: ../../http/
Loading