Skip to content

Commit a286ce7

Browse files
brianquinlanCommit Queue
authored andcommitted
[io]: Fix a bug where HttpResponse.writeln did not honor the charset.
Bug:#59719 Change-Id: Ie2606b557b1f9f50d52de23c56d36b45e52efda1 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/402281 Reviewed-by: Alexander Aprelev <[email protected]> Commit-Queue: Brian Quinlan <[email protected]>
1 parent 6a29e9f commit a286ce7

File tree

3 files changed

+237
-3
lines changed

3 files changed

+237
-3
lines changed

sdk/lib/_http/http.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,6 +1040,10 @@ abstract interface class HttpRequest implements Stream<Uint8List> {
10401040
/// first time, the request header is sent. Calling any methods that
10411041
/// change the header after it is sent throws an exception.
10421042
///
1043+
/// If no "Content-Type" header is set then a default of
1044+
/// "text/plain; charset=utf-8" is used and string data written to the IOSink
1045+
/// will be encoded using UTF-8.
1046+
///
10431047
/// ## Setting the headers
10441048
///
10451049
/// The HttpResponse object has a number of properties for setting up
@@ -1060,8 +1064,9 @@ abstract interface class HttpRequest implements Stream<Uint8List> {
10601064
/// response.headers.add(HttpHeaders.contentTypeHeader, "text/plain");
10611065
/// response.write(...); // Strings written will be ISO-8859-1 encoded.
10621066
///
1063-
/// An exception is thrown if you use the `write()` method
1064-
/// while an unsupported content-type is set.
1067+
/// If a charset is provided but it is not recognized, then the "Content-Type"
1068+
/// header will include that charset but string data will be encoded using
1069+
/// ISO-8859-1 (Latin 1).
10651070
abstract interface class HttpResponse implements IOSink {
10661071
// TODO(ajohnsen): Add documentation of how to pipe a file to the response.
10671072
/// Gets and sets the content length of the response. If the size of

sdk/lib/_http/http_impl.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1129,7 +1129,7 @@ class _IOSinkImpl extends _StreamSinkImpl<List<int>> implements IOSink {
11291129
}
11301130

11311131
void writeln([Object? object = ""]) {
1132-
_writeString('$object\n');
1132+
write('$object\n');
11331133
}
11341134

11351135
void writeCharCode(int charCode) {
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// Tests that the server response body is returned according to defaults or the
6+
// charset set in the "Content-Type" header.
7+
8+
import 'dart:convert';
9+
import 'dart:io';
10+
11+
import "package:expect/expect.dart";
12+
13+
Future<void> testWriteWithoutContentTypeJapanese() async {
14+
final server = await HttpServer.bind(InternetAddress.anyIPv4, 0);
15+
16+
server.first.then((request) {
17+
request.response
18+
..write('日本語')
19+
..close();
20+
});
21+
final request = await HttpClient().get('localhost', server.port, '/');
22+
final response = await request.close();
23+
Expect.listEquals([
24+
'text/plain; charset=utf-8',
25+
], response.headers[HttpHeaders.contentTypeHeader] ?? []);
26+
final body = utf8.decode(await response.fold([], (o, n) => o + n));
27+
Expect.equals('日本語', body);
28+
}
29+
30+
Future<void> testWritelnWithoutContentTypeJapanese() async {
31+
final server = await HttpServer.bind(InternetAddress.anyIPv4, 0);
32+
33+
server.first.then((request) {
34+
request.response
35+
..writeln('日本語')
36+
..close();
37+
});
38+
final request = await HttpClient().get('localhost', server.port, '/');
39+
final response = await request.close();
40+
Expect.listEquals([
41+
'text/plain; charset=utf-8',
42+
], response.headers[HttpHeaders.contentTypeHeader] ?? []);
43+
final body = utf8.decode(await response.fold([], (o, n) => o + n));
44+
Expect.equals('日本語\n', body);
45+
}
46+
47+
Future<void> testWriteAllWithoutContentTypeJapanese() async {
48+
final server = await HttpServer.bind(InternetAddress.anyIPv4, 0);
49+
50+
server.first.then((request) {
51+
request.response
52+
..writeAll(['日', '本', '語'])
53+
..close();
54+
});
55+
final request = await HttpClient().get('localhost', server.port, '/');
56+
final response = await request.close();
57+
Expect.listEquals([
58+
'text/plain; charset=utf-8',
59+
], response.headers[HttpHeaders.contentTypeHeader] ?? []);
60+
final body = utf8.decode(await response.fold([], (o, n) => o + n));
61+
Expect.equals('日本語', body);
62+
}
63+
64+
Future<void> testWriteCharCodeWithoutContentTypeJapanese() async {
65+
final server = await HttpServer.bind(InternetAddress.anyIPv4, 0);
66+
67+
server.first.then((request) {
68+
request.response
69+
..writeCharCode(0x65E5)
70+
..close();
71+
});
72+
final request = await HttpClient().get('localhost', server.port, '/');
73+
final response = await request.close();
74+
Expect.listEquals([
75+
'text/plain; charset=utf-8',
76+
], response.headers[HttpHeaders.contentTypeHeader] ?? []);
77+
final body = utf8.decode(await response.fold([], (o, n) => o + n));
78+
Expect.equals('日', body);
79+
}
80+
81+
Future<void> testWriteWithCharsetJapanese() async {
82+
final server = await HttpServer.bind(InternetAddress.anyIPv4, 0);
83+
84+
server.first.then((request) {
85+
request.response
86+
..headers.contentType = ContentType('text', 'plain', charset: 'utf-8')
87+
..write('日本語')
88+
..close();
89+
});
90+
final request = await HttpClient().get('localhost', server.port, '/');
91+
final response = await request.close();
92+
Expect.listEquals([
93+
'text/plain; charset=utf-8',
94+
], response.headers[HttpHeaders.contentTypeHeader] ?? []);
95+
final body = utf8.decode(await response.fold([], (o, n) => o + n));
96+
Expect.equals('日本語', body);
97+
}
98+
99+
/// Tests for regression: https://github.com/dart-lang/sdk/issues/59719
100+
Future<void> testWritelnWithCharsetJapanese() async {
101+
final server = await HttpServer.bind(InternetAddress.anyIPv4, 0);
102+
103+
server.first.then((request) {
104+
request.response
105+
..headers.contentType = ContentType('text', 'plain', charset: 'utf-8')
106+
..writeln('日本語')
107+
..close();
108+
});
109+
final request = await HttpClient().get('localhost', server.port, '/');
110+
final response = await request.close();
111+
Expect.listEquals([
112+
'text/plain; charset=utf-8',
113+
], response.headers[HttpHeaders.contentTypeHeader] ?? []);
114+
final body = utf8.decode(await response.fold([], (o, n) => o + n));
115+
Expect.equals('日本語\n', body);
116+
}
117+
118+
Future<void> testWriteAllWithCharsetJapanese() async {
119+
final server = await HttpServer.bind(InternetAddress.anyIPv4, 0);
120+
121+
server.first.then((request) {
122+
request.response
123+
..headers.contentType = ContentType('text', 'plain', charset: 'utf-8')
124+
..writeAll(['日', '本', '語'])
125+
..close();
126+
});
127+
final request = await HttpClient().get('localhost', server.port, '/');
128+
final response = await request.close();
129+
Expect.listEquals([
130+
'text/plain; charset=utf-8',
131+
], response.headers[HttpHeaders.contentTypeHeader] ?? []);
132+
final body = utf8.decode(await response.fold([], (o, n) => o + n));
133+
Expect.equals('日本語', body);
134+
}
135+
136+
Future<void> testWriteCharCodeWithCharsetJapanese() async {
137+
final server = await HttpServer.bind(InternetAddress.anyIPv4, 0);
138+
139+
server.first.then((request) {
140+
request.response
141+
..headers.contentType = ContentType('text', 'plain', charset: 'utf-8')
142+
..writeCharCode(0x65E5)
143+
..close();
144+
});
145+
final request = await HttpClient().get('localhost', server.port, '/');
146+
final response = await request.close();
147+
Expect.listEquals([
148+
'text/plain; charset=utf-8',
149+
], response.headers[HttpHeaders.contentTypeHeader] ?? []);
150+
final body = utf8.decode(await response.fold([], (o, n) => o + n));
151+
Expect.equals('日', body);
152+
}
153+
154+
Future<void> testWriteWithoutCharsetGerman() async {
155+
final server = await HttpServer.bind(InternetAddress.anyIPv4, 0);
156+
157+
server.first.then((request) {
158+
request.response
159+
..headers.contentType = ContentType('text', 'plain')
160+
..write('Löscherstraße')
161+
..close();
162+
});
163+
final request = await HttpClient().get('localhost', server.port, '/');
164+
final response = await request.close();
165+
Expect.listEquals([
166+
'text/plain',
167+
], response.headers[HttpHeaders.contentTypeHeader] ?? []);
168+
final body = latin1.decode(await response.fold([], (o, n) => o + n));
169+
Expect.equals('Löscherstraße', body);
170+
}
171+
172+
/// If the charset is not recognized then the text is encoded using ISO-8859-1.
173+
///
174+
/// NOTE: If you change this behavior, make sure that you change the
175+
/// documentation for [HttpResponse].
176+
Future<void> testWriteWithUnrecognizedCharsetGerman() async {
177+
final server = await HttpServer.bind(InternetAddress.anyIPv4, 0);
178+
179+
server.first.then((request) {
180+
request.response
181+
..headers.contentType = ContentType('text', 'plain', charset: '123')
182+
..write('Löscherstraße')
183+
..close();
184+
});
185+
final request = await HttpClient().get('localhost', server.port, '/');
186+
final response = await request.close();
187+
Expect.listEquals([
188+
'text/plain; charset=123',
189+
], response.headers[HttpHeaders.contentTypeHeader] ?? []);
190+
final body = latin1.decode(await response.fold([], (o, n) => o + n));
191+
Expect.equals('Löscherstraße', body);
192+
}
193+
194+
Future<void> testWriteWithoutContentTypeGerman() async {
195+
final server = await HttpServer.bind(InternetAddress.anyIPv4, 0);
196+
197+
server.first.then((request) {
198+
request.response
199+
..write('Löscherstraße')
200+
..close();
201+
});
202+
final request = await HttpClient().get('localhost', server.port, '/');
203+
final response = await request.close();
204+
Expect.listEquals([
205+
'text/plain; charset=utf-8',
206+
], response.headers[HttpHeaders.contentTypeHeader] ?? []);
207+
final body = utf8.decode(await response.fold([], (o, n) => o + n));
208+
Expect.equals('Löscherstraße', body);
209+
}
210+
211+
main() async {
212+
// Japanese, utf-8 (only built-in encoding that supports Japanese)
213+
await testWriteWithoutContentTypeJapanese();
214+
await testWritelnWithoutContentTypeJapanese();
215+
await testWriteAllWithoutContentTypeJapanese();
216+
await testWriteCharCodeWithoutContentTypeJapanese();
217+
218+
await testWriteWithCharsetJapanese();
219+
await testWritelnWithCharsetJapanese();
220+
await testWriteAllWithCharsetJapanese();
221+
await testWriteCharCodeWithCharsetJapanese();
222+
223+
// Write using an invalid or non-utf-8 charset will fail for Japanese.
224+
225+
// German
226+
await testWriteWithoutCharsetGerman();
227+
await testWriteWithUnrecognizedCharsetGerman();
228+
await testWriteWithoutContentTypeGerman();
229+
}

0 commit comments

Comments
 (0)