Skip to content

Commit

Permalink
fix #175
Browse files Browse the repository at this point in the history
  • Loading branch information
Vladislav committed Dec 12, 2024
1 parent e166181 commit 5c26219
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 44 deletions.
2 changes: 1 addition & 1 deletion pkgs/http/lib/src/multipart_file.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
20 changes: 9 additions & 11 deletions pkgs/http/lib/src/response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -43,11 +43,7 @@ class Response extends BaseResponse {

/// Create a new HTTP response with a byte array body.
Response.bytes(List<int> 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);

Expand All @@ -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<String, String> headers) =>
encodingForCharset(_contentTypeForHeaders(headers).parameters['charset']);
encodingForContentTypeHeader(_contentTypeForHeaders(headers));

/// Returns the [MediaType] object for the given headers' content-type.
///
Expand Down
34 changes: 21 additions & 13 deletions pkgs/http/lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,42 @@ 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<String, String> map, {required Encoding encoding}) =>
map.entries
.map((e) => '${Uri.encodeQueryComponent(e.key, encoding: encoding)}'
'=${Uri.encodeQueryComponent(e.value, encoding: encoding)}')
.join('&');
String mapToQuery(Map<String, String> 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].
///
/// 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.
Expand Down
33 changes: 14 additions & 19 deletions pkgs/http/test/response_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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()', () {
Expand All @@ -50,8 +51,7 @@ void main() {
group('.fromStream()', () {
test('sets body', () async {
var controller = StreamController<List<int>>(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]);
Expand All @@ -62,8 +62,7 @@ void main() {

test('sets bodyBytes', () async {
var controller = StreamController<List<int>>(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);
Expand All @@ -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']
});
Expand Down

0 comments on commit 5c26219

Please sign in to comment.