diff --git a/pkgs/http/lib/src/multipart_file.dart b/pkgs/http/lib/src/multipart_file.dart index c773098953..47ed3c96ac 100644 --- a/pkgs/http/lib/src/multipart_file.dart +++ b/pkgs/http/lib/src/multipart_file.dart @@ -73,7 +73,7 @@ class MultipartFile { factory MultipartFile.fromString(String field, String value, {String? filename, MediaType? contentType}) { contentType ??= MediaType('text', 'plain'); - var encoding = encodingForCharset(contentType.parameters['charset'], utf8); + var encoding = encodingForContentTypeHeader(contentType, utf8); contentType = contentType.change(parameters: {'charset': encoding.name}); return MultipartFile.fromBytes(field, encoding.encode(value), diff --git a/pkgs/http/lib/src/response.dart b/pkgs/http/lib/src/response.dart index 9d8cdb88f5..585583eb22 100644 --- a/pkgs/http/lib/src/response.dart +++ b/pkgs/http/lib/src/response.dart @@ -21,10 +21,10 @@ class Response extends BaseResponse { /// /// This is converted from [bodyBytes] using the `charset` parameter of the /// `Content-Type` header field, if available. If it's unavailable or if the - /// encoding name is unknown, [latin1] is used by default, as per - /// [RFC 2616][]. + /// encoding name is unknown, [utf8] is used by default, as per + /// [RFC3629][]. /// - /// [RFC 2616]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html + /// [RFC3629]:https://www.rfc-editor.org/rfc/rfc3629. String get body => _encodingForHeaders(headers).decode(bodyBytes); /// Creates a new HTTP response with a string body. @@ -43,11 +43,7 @@ class Response extends BaseResponse { /// Create a new HTTP response with a byte array body. Response.bytes(List bodyBytes, super.statusCode, - {super.request, - super.headers, - super.isRedirect, - super.persistentConnection, - super.reasonPhrase}) + {super.request, super.headers, super.isRedirect, super.persistentConnection, super.reasonPhrase}) : bodyBytes = toUint8List(bodyBytes), super(contentLength: bodyBytes.length); @@ -66,10 +62,12 @@ class Response extends BaseResponse { /// Returns the encoding to use for a response with the given headers. /// -/// Defaults to [latin1] if the headers don't specify a charset or if that -/// charset is unknown. +/// If the `Content-Type` header specifies a charset, it will use that charset. +/// If no charset is provided or the charset is unknown: +/// - Defaults to [utf8] if the `Content-Type` is `application/json` (since JSON is defined to use UTF-8 by default). +/// - Otherwise, defaults to [latin1] for compatibility. Encoding _encodingForHeaders(Map headers) => - encodingForCharset(_contentTypeForHeaders(headers).parameters['charset']); + encodingForContentTypeHeader(_contentTypeForHeaders(headers)); /// Returns the [MediaType] object for the given headers' content-type. /// diff --git a/pkgs/http/lib/src/utils.dart b/pkgs/http/lib/src/utils.dart index 72ec1529f2..e7277eb3ee 100644 --- a/pkgs/http/lib/src/utils.dart +++ b/pkgs/http/lib/src/utils.dart @@ -6,25 +6,34 @@ import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; +import 'package:http_parser/http_parser.dart'; + import 'byte_stream.dart'; /// Converts a [Map] from parameter names to values to a URL query string. /// /// mapToQuery({"foo": "bar", "baz": "bang"}); /// //=> "foo=bar&baz=bang" -String mapToQuery(Map map, {required Encoding encoding}) => - map.entries - .map((e) => '${Uri.encodeQueryComponent(e.key, encoding: encoding)}' - '=${Uri.encodeQueryComponent(e.value, encoding: encoding)}') - .join('&'); +String mapToQuery(Map map, {required Encoding encoding}) => map.entries + .map((e) => '${Uri.encodeQueryComponent(e.key, encoding: encoding)}' + '=${Uri.encodeQueryComponent(e.value, encoding: encoding)}') + .join('&'); -/// Returns the [Encoding] that corresponds to [charset]. +/// Determines the appropriate [Encoding] based on the given [contentTypeHeader]. /// -/// Returns [fallback] if [charset] is null or if no [Encoding] was found that -/// corresponds to [charset]. -Encoding encodingForCharset(String? charset, [Encoding fallback = latin1]) { - if (charset == null) return fallback; - return Encoding.getByName(charset) ?? fallback; +/// - If the `Content-Type` is `application/json` and no charset is specified, it defaults to [utf8]. +/// - If a charset is specified in the parameters, it attempts to find a matching [Encoding]. +/// - If no charset is specified or the charset is unknown, it falls back to the provided [fallback], which defaults to [latin1]. +Encoding encodingForContentTypeHeader(MediaType contentTypeHeader, [Encoding fallback = latin1]) { + final charset = contentTypeHeader.parameters['charset']; + + // Default to utf8 for application/json when charset is unspecified. + if (contentTypeHeader.type == 'application' && contentTypeHeader.subtype == 'json' && charset == null) { + return utf8; + } + + // Attempt to find the encoding or fall back to the default. + return charset != null ? Encoding.getByName(charset) ?? fallback : fallback; } /// Returns the [Encoding] that corresponds to [charset]. @@ -32,8 +41,7 @@ Encoding encodingForCharset(String? charset, [Encoding fallback = latin1]) { /// Throws a [FormatException] if no [Encoding] was found that corresponds to /// [charset]. Encoding requiredEncodingForCharset(String charset) => - Encoding.getByName(charset) ?? - (throw FormatException('Unsupported encoding "$charset".')); + Encoding.getByName(charset) ?? (throw FormatException('Unsupported encoding "$charset".')); /// A regular expression that matches strings that are composed entirely of /// ASCII-compatible characters. diff --git a/pkgs/http/test/response_test.dart b/pkgs/http/test/response_test.dart index 1bd9fd8e38..6f56f0af0d 100644 --- a/pkgs/http/test/response_test.dart +++ b/pkgs/http/test/response_test.dart @@ -16,17 +16,18 @@ void main() { test('sets bodyBytes', () { var response = http.Response('Hello, world!', 200); - expect( - response.bodyBytes, - equals( - [72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33])); + expect(response.bodyBytes, equals([72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33])); }); test('respects the inferred encoding', () { - var response = http.Response('föøbãr', 200, - headers: {'content-type': 'text/plain; charset=iso-8859-1'}); + var response = http.Response('föøbãr', 200, headers: {'content-type': 'text/plain; charset=iso-8859-1'}); expect(response.bodyBytes, equals([102, 246, 248, 98, 227, 114])); }); + + test('test empty charset', () { + var response = http.Response('{"foo":"Привет, мир!"}', 200, headers: {'content-type': 'application/json'}); + expect(response.body, equals('{"foo":"Привет, мир!"}')); + }); }); group('.bytes()', () { @@ -50,8 +51,7 @@ void main() { group('.fromStream()', () { test('sets body', () async { var controller = StreamController>(sync: true); - var streamResponse = - http.StreamedResponse(controller.stream, 200, contentLength: 13); + var streamResponse = http.StreamedResponse(controller.stream, 200, contentLength: 13); controller ..add([72, 101, 108, 108, 111, 44, 32]) ..add([119, 111, 114, 108, 100, 33]); @@ -62,8 +62,7 @@ void main() { test('sets bodyBytes', () async { var controller = StreamController>(sync: true); - var streamResponse = - http.StreamedResponse(controller.stream, 200, contentLength: 5); + var streamResponse = http.StreamedResponse(controller.stream, 200, contentLength: 5); controller.add([104, 101, 108, 108, 111]); unawaited(controller.close()); var response = await http.Response.fromStream(streamResponse); @@ -78,33 +77,29 @@ void main() { }); test('one header', () async { - var response = - http.Response('Hello, world!', 200, headers: {'fruit': 'apple'}); + var response = http.Response('Hello, world!', 200, headers: {'fruit': 'apple'}); expect(response.headersSplitValues, const { 'fruit': ['apple'] }); }); test('two headers', () async { - var response = http.Response('Hello, world!', 200, - headers: {'fruit': 'apple,banana'}); + var response = http.Response('Hello, world!', 200, headers: {'fruit': 'apple,banana'}); expect(response.headersSplitValues, const { 'fruit': ['apple', 'banana'] }); }); test('two headers with lots of spaces', () async { - var response = http.Response('Hello, world!', 200, - headers: {'fruit': 'apple \t , \tbanana'}); + var response = http.Response('Hello, world!', 200, headers: {'fruit': 'apple \t , \tbanana'}); expect(response.headersSplitValues, const { 'fruit': ['apple', 'banana'] }); }); test('one set-cookie', () async { - var response = http.Response('Hello, world!', 200, headers: { - 'set-cookie': 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT' - }); + var response = http.Response('Hello, world!', 200, + headers: {'set-cookie': 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT'}); expect(response.headersSplitValues, const { 'set-cookie': ['id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT'] });