From 0820553704911091d28eb32270a3591eece54b93 Mon Sep 17 00:00:00 2001 From: Vladislav Date: Thu, 12 Dec 2024 16:56:51 +0300 Subject: [PATCH 1/3] fix https://github.com/dart-lang/http/issues/175 --- pkgs/http/lib/src/multipart_file.dart | 2 +- pkgs/http/lib/src/response.dart | 20 +++++++--------- pkgs/http/lib/src/utils.dart | 34 +++++++++++++++++---------- pkgs/http/test/response_test.dart | 33 +++++++++++--------------- 4 files changed, 45 insertions(+), 44 deletions(-) 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'] }); From 8c267c4d16f2e3516166aa6211ef3a03b9b741c4 Mon Sep 17 00:00:00 2001 From: Vladislav Date: Thu, 12 Dec 2024 17:44:30 +0300 Subject: [PATCH 2/3] format code --- pkgs/http/lib/src/response.dart | 6 +++++- pkgs/http/lib/src/utils.dart | 19 ++++++++++++------- pkgs/http/test/response_test.dart | 31 +++++++++++++++++++++---------- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/pkgs/http/lib/src/response.dart b/pkgs/http/lib/src/response.dart index 585583eb22..94559328eb 100644 --- a/pkgs/http/lib/src/response.dart +++ b/pkgs/http/lib/src/response.dart @@ -43,7 +43,11 @@ 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); diff --git a/pkgs/http/lib/src/utils.dart b/pkgs/http/lib/src/utils.dart index e7277eb3ee..5dc9e867cb 100644 --- a/pkgs/http/lib/src/utils.dart +++ b/pkgs/http/lib/src/utils.dart @@ -14,21 +14,25 @@ import 'byte_stream.dart'; /// /// 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('&'); /// Determines the appropriate [Encoding] based on the given [contentTypeHeader]. /// /// - 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]) { +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) { + if (contentTypeHeader.type == 'application' && + contentTypeHeader.subtype == 'json' && + charset == null) { return utf8; } @@ -41,7 +45,8 @@ Encoding encodingForContentTypeHeader(MediaType contentTypeHeader, [Encoding fal /// 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 6f56f0af0d..e51b49acc1 100644 --- a/pkgs/http/test/response_test.dart +++ b/pkgs/http/test/response_test.dart @@ -16,16 +16,21 @@ 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'}); + var response = http.Response('{"foo":"Привет, мир!"}', 200, + headers: {'content-type': 'application/json'}); expect(response.body, equals('{"foo":"Привет, мир!"}')); }); }); @@ -51,7 +56,8 @@ 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,7 +68,8 @@ 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); @@ -77,29 +84,33 @@ 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'] }); From 7223e9dfece4eff0e9af230a3887e6dc40ddba97 Mon Sep 17 00:00:00 2001 From: Vladislav Date: Thu, 12 Dec 2024 18:11:58 +0300 Subject: [PATCH 3/3] rewrite test --- pkgs/http/test/response_test.dart | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pkgs/http/test/response_test.dart b/pkgs/http/test/response_test.dart index e51b49acc1..3ce0c4b9ba 100644 --- a/pkgs/http/test/response_test.dart +++ b/pkgs/http/test/response_test.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:test/test.dart'; @@ -27,12 +28,6 @@ void main() { 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()', () { @@ -51,6 +46,13 @@ void main() { headers: {'content-type': 'text/plain; charset=iso-8859-1'}); expect(response.body, equals('föøbãr')); }); + test('test decoding with empty charset if content type is application/json', + () { + final utf8Bytes = utf8.encode('{"foo":"Привет, мир!"}'); + var response = http.Response.bytes(utf8Bytes, 200, + headers: {'content-type': 'application/json'}); + expect(response.body, equals('{"foo":"Привет, мир!"}')); + }); }); group('.fromStream()', () {