Skip to content

Commit b7cdf61

Browse files
Fix Default Encoding for application/json Content-Type in HTTP Responses (#1422)
1 parent aadf836 commit b7cdf61

File tree

6 files changed

+84
-14
lines changed

6 files changed

+84
-14
lines changed

pkgs/http/CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
## 1.3.0
1+
## 1.4.0-wip
2+
3+
* Fixed default encoding for application/json without a charset
4+
to use utf8 instead of latin1, ensuring proper JSON decoding.
5+
6+
## 1.3.0-wip
27

38
* Fixed unintended HTML tags in doc comments.
49
* Switched `BrowserClient` to use Fetch API instead of `XMLHttpRequest`.

pkgs/http/lib/src/multipart_file.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ class MultipartFile {
7373
factory MultipartFile.fromString(String field, String value,
7474
{String? filename, MediaType? contentType}) {
7575
contentType ??= MediaType('text', 'plain');
76-
var encoding = encodingForCharset(contentType.parameters['charset'], utf8);
76+
var encoding = encodingForContentTypeHeader(contentType, utf8);
7777
contentType = contentType.change(parameters: {'charset': encoding.name});
7878

7979
return MultipartFile.fromBytes(field, encoding.encode(value),

pkgs/http/lib/src/response.dart

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ class Response extends BaseResponse {
2121
///
2222
/// This is converted from [bodyBytes] using the `charset` parameter of the
2323
/// `Content-Type` header field, if available. If it's unavailable or if the
24-
/// encoding name is unknown, [latin1] is used by default, as per
25-
/// [RFC 2616][].
24+
/// encoding name is unknown:
25+
/// - [utf8] is used when the content-type is 'application/json' (see [RFC 3629][]).
26+
/// - [latin1] is used in all other cases (see [RFC 2616][])
2627
///
28+
/// [RFC 3629]: https://www.rfc-editor.org/rfc/rfc3629.
2729
/// [RFC 2616]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html
2830
String get body => _encodingForHeaders(headers).decode(bodyBytes);
2931

@@ -66,10 +68,13 @@ class Response extends BaseResponse {
6668

6769
/// Returns the encoding to use for a response with the given headers.
6870
///
69-
/// Defaults to [latin1] if the headers don't specify a charset or if that
70-
/// charset is unknown.
71+
/// If the `Content-Type` header specifies a charset, it will use that charset.
72+
/// If no charset is provided or the charset is unknown:
73+
/// - Defaults to [utf8] if the `Content-Type` is `application/json`
74+
/// (since JSON is defined to use UTF-8 by default).
75+
/// - Otherwise, defaults to [latin1] for compatibility.
7176
Encoding _encodingForHeaders(Map<String, String> headers) =>
72-
encodingForCharset(_contentTypeForHeaders(headers).parameters['charset']);
77+
encodingForContentTypeHeader(_contentTypeForHeaders(headers));
7378

7479
/// Returns the [MediaType] object for the given headers' content-type.
7580
///

pkgs/http/lib/src/utils.dart

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import 'dart:async';
66
import 'dart:convert';
77
import 'dart:typed_data';
88

9+
import 'package:http_parser/http_parser.dart';
10+
911
import 'byte_stream.dart';
1012

1113
/// Converts a [Map] from parameter names to values to a URL query string.
@@ -18,13 +20,27 @@ String mapToQuery(Map<String, String> map, {required Encoding encoding}) =>
1820
'=${Uri.encodeQueryComponent(e.value, encoding: encoding)}')
1921
.join('&');
2022

21-
/// Returns the [Encoding] that corresponds to [charset].
23+
/// Determines the appropriate [Encoding] based on the given [contentTypeHeader]
2224
///
23-
/// Returns [fallback] if [charset] is null or if no [Encoding] was found that
24-
/// corresponds to [charset].
25-
Encoding encodingForCharset(String? charset, [Encoding fallback = latin1]) {
26-
if (charset == null) return fallback;
27-
return Encoding.getByName(charset) ?? fallback;
25+
/// - If the `Content-Type` is `application/json` and no charset is specified,
26+
/// it defaults to [utf8].
27+
/// - If a charset is specified in the parameters,
28+
/// it attempts to find a matching [Encoding].
29+
/// - If no charset is specified or the charset is unknown,
30+
/// it falls back to the provided [fallback], which defaults to [latin1].
31+
Encoding encodingForContentTypeHeader(MediaType contentTypeHeader,
32+
[Encoding fallback = latin1]) {
33+
final charset = contentTypeHeader.parameters['charset'];
34+
35+
// Default to utf8 for application/json when charset is unspecified.
36+
if (contentTypeHeader.type == 'application' &&
37+
contentTypeHeader.subtype == 'json' &&
38+
charset == null) {
39+
return utf8;
40+
}
41+
42+
// Attempt to find the encoding or fall back to the default.
43+
return charset != null ? Encoding.getByName(charset) ?? fallback : fallback;
2844
}
2945

3046
/// Returns the [Encoding] that corresponds to [charset].

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.3.0
2+
version: 1.4.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/response_test.dart

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'dart:async';
6+
import 'dart:convert';
67

78
import 'package:http/http.dart' as http;
89
import 'package:test/test.dart';
@@ -45,6 +46,49 @@ void main() {
4546
headers: {'content-type': 'text/plain; charset=iso-8859-1'});
4647
expect(response.body, equals('föøbãr'));
4748
});
49+
50+
test(
51+
'decoding with empty charset if content type is application/json, Russian text',
52+
() {
53+
final utf8Bytes = utf8.encode('{"foo":"Привет, мир!"}');
54+
var response = http.Response.bytes(utf8Bytes, 200,
55+
headers: {'content-type': 'application/json'});
56+
expect(response.body, equals('{"foo":"Привет, мир!"}'));
57+
});
58+
59+
test(
60+
'test decoding with empty charset if content type is application/json, Chinese text',
61+
() {
62+
final chineseUtf8Bytes = utf8.encode('{"foo":"你好,世界!"}');
63+
var responseChinese = http.Response.bytes(chineseUtf8Bytes, 200,
64+
headers: {'content-type': 'application/json'});
65+
66+
expect(responseChinese.body, equals('{"foo":"你好,世界!"}'));
67+
});
68+
69+
test(
70+
'test decoding with empty charset if content type is application/json, Korean text',
71+
() {
72+
final koreanUtf8Bytes = utf8.encode('{"foo":"안녕하세요, 세계!"}');
73+
var responseKorean = http.Response.bytes(koreanUtf8Bytes, 200,
74+
headers: {'content-type': 'application/json'});
75+
expect(responseKorean.body, equals('{"foo":"안녕하세요, 세계!"}'));
76+
});
77+
78+
test('respects the inferred encoding for application/json content-type',
79+
() {
80+
final latin1Bytes = latin1.encode('{"foo":"Olá, mundo!"}');
81+
var response = http.Response.bytes(latin1Bytes, 200,
82+
headers: {'content-type': 'application/json; charset=iso-8859-1'});
83+
expect(response.body, equals('{"foo":"Olá, mundo!"}'));
84+
});
85+
86+
test('latin1 decode on empty charset if content type is text/plain', () {
87+
final latin1Bytes = latin1.encode('Hello, wold!');
88+
var response = http.Response.bytes(latin1Bytes, 200,
89+
headers: {'content-type': 'text/plain'});
90+
expect(response.body, equals('Hello, wold!'));
91+
});
4892
});
4993

5094
group('.fromStream()', () {

0 commit comments

Comments
 (0)